@sansavision/yousaid 0.0.0-stage → 0.2.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/CHANGELOG.md +13 -0
- package/LICENSE +6 -0
- package/README.md +152 -2
- package/dist/index.d.ts +532 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +383 -0
- package/dist/proof-strings.d.ts +39 -0
- package/dist/proof-strings.d.ts.map +1 -0
- package/dist/proof-strings.js +56 -0
- package/dist/proof-upload.d.ts +96 -0
- package/dist/proof-upload.d.ts.map +1 -0
- package/dist/proof-upload.js +174 -0
- package/dist/proof.d.ts +356 -0
- package/dist/proof.d.ts.map +1 -0
- package/dist/proof.js +278 -0
- package/dist/realtime.d.ts +113 -0
- package/dist/realtime.d.ts.map +1 -0
- package/dist/realtime.js +246 -0
- package/package.json +37 -4
package/dist/index.js
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import { YouSaidProofClient } from "./proof.js";
|
|
2
|
+
import { RoomRealtimeClient } from "./realtime.js";
|
|
3
|
+
export { YouSaidProofClient };
|
|
4
|
+
export { mentionAtCaret, reconnectDelay, RoomRealtimeClient } from "./realtime.js";
|
|
5
|
+
export const YOUSAID_SDK_VERSION = "0.2.0";
|
|
6
|
+
export const SIGNAL_ATTACHMENT_LIMITS = { maxFiles: 3, maxFileBytes: 5 * 1024 * 1024, types: ["image/png", "image/jpeg", "image/webp", "image/gif", "application/pdf", "text/plain", "text/csv", "application/json"] };
|
|
7
|
+
/**
|
|
8
|
+
* Builds a diagnostic capsule from the current page. Query strings are never
|
|
9
|
+
* included. Pass `consent: true` only after the user saw what will be shared.
|
|
10
|
+
*/
|
|
11
|
+
export function collectDiagnostics(options) {
|
|
12
|
+
const hasWindow = typeof window !== "undefined";
|
|
13
|
+
const capsule = { consent: true, sdkVersion: YOUSAID_SDK_VERSION };
|
|
14
|
+
if (hasWindow) {
|
|
15
|
+
capsule.route = window.location.pathname;
|
|
16
|
+
if (document.referrer) {
|
|
17
|
+
try {
|
|
18
|
+
const referrer = new URL(document.referrer);
|
|
19
|
+
capsule.referrer = `${referrer.origin}${referrer.pathname}`;
|
|
20
|
+
}
|
|
21
|
+
catch { /* ignore malformed referrer */ }
|
|
22
|
+
}
|
|
23
|
+
capsule.viewport = `${window.innerWidth}x${window.innerHeight}`;
|
|
24
|
+
}
|
|
25
|
+
if (typeof navigator !== "undefined") {
|
|
26
|
+
capsule.userAgent = navigator.userAgent.slice(0, 400);
|
|
27
|
+
capsule.locale = navigator.language;
|
|
28
|
+
const platform = navigator.userAgentData?.platform;
|
|
29
|
+
if (platform)
|
|
30
|
+
capsule.platform = platform;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
capsule.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
34
|
+
}
|
|
35
|
+
catch { /* ignore */ }
|
|
36
|
+
if (options.appVersion)
|
|
37
|
+
capsule.appVersion = options.appVersion;
|
|
38
|
+
if (options.featureFlags?.length)
|
|
39
|
+
capsule.featureFlags = options.featureFlags.slice(0, 50);
|
|
40
|
+
if (options.breadcrumbs?.length)
|
|
41
|
+
capsule.breadcrumbs = options.breadcrumbs.slice(-30);
|
|
42
|
+
if (options.errors?.length)
|
|
43
|
+
capsule.errors = options.errors.slice(-20);
|
|
44
|
+
if (options.network?.length)
|
|
45
|
+
capsule.network = options.network.slice(-30).map((entry) => ({ ...entry, url: entry.url.split(/[?#]/)[0] ?? "" }));
|
|
46
|
+
if (options.correlationIds?.length)
|
|
47
|
+
capsule.correlationIds = options.correlationIds.slice(0, 10);
|
|
48
|
+
if (options.annotation)
|
|
49
|
+
capsule.annotation = options.annotation.slice(0, 1_000);
|
|
50
|
+
return capsule;
|
|
51
|
+
}
|
|
52
|
+
const VISITOR_KEY = "yousaid-visitor-id";
|
|
53
|
+
/**
|
|
54
|
+
* Installation-scoped pseudonymous visitor id, stored in the host app's
|
|
55
|
+
* localStorage. Append it to hosted YouSaid links as `?visitor=` while the user
|
|
56
|
+
* is anonymous, then call `client.mergeAnonymousHistory()` after sign-in.
|
|
57
|
+
*/
|
|
58
|
+
export function anonymousVisitorId() {
|
|
59
|
+
try {
|
|
60
|
+
const existing = globalThis.localStorage?.getItem(VISITOR_KEY);
|
|
61
|
+
if (existing && /^[a-f0-9-]{20,64}$/i.test(existing))
|
|
62
|
+
return existing;
|
|
63
|
+
const value = crypto.randomUUID();
|
|
64
|
+
globalThis.localStorage?.setItem(VISITOR_KEY, value);
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return crypto.randomUUID();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Incremental Server-Sent Events parser. Feed decoded text; returns complete events.
|
|
73
|
+
* @experimental
|
|
74
|
+
*/
|
|
75
|
+
export function createSseParser() {
|
|
76
|
+
let buffer = "";
|
|
77
|
+
return (chunk) => {
|
|
78
|
+
buffer += chunk.replace(/\r\n/g, "\n");
|
|
79
|
+
const events = [];
|
|
80
|
+
let boundary = buffer.indexOf("\n\n");
|
|
81
|
+
while (boundary !== -1) {
|
|
82
|
+
const block = buffer.slice(0, boundary);
|
|
83
|
+
buffer = buffer.slice(boundary + 2);
|
|
84
|
+
let event = "message";
|
|
85
|
+
const data = [];
|
|
86
|
+
for (const line of block.split("\n")) {
|
|
87
|
+
if (!line || line.startsWith(":"))
|
|
88
|
+
continue;
|
|
89
|
+
const colon = line.indexOf(":");
|
|
90
|
+
const field = colon === -1 ? line : line.slice(0, colon);
|
|
91
|
+
const value = colon === -1 ? "" : line.slice(colon + 1).replace(/^ /, "");
|
|
92
|
+
if (field === "event")
|
|
93
|
+
event = value;
|
|
94
|
+
else if (field === "data")
|
|
95
|
+
data.push(value);
|
|
96
|
+
}
|
|
97
|
+
if (data.length)
|
|
98
|
+
events.push({ event, data: data.join("\n") });
|
|
99
|
+
boundary = buffer.indexOf("\n\n");
|
|
100
|
+
}
|
|
101
|
+
return events;
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* A failed YouSaid API call. `code` is the typed error code when the endpoint returns one.
|
|
106
|
+
* @experimental
|
|
107
|
+
*/
|
|
108
|
+
export class YouSaidApiError extends Error {
|
|
109
|
+
status;
|
|
110
|
+
code;
|
|
111
|
+
constructor(message, status, code = "") {
|
|
112
|
+
super(message);
|
|
113
|
+
this.status = status;
|
|
114
|
+
this.code = code;
|
|
115
|
+
this.name = "YouSaidApiError";
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
export class YouSaidClient {
|
|
119
|
+
#options;
|
|
120
|
+
constructor(options) {
|
|
121
|
+
this.#options = options;
|
|
122
|
+
}
|
|
123
|
+
async sendSignal(input) {
|
|
124
|
+
const token = typeof this.#options.token === "function" ? await this.#options.token() : this.#options.token;
|
|
125
|
+
const request = this.#options.fetch ?? globalThis.fetch;
|
|
126
|
+
const endpoint = this.#options.endpoint ?? "https://yousaid.dev/api/signals";
|
|
127
|
+
const signalJson = JSON.stringify({
|
|
128
|
+
kind: input.kind ?? "support",
|
|
129
|
+
title: input.title,
|
|
130
|
+
body: input.body,
|
|
131
|
+
idempotencyKey: input.idempotencyKey ?? crypto.randomUUID(),
|
|
132
|
+
context: input.context,
|
|
133
|
+
...(input.diagnostics ? { diagnostics: input.diagnostics } : {}),
|
|
134
|
+
...(input.visitorId ? { visitorId: input.visitorId } : {}),
|
|
135
|
+
});
|
|
136
|
+
const files = input.attachments ?? [];
|
|
137
|
+
let response;
|
|
138
|
+
if (files.length || input.screenshot) {
|
|
139
|
+
// Multipart: the signal JSON travels in `payload`; never set content-type so the boundary is added.
|
|
140
|
+
const form = new FormData();
|
|
141
|
+
form.set("payload", signalJson);
|
|
142
|
+
for (const file of files)
|
|
143
|
+
form.append("files", file, file.name);
|
|
144
|
+
if (input.screenshot)
|
|
145
|
+
form.set("screenshot", input.screenshot, input.screenshot.name || "screenshot.png");
|
|
146
|
+
response = await request(endpoint, { method: "POST", headers: { authorization: `Bearer ${token}` }, body: form });
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
response = await request(endpoint, {
|
|
150
|
+
method: "POST",
|
|
151
|
+
headers: {
|
|
152
|
+
authorization: `Bearer ${token}`,
|
|
153
|
+
"content-type": "application/json",
|
|
154
|
+
},
|
|
155
|
+
body: signalJson,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
const payload = await response.json();
|
|
159
|
+
if (!response.ok)
|
|
160
|
+
throw new Error("error" in payload && payload.error ? payload.error : "YouSaid could not accept the signal.");
|
|
161
|
+
return payload;
|
|
162
|
+
}
|
|
163
|
+
/** Install check: verifies the signed user and records SDK health for the onboarding "verify install" step. */
|
|
164
|
+
async verifyInstallation() {
|
|
165
|
+
return this.#request(`/api/v1/installation?sdk=${encodeURIComponent(YOUSAID_SDK_VERSION)}`);
|
|
166
|
+
}
|
|
167
|
+
/** Links this browser's anonymous history (hosted forms, surveys) to the signed user. */
|
|
168
|
+
async mergeAnonymousHistory(visitorId = anonymousVisitorId()) {
|
|
169
|
+
return this.#request("/api/v1/identity/merge", { method: "POST", body: JSON.stringify({ visitorId }) });
|
|
170
|
+
}
|
|
171
|
+
/** End-user confirmation after a release: did it resolve their problem? */
|
|
172
|
+
async confirmRelease(releaseId, outcome, note) {
|
|
173
|
+
return this.#request(`/api/v1/releases/${encodeURIComponent(releaseId)}/confirmation`, { method: "POST", body: JSON.stringify({ outcome, ...(note ? { note } : {}) }) });
|
|
174
|
+
}
|
|
175
|
+
/** @experimental */
|
|
176
|
+
async listRooms() {
|
|
177
|
+
const payload = await this.#request("/api/v1/rooms");
|
|
178
|
+
return payload.rooms;
|
|
179
|
+
}
|
|
180
|
+
/** @experimental */
|
|
181
|
+
async createRoom(input) {
|
|
182
|
+
return this.#request("/api/v1/rooms", { method: "POST", body: JSON.stringify({ ...input, idempotencyKey: input.idempotencyKey ?? crypto.randomUUID() }) });
|
|
183
|
+
}
|
|
184
|
+
/** @experimental */
|
|
185
|
+
async getRoom(roomId) {
|
|
186
|
+
return this.#request(`/api/v1/rooms/${encodeURIComponent(roomId)}`);
|
|
187
|
+
}
|
|
188
|
+
/** @experimental */
|
|
189
|
+
async postRoomMessage(roomId, body, options = {}) {
|
|
190
|
+
return this.#request(`/api/v1/rooms/${encodeURIComponent(roomId)}/messages`, { method: "POST", body: JSON.stringify({ body, threadRootId: options.threadRootId ?? null, idempotencyKey: options.idempotencyKey ?? crypto.randomUUID() }) });
|
|
191
|
+
}
|
|
192
|
+
/** @experimental */
|
|
193
|
+
async editRoomMessage(roomId, messageId, body) {
|
|
194
|
+
return this.#request(`/api/v1/rooms/${encodeURIComponent(roomId)}/messages/${encodeURIComponent(messageId)}`, { method: "PATCH", body: JSON.stringify({ body, idempotencyKey: crypto.randomUUID() }) });
|
|
195
|
+
}
|
|
196
|
+
/** @experimental */
|
|
197
|
+
async deleteRoomMessage(roomId, messageId) {
|
|
198
|
+
await this.#request(`/api/v1/rooms/${encodeURIComponent(roomId)}/messages/${encodeURIComponent(messageId)}`, { method: "DELETE", headers: { "idempotency-key": crypto.randomUUID() } });
|
|
199
|
+
}
|
|
200
|
+
/** @experimental */
|
|
201
|
+
async uploadRoomAttachment(roomId, file) {
|
|
202
|
+
const form = new FormData();
|
|
203
|
+
form.set("file", file);
|
|
204
|
+
return this.#request(`/api/v1/rooms/${encodeURIComponent(roomId)}/attachments`, { method: "POST", body: form, headers: { "idempotency-key": crypto.randomUUID() } });
|
|
205
|
+
}
|
|
206
|
+
/** @experimental */
|
|
207
|
+
async downloadRoomAttachment(roomId, attachmentId) {
|
|
208
|
+
const token = typeof this.#options.token === "function" ? await this.#options.token() : this.#options.token;
|
|
209
|
+
const request = this.#options.fetch ?? globalThis.fetch;
|
|
210
|
+
const response = await request(this.#apiUrl(`/api/v1/rooms/${encodeURIComponent(roomId)}/attachments/${encodeURIComponent(attachmentId)}`), { headers: { authorization: `Bearer ${token}` } });
|
|
211
|
+
if (!response.ok)
|
|
212
|
+
throw new Error("The Room attachment is unavailable.");
|
|
213
|
+
return response.blob();
|
|
214
|
+
}
|
|
215
|
+
/** @experimental */
|
|
216
|
+
async reactToRoomMessage(roomId, messageId, emoji, active = true) {
|
|
217
|
+
await this.#request(`/api/v1/rooms/${encodeURIComponent(roomId)}/messages/${encodeURIComponent(messageId)}/reactions`, { method: "POST", body: JSON.stringify({ emoji, active, idempotencyKey: crypto.randomUUID() }) });
|
|
218
|
+
}
|
|
219
|
+
/** @experimental */
|
|
220
|
+
async updateRoomNotifications(roomId, notificationMode, timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC") {
|
|
221
|
+
await this.#request(`/api/v1/rooms/${encodeURIComponent(roomId)}`, { method: "PATCH", body: JSON.stringify({ notificationMode, timezone }) });
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Catch up by sequence over HTTP. When far behind, `snapshot` carries the collapsed state to rebuild from.
|
|
225
|
+
* @experimental
|
|
226
|
+
*/
|
|
227
|
+
async roomEvents(roomId, afterSequence = 0, limit = 200) {
|
|
228
|
+
return this.#request(`/api/v1/rooms/${encodeURIComponent(roomId)}/events?after_seq=${Math.max(0, afterSequence)}&limit=${Math.min(500, Math.max(1, limit))}`);
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* A realtime connection that reconnects with backoff and catches up by sequence. Call start() to connect.
|
|
232
|
+
* @experimental
|
|
233
|
+
*/
|
|
234
|
+
realtime(roomId, options = {}) {
|
|
235
|
+
const origin = new URL(this.#options.endpoint ?? "https://yousaid.dev", "https://yousaid.dev").origin;
|
|
236
|
+
return new RoomRealtimeClient({ origin, ...options, roomId, connect: options.connect ?? (async () => (await this.getRoom(roomId)).realtime) });
|
|
237
|
+
}
|
|
238
|
+
/** @experimental */
|
|
239
|
+
async getRoomCanvas(roomId) {
|
|
240
|
+
return (await this.#request(`/api/v1/rooms/${encodeURIComponent(roomId)}/canvas`)).data;
|
|
241
|
+
}
|
|
242
|
+
/** @experimental */
|
|
243
|
+
async listRoomMembers(roomId) {
|
|
244
|
+
const payload = await this.#request(`/api/v1/rooms/${encodeURIComponent(roomId)}/members`);
|
|
245
|
+
return { members: payload.data, memberCount: payload.memberCount };
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* "Talk to a person": acknowledged immediately, pauses agent replies to you, and notifies the team.
|
|
249
|
+
* @experimental
|
|
250
|
+
*/
|
|
251
|
+
async requestHuman(roomId, reason = "", idempotencyKey = crypto.randomUUID()) {
|
|
252
|
+
return (await this.#request(`/api/v1/rooms/${encodeURIComponent(roomId)}/handoff`, { method: "POST", body: JSON.stringify({ reason }), headers: { "idempotency-key": idempotencyKey } })).data;
|
|
253
|
+
}
|
|
254
|
+
/** @experimental */
|
|
255
|
+
async getHumanHandoff(roomId) {
|
|
256
|
+
return (await this.#request(`/api/v1/rooms/${encodeURIComponent(roomId)}/handoff`)).data;
|
|
257
|
+
}
|
|
258
|
+
/** @experimental */
|
|
259
|
+
async withdrawHumanHandoff(roomId) {
|
|
260
|
+
return (await this.#request(`/api/v1/rooms/${encodeURIComponent(roomId)}/handoff`, { method: "DELETE" })).data;
|
|
261
|
+
}
|
|
262
|
+
/** @experimental */
|
|
263
|
+
async invokeAgent(roomId, alias, request, idempotencyKey = crypto.randomUUID()) {
|
|
264
|
+
return (await this.#request(`/api/v1/rooms/${encodeURIComponent(roomId)}/agents/${alias}/invoke`, { method: "POST", body: JSON.stringify({ request }), headers: { "idempotency-key": idempotencyKey } })).data;
|
|
265
|
+
}
|
|
266
|
+
/** @experimental */
|
|
267
|
+
async getAgentRun(runId) {
|
|
268
|
+
return (await this.#request(`/api/v1/agent-runs/${encodeURIComponent(runId)}`)).data;
|
|
269
|
+
}
|
|
270
|
+
/** @experimental */
|
|
271
|
+
async cancelAgentRun(runId) {
|
|
272
|
+
return (await this.#request(`/api/v1/agent-runs/${encodeURIComponent(runId)}/cancel`, { method: "POST", body: "{}", headers: { "idempotency-key": `cancel-${runId}` } })).data;
|
|
273
|
+
}
|
|
274
|
+
/** @experimental */
|
|
275
|
+
async listPendingApprovals(roomId) {
|
|
276
|
+
return (await this.#request(`/api/v1/approvals?status=pending${roomId ? `&room_id=${encodeURIComponent(roomId)}` : ""}`)).data;
|
|
277
|
+
}
|
|
278
|
+
/** @experimental */
|
|
279
|
+
async searchRooms(query, options = {}) {
|
|
280
|
+
const params = new URLSearchParams({ q: query });
|
|
281
|
+
if (options.roomId)
|
|
282
|
+
params.set("room_id", options.roomId);
|
|
283
|
+
if (options.types?.length)
|
|
284
|
+
params.set("type", options.types.join(","));
|
|
285
|
+
if (options.limit)
|
|
286
|
+
params.set("limit", String(options.limit));
|
|
287
|
+
const payload = await this.#request(`/api/v1/rooms/search?${params}`);
|
|
288
|
+
return { results: payload.data, mode: payload.mode };
|
|
289
|
+
}
|
|
290
|
+
/** @experimental */
|
|
291
|
+
async joinRoom(roomId, invitationToken) {
|
|
292
|
+
await this.#request(`/api/v1/rooms/${encodeURIComponent(roomId)}/join`, { method: "POST", body: JSON.stringify({ invitationToken: invitationToken ?? null }) });
|
|
293
|
+
}
|
|
294
|
+
/** @experimental */
|
|
295
|
+
async leaveRoom(roomId) {
|
|
296
|
+
await this.#request(`/api/v1/rooms/${encodeURIComponent(roomId)}/leave`, { method: "POST", body: "{}" });
|
|
297
|
+
}
|
|
298
|
+
/** @experimental */
|
|
299
|
+
async respondToRoomPoll(roomId, pollId, input) {
|
|
300
|
+
await this.#request(`/api/v1/rooms/${encodeURIComponent(roomId)}/polls/${encodeURIComponent(pollId)}/responses`, { method: "PUT", body: JSON.stringify({ optionIds: input.optionIds ?? [], answer: input.answer ?? null, ranking: input.ranking ?? [] }), headers: { "idempotency-key": crypto.randomUUID() } });
|
|
301
|
+
}
|
|
302
|
+
/** @experimental */
|
|
303
|
+
async joinSolveSession(roomId, sessionId) {
|
|
304
|
+
return this.#request(`/api/v1/rooms/${encodeURIComponent(roomId)}/solve-sessions/${encodeURIComponent(sessionId)}/join`, { method: "POST", body: "{}" });
|
|
305
|
+
}
|
|
306
|
+
/** @experimental */
|
|
307
|
+
async reportRoomContent(roomId, input, idempotencyKey = crypto.randomUUID()) {
|
|
308
|
+
return this.#request(`/api/v1/rooms/${encodeURIComponent(roomId)}/moderation/reports`, { method: "POST", body: JSON.stringify(input), headers: { "idempotency-key": idempotencyKey } });
|
|
309
|
+
}
|
|
310
|
+
/** @experimental */
|
|
311
|
+
async appealModerationCase(roomId, caseId, statement, idempotencyKey = crypto.randomUUID()) {
|
|
312
|
+
return this.#request(`/api/v1/rooms/${encodeURIComponent(roomId)}/moderation/${encodeURIComponent(caseId)}/appeals`, { method: "POST", body: JSON.stringify({ statement }), headers: { "idempotency-key": idempotencyKey } });
|
|
313
|
+
}
|
|
314
|
+
async listInbox(options = {}) {
|
|
315
|
+
const query = new URLSearchParams();
|
|
316
|
+
if (options.limit)
|
|
317
|
+
query.set("limit", String(options.limit));
|
|
318
|
+
if (options.before)
|
|
319
|
+
query.set("before", options.before);
|
|
320
|
+
return this.#request(`/api/v1/inbox${query.size ? `?${query}` : ""}`);
|
|
321
|
+
}
|
|
322
|
+
async markInboxRead(ids) {
|
|
323
|
+
return this.#request("/api/v1/inbox/read", { method: "POST", body: JSON.stringify(ids ? { ids } : {}) });
|
|
324
|
+
}
|
|
325
|
+
/** Stream live inbox events until `signal` aborts. Resolves when the server closes the stream. */
|
|
326
|
+
async streamInbox(onEvent, signal) {
|
|
327
|
+
const token = typeof this.#options.token === "function" ? await this.#options.token() : this.#options.token;
|
|
328
|
+
const request = this.#options.fetch ?? globalThis.fetch;
|
|
329
|
+
const response = await request(this.#apiUrl("/api/v1/inbox/stream"), { headers: { authorization: `Bearer ${token}`, accept: "text/event-stream" }, ...(signal ? { signal } : {}) });
|
|
330
|
+
if (!response.ok || !response.body)
|
|
331
|
+
throw new Error("The inbox stream is unavailable.");
|
|
332
|
+
const reader = response.body.getReader();
|
|
333
|
+
const decoder = new TextDecoder();
|
|
334
|
+
const parse = createSseParser();
|
|
335
|
+
while (true) {
|
|
336
|
+
const { done, value } = await reader.read();
|
|
337
|
+
if (done)
|
|
338
|
+
return;
|
|
339
|
+
for (const event of parse(decoder.decode(value, { stream: true }))) {
|
|
340
|
+
try {
|
|
341
|
+
onEvent({ type: event.event, ...JSON.parse(event.data) });
|
|
342
|
+
}
|
|
343
|
+
catch { /* ignore malformed frames */ }
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Proof collection (in-app prompt, known-customer sessions, preferences).
|
|
349
|
+
* @experimental
|
|
350
|
+
*/
|
|
351
|
+
proof() {
|
|
352
|
+
return new YouSaidProofClient(this.#options);
|
|
353
|
+
}
|
|
354
|
+
async #request(path, init = {}) {
|
|
355
|
+
const token = typeof this.#options.token === "function" ? await this.#options.token() : this.#options.token;
|
|
356
|
+
const request = this.#options.fetch ?? globalThis.fetch;
|
|
357
|
+
const headers = new Headers(init.headers);
|
|
358
|
+
headers.set("authorization", `Bearer ${token}`);
|
|
359
|
+
if (!(init.body instanceof FormData))
|
|
360
|
+
headers.set("content-type", "application/json");
|
|
361
|
+
const response = await request(this.#apiUrl(path), { ...init, headers });
|
|
362
|
+
if (response.status === 204)
|
|
363
|
+
return undefined;
|
|
364
|
+
const payload = await response.json();
|
|
365
|
+
if (!response.ok) {
|
|
366
|
+
const error = payload.error;
|
|
367
|
+
if (typeof error === "string")
|
|
368
|
+
throw new YouSaidApiError(error, response.status);
|
|
369
|
+
if (error && typeof error === "object")
|
|
370
|
+
throw new YouSaidApiError(String(error.message ?? "YouSaid could not complete the Room request."), response.status, String(error.code ?? ""));
|
|
371
|
+
throw new YouSaidApiError("YouSaid could not complete the Room request.", response.status);
|
|
372
|
+
}
|
|
373
|
+
return payload;
|
|
374
|
+
}
|
|
375
|
+
#apiUrl(path) {
|
|
376
|
+
const configured = this.#options.endpoint ?? "https://yousaid.dev";
|
|
377
|
+
const url = new URL(configured, "https://yousaid.dev");
|
|
378
|
+
return `${url.origin}${path}`;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
export * from "./proof.js";
|
|
382
|
+
export * from "./proof-upload.js";
|
|
383
|
+
export * from "./proof-strings.js";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Widget text and the world outline shared by the React SDK and the
|
|
3
|
+
* <yousaid-proof> Web Component (public/embed/yousaid-proof.js keeps its own
|
|
4
|
+
* copy because it has no build step; tests/r5-map-bubble.test.ts checks that
|
|
5
|
+
* both stay identical). Plain data: no network, no storage.
|
|
6
|
+
*/
|
|
7
|
+
/** @experimental */
|
|
8
|
+
export type ProofStringKey = "about" | "approved" | "imported" | "permission" | "customer" | "stories" | "prev" | "next" | "pauseRotation" | "rated" | "storyOne" | "storyMany" | "from" | "via" | "videoFrom" | "video" | "audio" | "strip" | "map" | "places" | "allPlaces" | "storiesFrom" | "fromPlace" | "watch" | "watchFrom" | "close" | "dismiss" | "pause" | "play";
|
|
9
|
+
/**
|
|
10
|
+
* Supported widget languages; anything else falls back to English.
|
|
11
|
+
* @experimental
|
|
12
|
+
*/
|
|
13
|
+
export declare const PROOF_STRINGS: Record<string, Record<ProofStringKey, string>>;
|
|
14
|
+
/**
|
|
15
|
+
* The first supported base language among the candidates (a locale prop, the page language, the browser), else "en".
|
|
16
|
+
* @experimental
|
|
17
|
+
*/
|
|
18
|
+
export declare function proofLanguage(...candidates: Array<string | null | undefined>): string;
|
|
19
|
+
/** @experimental */
|
|
20
|
+
export declare function proofText(language: string, key: ProofStringKey, vars?: Record<string, string | number>): string;
|
|
21
|
+
/**
|
|
22
|
+
* Stylized world outline: polygons of "longitude latitude" pairs. No tiles, no third-party data.
|
|
23
|
+
* @experimental
|
|
24
|
+
*/
|
|
25
|
+
export declare const WORLD_OUTLINE = "-167 66,-156 71,-130 70,-110 72,-85 72,-68 62,-60 55,-55 48,-66 45,-70 41,-76 35,-81 31,-80 25,-83 29,-90 29,-97 26,-97 21,-92 18,-88 21,-87 16,-83 10,-78 8,-80 7,-85 11,-92 14,-105 20,-110 24,-112 31,-117 32,-124 40,-124 48,-133 55,-146 60,-153 57,-165 60;-73 78,-60 82,-30 83,-20 76,-22 70,-42 60,-50 64,-55 70,-66 76;-85 22,-80 23,-74 20,-78 20;-74 19,-69 19,-68 18,-72 18;-80 8,-72 12,-62 10,-52 5,-44 -2,-35 -6,-39 -15,-41 -22,-48 -26,-53 -34,-58 -38,-62 -40,-65 -45,-68 -52,-73 -53,-75 -46,-73 -37,-71 -28,-70 -18,-76 -14,-81 -5,-80 0,-77 4;-10 36,-9 43,-2 44,-4 48,2 51,5 53,8 54,9 57,5 58,5 62,12 65,18 70,28 71,40 68,45 68,60 70,60 55,50 47,40 45,36 45,28 41,26 40,23 36,21 40,19 42,14 45,12 44,16 41,18 40,16 38,15 40,12 42,9 44,3 43,-1 37,-6 36;-6 50,-5 58,-3 59,0 53,1 51;-10 52,-8 55,-6 54,-6 52;-24 64,-22 66,-14 66,-14 64;-17 21,-16 28,-10 30,-6 36,10 37,11 33,20 31,30 31,33 30,36 22,39 15,43 12,51 12,48 5,40 -3,40 -10,35 -20,33 -26,27 -34,20 -35,18 -30,12 -17,13 -8,9 0,9 4,4 6,-8 4,-13 8,-17 15;44 -25,47 -25,50 -15,48 -13,44 -17;28 41,36 37,35 33,34 28,39 21,43 13,45 13,52 16,55 17,58 21,56 24,52 24,50 26,48 30,51 28,56 27,58 25,62 25,67 24,72 21,77 8,80 10,80 16,87 22,92 22,98 16,98 9,100 6,101 3,104 1,103 5,100 13,103 12,105 9,109 11,108 16,106 20,110 21,117 24,122 31,119 35,122 40,126 37,129 35,129 42,135 43,141 48,140 53,135 55,143 59,155 59,163 62,172 65,180 66,180 71,160 70,140 73,113 74,100 77,80 73,70 73,60 70,60 55,50 47,40 45,36 45;80 6,82 7,81 9,80 9;120 22,121 25,122 25,121 22;130 31,132 34,135 34,140 36,142 40,141 45,145 44,140 42,139 38,136 36,133 36,130 34;95 5,98 4,104 -2,106 -6,101 -3,96 2;109 1,113 3,117 7,119 5,117 1,116 -4,111 -3,109 -1;105 -6,110 -7,114 -8,110 -8,106 -7;120 18,122 18,124 12,126 7,122 7,120 14;131 -1,141 -2,147 -6,150 -10,141 -9,138 -8,132 -4;114 -22,114 -34,117 -35,123 -34,131 -31,138 -35,141 -38,147 -39,150 -37,153 -32,153 -25,146 -19,145 -15,142 -11,141 -17,136 -12,132 -11,129 -15,126 -14,122 -18;145 -41,148 -41,148 -43,146 -44;172 -34,175 -37,178 -38,176 -41,174 -41,173 -38;172 -41,174 -41,171 -44,167 -46,168 -44";
|
|
26
|
+
/**
|
|
27
|
+
* SVG path for WORLD_OUTLINE in a 360 x 142 equirectangular box cropped to 84N..58S (x = lon + 180, y = 84 - lat).
|
|
28
|
+
* @experimental
|
|
29
|
+
*/
|
|
30
|
+
export declare function worldOutlinePath(): string;
|
|
31
|
+
/**
|
|
32
|
+
* Marker position for a coarse location in the same box.
|
|
33
|
+
* @experimental
|
|
34
|
+
*/
|
|
35
|
+
export declare function mapPoint(lat: number, lon: number): {
|
|
36
|
+
x: number;
|
|
37
|
+
y: number;
|
|
38
|
+
};
|
|
39
|
+
//# sourceMappingURL=proof-strings.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proof-strings.d.ts","sourceRoot":"","sources":["../src/proof-strings.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,oBAAoB;AACpB,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,UAAU,GAAG,UAAU,GAAG,YAAY,GAAG,UAAU,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,eAAe,GAAG,OAAO,GAAG,UAAU,GAAG,WAAW,GAAG,MAAM,GAAG,KAAK,GAAG,WAAW,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,KAAK,GAAG,QAAQ,GAAG,WAAW,GAAG,aAAa,GAAG,WAAW,GAAG,OAAO,GAAG,WAAW,GAAG,OAAO,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,CAAC;AAE7W;;;GAGG;AACH,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,cAAc,EAAE,MAAM,CAAC,CAQxE,CAAC;AAEF;;;GAGG;AACH,wBAAgB,aAAa,CAAC,GAAG,UAAU,EAAE,KAAK,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,GAAG,MAAM,CAMrF;AAED,oBAAoB;AACpB,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,cAAc,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,GAAG,MAAM,CAG/G;AAED;;;GAGG;AACH,eAAO,MAAM,aAAa,kjEAAkjE,CAAC;AAE7kE;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAG3E"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Widget text and the world outline shared by the React SDK and the
|
|
3
|
+
* <yousaid-proof> Web Component (public/embed/yousaid-proof.js keeps its own
|
|
4
|
+
* copy because it has no build step; tests/r5-map-bubble.test.ts checks that
|
|
5
|
+
* both stay identical). Plain data: no network, no storage.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Supported widget languages; anything else falls back to English.
|
|
9
|
+
* @experimental
|
|
10
|
+
*/
|
|
11
|
+
export const PROOF_STRINGS = {
|
|
12
|
+
"en": { "about": "About this proof", "approved": "Approved by the customer", "imported": "Imported public review", "permission": "Shared with permission", "customer": "Customer", "stories": "Customer stories", "prev": "Previous story", "next": "Next story", "pauseRotation": "Pause rotation", "rated": "Rated {n} out of 5", "storyOne": "customer story", "storyMany": "customer stories", "from": "from", "via": "Shared with permission via", "videoFrom": "Video story from {name}", "video": "Customer video story", "audio": "Customer audio story", "strip": "Customers who shared their story", "map": "Map of customer stories by country or region", "places": "Places", "allPlaces": "All places", "storiesFrom": "Stories from {place}", "fromPlace": "From {place}", "watch": "Watch a customer video story", "watchFrom": "Watch a video story from {name}", "close": "Close video", "dismiss": "Hide video stories", "pause": "Pause scrolling", "play": "Resume scrolling" },
|
|
13
|
+
"es": { "about": "Sobre esta opinión", "approved": "Aprobado por el cliente", "imported": "Reseña pública importada", "permission": "Compartido con permiso", "customer": "Cliente", "stories": "Historias de clientes", "prev": "Historia anterior", "next": "Historia siguiente", "pauseRotation": "Pausar rotación", "rated": "Valoración: {n} de 5", "storyOne": "historia de cliente", "storyMany": "historias de clientes", "from": "de", "via": "Compartido con permiso mediante", "videoFrom": "Historia en video de {name}", "video": "Historia en video de un cliente", "audio": "Historia en audio de un cliente", "strip": "Clientes que compartieron su historia", "map": "Mapa de historias de clientes por país o región", "places": "Lugares", "allPlaces": "Todos los lugares", "storiesFrom": "Historias de {place}", "fromPlace": "Desde {place}", "watch": "Ver una historia en video de un cliente", "watchFrom": "Ver la historia en video de {name}", "close": "Cerrar video", "dismiss": "Ocultar historias en video", "pause": "Pausar desplazamiento", "play": "Reanudar desplazamiento" },
|
|
14
|
+
"fr": { "about": "À propos de ce témoignage", "approved": "Approuvé par le client", "imported": "Avis public importé", "permission": "Partagé avec autorisation", "customer": "Client", "stories": "Témoignages clients", "prev": "Témoignage précédent", "next": "Témoignage suivant", "pauseRotation": "Mettre la rotation en pause", "rated": "Noté {n} sur 5", "storyOne": "témoignage client", "storyMany": "témoignages clients", "from": "sur", "via": "Partagé avec autorisation via", "videoFrom": "Témoignage vidéo de {name}", "video": "Témoignage vidéo d'un client", "audio": "Témoignage audio d'un client", "strip": "Clients qui ont partagé leur témoignage", "map": "Carte des témoignages clients par pays ou région", "places": "Lieux", "allPlaces": "Tous les lieux", "storiesFrom": "Témoignages de {place}", "fromPlace": "Depuis {place}", "watch": "Regarder un témoignage vidéo", "watchFrom": "Regarder le témoignage vidéo de {name}", "close": "Fermer la vidéo", "dismiss": "Masquer les témoignages vidéo", "pause": "Mettre le défilement en pause", "play": "Reprendre le défilement" },
|
|
15
|
+
"de": { "about": "Über diesen Nachweis", "approved": "Vom Kunden freigegeben", "imported": "Importierte öffentliche Bewertung", "permission": "Mit Erlaubnis geteilt", "customer": "Kunde", "stories": "Kundenstimmen", "prev": "Vorherige Stimme", "next": "Nächste Stimme", "pauseRotation": "Wechsel anhalten", "rated": "Bewertet mit {n} von 5", "storyOne": "Kundenstimme", "storyMany": "Kundenstimmen", "from": "aus", "via": "Mit Erlaubnis geteilt über", "videoFrom": "Videostimme von {name}", "video": "Video einer Kundenstimme", "audio": "Audio einer Kundenstimme", "strip": "Kunden, die ihre Erfahrung geteilt haben", "map": "Karte der Kundenstimmen nach Land oder Region", "places": "Orte", "allPlaces": "Alle Orte", "storiesFrom": "Stimmen aus {place}", "fromPlace": "Aus {place}", "watch": "Video einer Kundenstimme ansehen", "watchFrom": "Videostimme von {name} ansehen", "close": "Video schließen", "dismiss": "Videostimmen ausblenden", "pause": "Laufband anhalten", "play": "Laufband fortsetzen" },
|
|
16
|
+
"pt": { "about": "Sobre este depoimento", "approved": "Aprovado pelo cliente", "imported": "Avaliação pública importada", "permission": "Compartilhado com permissão", "customer": "Cliente", "stories": "Histórias de clientes", "prev": "História anterior", "next": "Próxima história", "pauseRotation": "Pausar rotação", "rated": "Nota {n} de 5", "storyOne": "história de cliente", "storyMany": "histórias de clientes", "from": "de", "via": "Compartilhado com permissão via", "videoFrom": "História em vídeo de {name}", "video": "História em vídeo de um cliente", "audio": "História em áudio de um cliente", "strip": "Clientes que compartilharam sua história", "map": "Mapa de histórias de clientes por país ou região", "places": "Lugares", "allPlaces": "Todos os lugares", "storiesFrom": "Histórias de {place}", "fromPlace": "De {place}", "watch": "Assistir a uma história em vídeo", "watchFrom": "Assistir à história em vídeo de {name}", "close": "Fechar vídeo", "dismiss": "Ocultar histórias em vídeo", "pause": "Pausar rolagem", "play": "Retomar rolagem" },
|
|
17
|
+
"it": { "about": "Informazioni su questa testimonianza", "approved": "Approvato dal cliente", "imported": "Recensione pubblica importata", "permission": "Condiviso con permesso", "customer": "Cliente", "stories": "Storie dei clienti", "prev": "Storia precedente", "next": "Storia successiva", "pauseRotation": "Metti in pausa la rotazione", "rated": "Valutazione {n} su 5", "storyOne": "storia di un cliente", "storyMany": "storie di clienti", "from": "da", "via": "Condiviso con permesso tramite", "videoFrom": "Storia video di {name}", "video": "Storia video di un cliente", "audio": "Storia audio di un cliente", "strip": "Clienti che hanno condiviso la loro storia", "map": "Mappa delle storie dei clienti per paese o regione", "places": "Luoghi", "allPlaces": "Tutti i luoghi", "storiesFrom": "Storie da {place}", "fromPlace": "Da {place}", "watch": "Guarda una storia video di un cliente", "watchFrom": "Guarda la storia video di {name}", "close": "Chiudi video", "dismiss": "Nascondi le storie video", "pause": "Metti in pausa lo scorrimento", "play": "Riprendi lo scorrimento" },
|
|
18
|
+
"nl": { "about": "Over dit bewijs", "approved": "Goedgekeurd door de klant", "imported": "Geïmporteerde openbare review", "permission": "Gedeeld met toestemming", "customer": "Klant", "stories": "Klantverhalen", "prev": "Vorig verhaal", "next": "Volgend verhaal", "pauseRotation": "Wisselen pauzeren", "rated": "Beoordeeld met {n} van 5", "storyOne": "klantverhaal", "storyMany": "klantverhalen", "from": "uit", "via": "Gedeeld met toestemming via", "videoFrom": "Videoverhaal van {name}", "video": "Videoverhaal van een klant", "audio": "Audioverhaal van een klant", "strip": "Klanten die hun verhaal deelden", "map": "Kaart van klantverhalen per land of regio", "places": "Plaatsen", "allPlaces": "Alle plaatsen", "storiesFrom": "Verhalen uit {place}", "fromPlace": "Uit {place}", "watch": "Bekijk een videoverhaal van een klant", "watchFrom": "Bekijk het videoverhaal van {name}", "close": "Video sluiten", "dismiss": "Videoverhalen verbergen", "pause": "Scrollen pauzeren", "play": "Scrollen hervatten" },
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* The first supported base language among the candidates (a locale prop, the page language, the browser), else "en".
|
|
22
|
+
* @experimental
|
|
23
|
+
*/
|
|
24
|
+
export function proofLanguage(...candidates) {
|
|
25
|
+
for (const candidate of candidates) {
|
|
26
|
+
const base = String(candidate ?? "").toLowerCase().split(/[-_]/)[0] ?? "";
|
|
27
|
+
if (PROOF_STRINGS[base])
|
|
28
|
+
return base;
|
|
29
|
+
}
|
|
30
|
+
return "en";
|
|
31
|
+
}
|
|
32
|
+
/** @experimental */
|
|
33
|
+
export function proofText(language, key, vars) {
|
|
34
|
+
const text = PROOF_STRINGS[language]?.[key] ?? PROOF_STRINGS.en[key] ?? key;
|
|
35
|
+
return text.replace(/\{(\w+)\}/g, (all, name) => (vars && vars[name] !== undefined ? String(vars[name]) : all));
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Stylized world outline: polygons of "longitude latitude" pairs. No tiles, no third-party data.
|
|
39
|
+
* @experimental
|
|
40
|
+
*/
|
|
41
|
+
export const WORLD_OUTLINE = "-167 66,-156 71,-130 70,-110 72,-85 72,-68 62,-60 55,-55 48,-66 45,-70 41,-76 35,-81 31,-80 25,-83 29,-90 29,-97 26,-97 21,-92 18,-88 21,-87 16,-83 10,-78 8,-80 7,-85 11,-92 14,-105 20,-110 24,-112 31,-117 32,-124 40,-124 48,-133 55,-146 60,-153 57,-165 60;-73 78,-60 82,-30 83,-20 76,-22 70,-42 60,-50 64,-55 70,-66 76;-85 22,-80 23,-74 20,-78 20;-74 19,-69 19,-68 18,-72 18;-80 8,-72 12,-62 10,-52 5,-44 -2,-35 -6,-39 -15,-41 -22,-48 -26,-53 -34,-58 -38,-62 -40,-65 -45,-68 -52,-73 -53,-75 -46,-73 -37,-71 -28,-70 -18,-76 -14,-81 -5,-80 0,-77 4;-10 36,-9 43,-2 44,-4 48,2 51,5 53,8 54,9 57,5 58,5 62,12 65,18 70,28 71,40 68,45 68,60 70,60 55,50 47,40 45,36 45,28 41,26 40,23 36,21 40,19 42,14 45,12 44,16 41,18 40,16 38,15 40,12 42,9 44,3 43,-1 37,-6 36;-6 50,-5 58,-3 59,0 53,1 51;-10 52,-8 55,-6 54,-6 52;-24 64,-22 66,-14 66,-14 64;-17 21,-16 28,-10 30,-6 36,10 37,11 33,20 31,30 31,33 30,36 22,39 15,43 12,51 12,48 5,40 -3,40 -10,35 -20,33 -26,27 -34,20 -35,18 -30,12 -17,13 -8,9 0,9 4,4 6,-8 4,-13 8,-17 15;44 -25,47 -25,50 -15,48 -13,44 -17;28 41,36 37,35 33,34 28,39 21,43 13,45 13,52 16,55 17,58 21,56 24,52 24,50 26,48 30,51 28,56 27,58 25,62 25,67 24,72 21,77 8,80 10,80 16,87 22,92 22,98 16,98 9,100 6,101 3,104 1,103 5,100 13,103 12,105 9,109 11,108 16,106 20,110 21,117 24,122 31,119 35,122 40,126 37,129 35,129 42,135 43,141 48,140 53,135 55,143 59,155 59,163 62,172 65,180 66,180 71,160 70,140 73,113 74,100 77,80 73,70 73,60 70,60 55,50 47,40 45,36 45;80 6,82 7,81 9,80 9;120 22,121 25,122 25,121 22;130 31,132 34,135 34,140 36,142 40,141 45,145 44,140 42,139 38,136 36,133 36,130 34;95 5,98 4,104 -2,106 -6,101 -3,96 2;109 1,113 3,117 7,119 5,117 1,116 -4,111 -3,109 -1;105 -6,110 -7,114 -8,110 -8,106 -7;120 18,122 18,124 12,126 7,122 7,120 14;131 -1,141 -2,147 -6,150 -10,141 -9,138 -8,132 -4;114 -22,114 -34,117 -35,123 -34,131 -31,138 -35,141 -38,147 -39,150 -37,153 -32,153 -25,146 -19,145 -15,142 -11,141 -17,136 -12,132 -11,129 -15,126 -14,122 -18;145 -41,148 -41,148 -43,146 -44;172 -34,175 -37,178 -38,176 -41,174 -41,173 -38;172 -41,174 -41,171 -44,167 -46,168 -44";
|
|
42
|
+
/**
|
|
43
|
+
* SVG path for WORLD_OUTLINE in a 360 x 142 equirectangular box cropped to 84N..58S (x = lon + 180, y = 84 - lat).
|
|
44
|
+
* @experimental
|
|
45
|
+
*/
|
|
46
|
+
export function worldOutlinePath() {
|
|
47
|
+
return WORLD_OUTLINE.split(";").map((shape) => `M${shape.split(",").map((pair) => { const [lon, lat] = pair.split(" ").map(Number); return `${lon + 180} ${84 - lat}`; }).join("L")}Z`).join("");
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Marker position for a coarse location in the same box.
|
|
51
|
+
* @experimental
|
|
52
|
+
*/
|
|
53
|
+
export function mapPoint(lat, lon) {
|
|
54
|
+
const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
|
|
55
|
+
return { x: Math.round((clamp(lon, -180, 180) + 180) * 10) / 10, y: Math.round((84 - clamp(lat, -58, 84)) * 10) / 10 };
|
|
56
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resumable media upload for YouSaid Proof collection drafts.
|
|
3
|
+
*
|
|
4
|
+
* Protocol (same for the hosted page, the in-app prompt and headless API
|
|
5
|
+
* sessions; the draft session secret comes from startSession or
|
|
6
|
+
* POST /api/v1/proof/submissions/sessions):
|
|
7
|
+
* POST /api/proof/collect/drafts/{draftId}/uploads -> { sessionId, token, partBytes, parts }
|
|
8
|
+
* PUT /api/proof/collect/drafts/{draftId}/uploads/{id}?part=n (exactly partBytes; last part = remainder)
|
|
9
|
+
* GET /api/proof/collect/drafts/{draftId}/uploads/{id} -> { received: number[], state }
|
|
10
|
+
* POST /api/proof/collect/drafts/{draftId}/uploads/{id} -> { media, draft }
|
|
11
|
+
* Bytes stream to R2 as a multipart upload (video up to 500 MB). Each part
|
|
12
|
+
* retries with backoff; after a dropped connection, or a reload with the same
|
|
13
|
+
* file, the upload continues from the parts the server already has.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* `screen_recording` is stored as a video; it needs `screenConsent` (see ProofUploadOptions).
|
|
17
|
+
* @experimental
|
|
18
|
+
*/
|
|
19
|
+
export type ProofUploadMode = "video" | "audio" | "image" | "screenshot" | "screen_recording";
|
|
20
|
+
/** @experimental */
|
|
21
|
+
export type ProofUploadProgress = {
|
|
22
|
+
sent: number;
|
|
23
|
+
total: number;
|
|
24
|
+
state: "starting" | "uploading" | "retrying" | "finalizing" | "done";
|
|
25
|
+
};
|
|
26
|
+
/** @experimental */
|
|
27
|
+
export type ProofMediaRef = {
|
|
28
|
+
uploadSessionId: string;
|
|
29
|
+
kind: "video" | "audio" | "image";
|
|
30
|
+
mode: ProofUploadMode;
|
|
31
|
+
mimeType: string;
|
|
32
|
+
bytes: number;
|
|
33
|
+
sha256: string;
|
|
34
|
+
durationMs?: number;
|
|
35
|
+
};
|
|
36
|
+
/** @experimental */
|
|
37
|
+
export type ProofUploadResult<Draft = unknown> = {
|
|
38
|
+
media: ProofMediaRef;
|
|
39
|
+
draft: Draft;
|
|
40
|
+
previewToken?: string;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Where an in-progress upload is remembered so it can resume after a reload. Defaults to localStorage.
|
|
44
|
+
* @experimental
|
|
45
|
+
*/
|
|
46
|
+
export type ProofUploadResumeStore = {
|
|
47
|
+
get(key: string): string | null;
|
|
48
|
+
set(key: string, value: string): void;
|
|
49
|
+
remove(key: string): void;
|
|
50
|
+
};
|
|
51
|
+
/** @experimental */
|
|
52
|
+
export type ProofUploadOptions = {
|
|
53
|
+
/** Origin of the YouSaid deployment; defaults to the current page's origin (hosted page) or https://yousaid.dev. */
|
|
54
|
+
endpoint?: string;
|
|
55
|
+
fetch?: typeof globalThis.fetch;
|
|
56
|
+
durationMs?: number;
|
|
57
|
+
/** How the file was captured; stored with the original's metadata. */
|
|
58
|
+
source?: "recorder" | "file" | "api";
|
|
59
|
+
onProgress?: (progress: ProofUploadProgress) => void;
|
|
60
|
+
signal?: AbortSignal;
|
|
61
|
+
resume?: ProofUploadResumeStore | null;
|
|
62
|
+
/** Maximum attempts per part before giving up (the upload can still be resumed later). */
|
|
63
|
+
maxAttempts?: number;
|
|
64
|
+
/**
|
|
65
|
+
* Up to three still frames of a recording (data URLs, for example from
|
|
66
|
+
* extractVideoFrames). Used only for the unsafe-media check and never
|
|
67
|
+
* stored; without frames the recording is held for a person to review.
|
|
68
|
+
*/
|
|
69
|
+
frames?: string[] | (() => Promise<string[]>);
|
|
70
|
+
/**
|
|
71
|
+
* Required for mode `screen_recording`: the customer ticked the screen
|
|
72
|
+
* consent box before the screen picker opened. `version` names the consent
|
|
73
|
+
* wording they saw; it is stored with the original as evidence.
|
|
74
|
+
*/
|
|
75
|
+
screenConsent?: {
|
|
76
|
+
version: string;
|
|
77
|
+
microphone?: boolean;
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
/** @experimental */
|
|
81
|
+
export declare class ProofUploadError extends Error {
|
|
82
|
+
readonly status: number;
|
|
83
|
+
readonly code?: string | undefined;
|
|
84
|
+
constructor(message: string, status: number, code?: string | undefined);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Uploads one recording or image into a draft, resumably.
|
|
88
|
+
* @experimental
|
|
89
|
+
*/
|
|
90
|
+
export declare function uploadProofMedia<Draft = unknown>(draftId: string, sessionToken: string, blob: Blob, mode: ProofUploadMode, options?: ProofUploadOptions): Promise<ProofUploadResult<Draft>>;
|
|
91
|
+
/**
|
|
92
|
+
* Cancels an in-progress upload (for example when the customer re-records).
|
|
93
|
+
* @experimental
|
|
94
|
+
*/
|
|
95
|
+
export declare function abortProofUpload(draftId: string, sessionToken: string, uploadSessionId: string, uploadToken: string, options?: Pick<ProofUploadOptions, "endpoint" | "fetch">): Promise<void>;
|
|
96
|
+
//# sourceMappingURL=proof-upload.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proof-upload.d.ts","sourceRoot":"","sources":["../src/proof-upload.ts"],"names":[],"mappings":"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"}
|