@semiont/api-client 0.4.21 → 0.4.22
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/README.md +49 -91
- package/dist/index.d.ts +87 -1965
- package/dist/index.js +479 -4275
- package/dist/index.js.map +1 -1
- package/package.json +3 -7
- package/dist/utils/index.d.ts +0 -584
- package/dist/utils/index.js +0 -646
- package/dist/utils/index.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,18 +1,10 @@
|
|
|
1
1
|
import ky, { HTTPError } from 'ky';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
import { share, filter, map
|
|
2
|
+
import { Subject, BehaviorSubject } from 'rxjs';
|
|
3
|
+
import { RESOURCE_BROADCAST_TYPES, PERSISTED_EVENT_TYPES, BRIDGED_CHANNELS, busLog } from '@semiont/core';
|
|
4
|
+
import { getActiveTraceparent, recordBusEmit, withSpan, SpanKind, extractTraceparent, withTraceparent } from '@semiont/observability';
|
|
5
|
+
import { share, filter, map } from 'rxjs/operators';
|
|
6
6
|
|
|
7
|
-
// src/
|
|
8
|
-
function busLog(direction, channel, payload, scope) {
|
|
9
|
-
if (typeof globalThis === "undefined") return;
|
|
10
|
-
const g = globalThis;
|
|
11
|
-
if (!g.__SEMIONT_BUS_LOG__) return;
|
|
12
|
-
const cid = payload?.correlationId;
|
|
13
|
-
const tag = `[bus ${direction}] ${channel}` + (scope ? ` scope=${scope}` : "") + (cid ? ` cid=${String(cid).slice(0, 8)}` : "");
|
|
14
|
-
console.debug(tag, payload);
|
|
15
|
-
}
|
|
7
|
+
// src/transport/http-transport.ts
|
|
16
8
|
var DEGRADED_THRESHOLD_MS = 3e3;
|
|
17
9
|
var ALLOWED_TRANSITIONS = {
|
|
18
10
|
initial: ["connecting", "closed"],
|
|
@@ -23,12 +15,12 @@ var ALLOWED_TRANSITIONS = {
|
|
|
23
15
|
closed: []
|
|
24
16
|
};
|
|
25
17
|
function createActorVM(options) {
|
|
26
|
-
const { baseUrl
|
|
18
|
+
const { baseUrl, token: tokenOrGetter, channels: initialChannels, scope: initialScope, reconnectMs = 5e3 } = options;
|
|
27
19
|
const getToken = typeof tokenOrGetter === "function" ? tokenOrGetter : () => tokenOrGetter;
|
|
28
20
|
const g = globalThis;
|
|
29
21
|
g.__SEMIONT_ACTOR_INSTANCES__ = (g.__SEMIONT_ACTOR_INSTANCES__ ?? 0) + 1;
|
|
30
22
|
const actorSerial = g.__SEMIONT_ACTOR_INSTANCES__;
|
|
31
|
-
console.debug(`[diag] ActorVM #${actorSerial} constructed (baseUrl=${
|
|
23
|
+
console.debug(`[diag] ActorVM #${actorSerial} constructed (baseUrl=${baseUrl})`);
|
|
32
24
|
const globalChannels = new Set(initialChannels);
|
|
33
25
|
const scopedChannels = /* @__PURE__ */ new Set();
|
|
34
26
|
let activeScope = initialScope;
|
|
@@ -95,7 +87,7 @@ function createActorVM(options) {
|
|
|
95
87
|
params.append("scoped", ch);
|
|
96
88
|
}
|
|
97
89
|
}
|
|
98
|
-
const url = `${
|
|
90
|
+
const url = `${baseUrl}/bus/subscribe?${params.toString()}`;
|
|
99
91
|
const controller = new AbortController();
|
|
100
92
|
inflightControllers.add(controller);
|
|
101
93
|
try {
|
|
@@ -130,7 +122,25 @@ function createActorVM(options) {
|
|
|
130
122
|
if (currentId !== void 0) lastEventId = currentId;
|
|
131
123
|
const parsed = JSON.parse(currentData);
|
|
132
124
|
busLog("RECV", parsed.channel, parsed.payload, parsed.scope);
|
|
133
|
-
|
|
125
|
+
const carrier = extractTraceparent(
|
|
126
|
+
parsed.payload
|
|
127
|
+
);
|
|
128
|
+
await withTraceparent(
|
|
129
|
+
carrier,
|
|
130
|
+
() => withSpan(
|
|
131
|
+
`bus.recv:${parsed.channel}`,
|
|
132
|
+
() => {
|
|
133
|
+
events$.next(parsed);
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
kind: SpanKind.CONSUMER,
|
|
137
|
+
attrs: {
|
|
138
|
+
"bus.channel": parsed.channel,
|
|
139
|
+
...parsed.scope ? { "bus.scope": parsed.scope } : {}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
)
|
|
143
|
+
);
|
|
134
144
|
}
|
|
135
145
|
currentEvent = "";
|
|
136
146
|
currentData = "";
|
|
@@ -175,15 +185,20 @@ function createActorVM(options) {
|
|
|
175
185
|
);
|
|
176
186
|
},
|
|
177
187
|
emit: async (channel, payload, emitScope) => {
|
|
178
|
-
busLog("EMIT", channel, payload, emitScope);
|
|
179
188
|
const body = { channel, payload };
|
|
180
189
|
if (emitScope) body.scope = emitScope;
|
|
181
|
-
|
|
190
|
+
const headers = {
|
|
191
|
+
"Content-Type": "application/json",
|
|
192
|
+
Authorization: `Bearer ${getToken()}`
|
|
193
|
+
};
|
|
194
|
+
const trace = getActiveTraceparent();
|
|
195
|
+
if (trace) {
|
|
196
|
+
headers["traceparent"] = trace.traceparent;
|
|
197
|
+
if (trace.tracestate) headers["tracestate"] = trace.tracestate;
|
|
198
|
+
}
|
|
199
|
+
await fetch(`${baseUrl}/bus/emit`, {
|
|
182
200
|
method: "POST",
|
|
183
|
-
headers
|
|
184
|
-
"Content-Type": "application/json",
|
|
185
|
-
Authorization: `Bearer ${getToken()}`
|
|
186
|
-
},
|
|
201
|
+
headers,
|
|
187
202
|
body: JSON.stringify(body)
|
|
188
203
|
});
|
|
189
204
|
},
|
|
@@ -255,4316 +270,505 @@ function createActorVM(options) {
|
|
|
255
270
|
}
|
|
256
271
|
};
|
|
257
272
|
}
|
|
258
|
-
var
|
|
259
|
-
|
|
273
|
+
var RESOURCE_SCOPED_CHANNELS = [
|
|
274
|
+
...PERSISTED_EVENT_TYPES.filter((t) => t !== "mark:entity-type-added"),
|
|
275
|
+
...RESOURCE_BROADCAST_TYPES
|
|
276
|
+
];
|
|
277
|
+
var APIError = class extends Error {
|
|
278
|
+
constructor(message, status, statusText, details) {
|
|
260
279
|
super(message);
|
|
261
|
-
this.
|
|
280
|
+
this.status = status;
|
|
281
|
+
this.statusText = statusText;
|
|
282
|
+
this.details = details;
|
|
283
|
+
this.name = "APIError";
|
|
262
284
|
}
|
|
263
285
|
};
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
286
|
+
var HttpTransport = class {
|
|
287
|
+
baseUrl;
|
|
288
|
+
http;
|
|
289
|
+
token$;
|
|
290
|
+
logger;
|
|
291
|
+
_actor = null;
|
|
292
|
+
_actorStarted = false;
|
|
293
|
+
disposed = false;
|
|
294
|
+
activeResource = null;
|
|
295
|
+
/** Buses we've been asked to bridge wire events into. */
|
|
296
|
+
bridges = [];
|
|
297
|
+
constructor(config) {
|
|
298
|
+
const { baseUrl, timeout = 3e4, retry = 2, logger, tokenRefresher } = config;
|
|
299
|
+
this.baseUrl = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
300
|
+
this.token$ = config.token$ ?? new BehaviorSubject(null);
|
|
301
|
+
this.logger = logger;
|
|
302
|
+
const retryConfig = tokenRefresher ? {
|
|
303
|
+
limit: 1,
|
|
304
|
+
methods: ["get", "post", "put", "patch", "delete", "head", "options"],
|
|
305
|
+
statusCodes: [401, 408, 413, 429, 500, 502, 503, 504]
|
|
306
|
+
} : retry;
|
|
307
|
+
this.http = ky.create({
|
|
308
|
+
timeout,
|
|
309
|
+
retry: retryConfig,
|
|
310
|
+
credentials: "include",
|
|
311
|
+
hooks: {
|
|
312
|
+
beforeRequest: [
|
|
313
|
+
(request) => {
|
|
314
|
+
if (this.logger) {
|
|
315
|
+
this.logger.debug("HTTP Request", {
|
|
316
|
+
type: "http_request",
|
|
317
|
+
url: request.url,
|
|
318
|
+
method: request.method,
|
|
319
|
+
timestamp: Date.now(),
|
|
320
|
+
hasAuth: request.headers.has("Authorization")
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
],
|
|
325
|
+
beforeRetry: tokenRefresher ? [
|
|
326
|
+
async ({ request, error }) => {
|
|
327
|
+
if (!(error instanceof HTTPError) || error.response.status !== 401) {
|
|
328
|
+
return void 0;
|
|
329
|
+
}
|
|
330
|
+
try {
|
|
331
|
+
const newToken = await tokenRefresher();
|
|
332
|
+
if (!newToken) return ky.stop;
|
|
333
|
+
request.headers.set("Authorization", `Bearer ${newToken}`);
|
|
334
|
+
return void 0;
|
|
335
|
+
} catch {
|
|
336
|
+
return ky.stop;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
] : [],
|
|
340
|
+
afterResponse: [
|
|
341
|
+
(request, _options, response) => {
|
|
342
|
+
if (this.logger) {
|
|
343
|
+
this.logger.debug("HTTP Response", {
|
|
344
|
+
type: "http_response",
|
|
345
|
+
url: request.url,
|
|
346
|
+
method: request.method,
|
|
347
|
+
status: response.status,
|
|
348
|
+
statusText: response.statusText
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
return response;
|
|
352
|
+
}
|
|
353
|
+
],
|
|
354
|
+
beforeError: [
|
|
355
|
+
async (error) => {
|
|
356
|
+
const { response, request } = error;
|
|
357
|
+
if (response) {
|
|
358
|
+
const body = await response.json().catch(() => ({}));
|
|
359
|
+
if (this.logger) {
|
|
360
|
+
this.logger.error("HTTP Request Failed", {
|
|
361
|
+
type: "http_error",
|
|
362
|
+
url: request.url,
|
|
363
|
+
method: request.method,
|
|
364
|
+
status: response.status,
|
|
365
|
+
statusText: response.statusText,
|
|
366
|
+
error: body.message || `HTTP ${response.status}: ${response.statusText}`
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
throw new APIError(
|
|
370
|
+
body.message || `HTTP ${response.status}: ${response.statusText}`,
|
|
371
|
+
response.status,
|
|
372
|
+
response.statusText,
|
|
373
|
+
body
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
return error;
|
|
377
|
+
}
|
|
378
|
+
]
|
|
342
379
|
}
|
|
343
|
-
},
|
|
344
|
-
dispose() {
|
|
345
|
-
store$.complete();
|
|
346
|
-
obsCache.clear();
|
|
347
|
-
inflight.clear();
|
|
348
|
-
}
|
|
349
|
-
};
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// src/namespaces/browse.ts
|
|
353
|
-
var ENTITY_TYPES_KEY = "_";
|
|
354
|
-
var BrowseNamespace = class {
|
|
355
|
-
constructor(http, eventBus, getToken, actor) {
|
|
356
|
-
this.http = http;
|
|
357
|
-
this.eventBus = eventBus;
|
|
358
|
-
this.getToken = getToken;
|
|
359
|
-
this.actor = actor;
|
|
360
|
-
const g = globalThis;
|
|
361
|
-
g.__SEMIONT_BROWSE_INSTANCES__ = (g.__SEMIONT_BROWSE_INSTANCES__ ?? 0) + 1;
|
|
362
|
-
const browseSerial = g.__SEMIONT_BROWSE_INSTANCES__;
|
|
363
|
-
this.__serial__ = browseSerial;
|
|
364
|
-
console.debug(`[diag] BrowseNamespace #${browseSerial} constructed`);
|
|
365
|
-
this.resourceCache = createCache(async (id) => {
|
|
366
|
-
const result = await busRequest(
|
|
367
|
-
this.actor,
|
|
368
|
-
"browse:resource-requested",
|
|
369
|
-
{ resourceId: id },
|
|
370
|
-
"browse:resource-result",
|
|
371
|
-
"browse:resource-failed"
|
|
372
|
-
);
|
|
373
|
-
return result.resource;
|
|
374
|
-
});
|
|
375
|
-
this.resourceListCache = createCache(async (key) => {
|
|
376
|
-
const filters = this.resourceListFilters.get(key) ?? {};
|
|
377
|
-
const search = filters.search ? searchQuery(filters.search) : void 0;
|
|
378
|
-
const result = await busRequest(
|
|
379
|
-
this.actor,
|
|
380
|
-
"browse:resources-requested",
|
|
381
|
-
{ search, archived: filters.archived, limit: filters.limit ?? 100, offset: 0 },
|
|
382
|
-
"browse:resources-result",
|
|
383
|
-
"browse:resources-failed"
|
|
384
|
-
);
|
|
385
|
-
return result.resources;
|
|
386
|
-
});
|
|
387
|
-
this.annotationListCache = createCache(async (resourceId) => {
|
|
388
|
-
return busRequest(
|
|
389
|
-
this.actor,
|
|
390
|
-
"browse:annotations-requested",
|
|
391
|
-
{ resourceId },
|
|
392
|
-
"browse:annotations-result",
|
|
393
|
-
"browse:annotations-failed"
|
|
394
|
-
);
|
|
395
380
|
});
|
|
396
|
-
this.
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
381
|
+
this.token$.subscribe((token) => {
|
|
382
|
+
if (token && !this._actorStarted && !this.disposed) {
|
|
383
|
+
this._actorStarted = true;
|
|
384
|
+
this.actor.start();
|
|
400
385
|
}
|
|
401
|
-
const result = await busRequest(
|
|
402
|
-
this.actor,
|
|
403
|
-
"browse:annotation-requested",
|
|
404
|
-
{ resourceId, annotationId },
|
|
405
|
-
"browse:annotation-result",
|
|
406
|
-
"browse:annotation-failed"
|
|
407
|
-
);
|
|
408
|
-
return result.annotation;
|
|
409
|
-
});
|
|
410
|
-
this.entityTypesCache = createCache(async () => {
|
|
411
|
-
const serial = this.__serial__;
|
|
412
|
-
console.debug(`[diag] BrowseNamespace#${serial} entityTypes fetchFn START`);
|
|
413
|
-
const result = await busRequest(
|
|
414
|
-
this.actor,
|
|
415
|
-
"browse:entity-types-requested",
|
|
416
|
-
{},
|
|
417
|
-
"browse:entity-types-result",
|
|
418
|
-
"browse:entity-types-failed"
|
|
419
|
-
);
|
|
420
|
-
console.debug(`[diag] BrowseNamespace#${serial} entityTypes fetchFn RESOLVE`, JSON.stringify(result.entityTypes).slice(0, 200));
|
|
421
|
-
return result.entityTypes;
|
|
422
|
-
});
|
|
423
|
-
this.referencedByCache = createCache(async (resourceId) => {
|
|
424
|
-
const result = await busRequest(
|
|
425
|
-
this.actor,
|
|
426
|
-
"browse:referenced-by-requested",
|
|
427
|
-
{ resourceId },
|
|
428
|
-
"browse:referenced-by-result",
|
|
429
|
-
"browse:referenced-by-failed"
|
|
430
|
-
);
|
|
431
|
-
return result.referencedBy;
|
|
432
|
-
});
|
|
433
|
-
this.resourceEventsCache = createCache(async (resourceId) => {
|
|
434
|
-
const result = await busRequest(
|
|
435
|
-
this.actor,
|
|
436
|
-
"browse:events-requested",
|
|
437
|
-
{ resourceId },
|
|
438
|
-
"browse:events-result",
|
|
439
|
-
"browse:events-failed"
|
|
440
|
-
);
|
|
441
|
-
return result.events;
|
|
442
386
|
});
|
|
443
|
-
this.subscribeToEvents();
|
|
444
387
|
}
|
|
445
|
-
// ──
|
|
446
|
-
//
|
|
447
|
-
// Each cache encapsulates the BehaviorSubject store, in-flight guard,
|
|
448
|
-
// and per-key observable memoization that was previously open-coded
|
|
449
|
-
// here. Behavioral contract: `packages/api-client/docs/CACHE-SEMANTICS.md`.
|
|
388
|
+
// ── Lazy actor construction + per-channel fan-in to bridges ───────────
|
|
450
389
|
//
|
|
451
|
-
//
|
|
452
|
-
//
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
resourceListFilters = /* @__PURE__ */ new Map();
|
|
470
|
-
/**
|
|
471
|
-
* Per-key memo for `annotations()` observables. The cache stores the
|
|
472
|
-
* full `AnnotationsListResponse`; the public shape is just the inner
|
|
473
|
-
* `Annotation[]`. Without this memo, every call to `annotations(rId)`
|
|
474
|
-
* would produce a fresh `.pipe(map(...))` observable, violating B4
|
|
475
|
-
* (per-key observable stability). Consumers that compare observable
|
|
476
|
-
* identity — React hooks depending on the observable reference,
|
|
477
|
-
* `distinctUntilChanged` at a higher level — would misbehave.
|
|
478
|
-
*/
|
|
479
|
-
annotationListObs = /* @__PURE__ */ new Map();
|
|
480
|
-
getToken;
|
|
481
|
-
actor;
|
|
482
|
-
// ── Live queries ────────────────────────────────────────────────────────
|
|
483
|
-
resource(resourceId) {
|
|
484
|
-
return this.resourceCache.observe(resourceId);
|
|
485
|
-
}
|
|
486
|
-
resources(filters) {
|
|
487
|
-
const key = JSON.stringify(filters ?? {});
|
|
488
|
-
this.resourceListFilters.set(key, filters ?? {});
|
|
489
|
-
return this.resourceListCache.observe(key);
|
|
490
|
-
}
|
|
491
|
-
annotations(resourceId) {
|
|
492
|
-
let obs = this.annotationListObs.get(resourceId);
|
|
493
|
-
if (!obs) {
|
|
494
|
-
obs = this.annotationListCache.observe(resourceId).pipe(map$1((r) => r?.annotations));
|
|
495
|
-
this.annotationListObs.set(resourceId, obs);
|
|
390
|
+
// `actor` is exposed so the legacy `SemiontClient` can keep `.actor`
|
|
391
|
+
// pointing at the same ActorVM during the transport-abstraction
|
|
392
|
+
// migration. Once SemiontClient is removed, this should be made
|
|
393
|
+
// private again — external callers should use emit/on/stream/state$.
|
|
394
|
+
get actor() {
|
|
395
|
+
if (!this._actor) {
|
|
396
|
+
this._actor = createActorVM({
|
|
397
|
+
baseUrl: this.baseUrl,
|
|
398
|
+
token: () => this.token$.getValue() ?? "",
|
|
399
|
+
channels: [...BRIDGED_CHANNELS]
|
|
400
|
+
});
|
|
401
|
+
for (const channel of BRIDGED_CHANNELS) {
|
|
402
|
+
this._actor.on$(channel).subscribe((payload) => {
|
|
403
|
+
for (const bus of this.bridges) {
|
|
404
|
+
bus.get(channel).next(payload);
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
}
|
|
496
408
|
}
|
|
497
|
-
return
|
|
409
|
+
return this._actor;
|
|
498
410
|
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
411
|
+
// ── ITransport — bus primitives ───────────────────────────────────────
|
|
412
|
+
async emit(channel, payload, resourceScope) {
|
|
413
|
+
busLog("EMIT", channel, payload, resourceScope);
|
|
414
|
+
recordBusEmit(channel, resourceScope);
|
|
415
|
+
await withSpan(
|
|
416
|
+
`bus.emit:${channel}`,
|
|
417
|
+
async () => {
|
|
418
|
+
if (resourceScope !== void 0) {
|
|
419
|
+
await this.actor.emit(
|
|
420
|
+
channel,
|
|
421
|
+
payload,
|
|
422
|
+
resourceScope
|
|
423
|
+
);
|
|
424
|
+
} else {
|
|
425
|
+
await this.actor.emit(
|
|
426
|
+
channel,
|
|
427
|
+
payload
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
kind: SpanKind.PRODUCER,
|
|
433
|
+
attrs: {
|
|
434
|
+
"bus.channel": channel,
|
|
435
|
+
...resourceScope ? { "bus.scope": resourceScope } : {}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
);
|
|
502
439
|
}
|
|
503
|
-
|
|
504
|
-
const
|
|
505
|
-
|
|
506
|
-
const self = this;
|
|
507
|
-
if (!self.__entityTypesDiag__) {
|
|
508
|
-
self.__entityTypesDiag__ = this.entityTypesCache.observe(ENTITY_TYPES_KEY).pipe(map$1((v) => {
|
|
509
|
-
console.debug(`[diag] BrowseNamespace#${serial} entityTypes$ EMIT`, v === void 0 ? "undefined" : JSON.stringify(v).slice(0, 200));
|
|
510
|
-
return v;
|
|
511
|
-
}));
|
|
512
|
-
}
|
|
513
|
-
return self.__entityTypesDiag__;
|
|
440
|
+
on(channel, handler) {
|
|
441
|
+
const sub = this.actor.on$(channel).subscribe(handler);
|
|
442
|
+
return () => sub.unsubscribe();
|
|
514
443
|
}
|
|
515
|
-
|
|
516
|
-
return this.
|
|
444
|
+
stream(channel) {
|
|
445
|
+
return this.actor.on$(channel);
|
|
517
446
|
}
|
|
518
|
-
|
|
519
|
-
|
|
447
|
+
/**
|
|
448
|
+
* Wire this transport's SSE fan-in into the given bus. Every channel
|
|
449
|
+
* in `BRIDGED_CHANNELS` (and subsequently per-resource scoped channels
|
|
450
|
+
* opened by `subscribeToResource`) is published on the bus. Safe to
|
|
451
|
+
* call multiple times — each bus is added to the fan-out list.
|
|
452
|
+
*/
|
|
453
|
+
bridgeInto(bus) {
|
|
454
|
+
this.bridges.push(bus);
|
|
520
455
|
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
456
|
+
subscribeToResource(resourceId) {
|
|
457
|
+
if (this.activeResource) {
|
|
458
|
+
if (this.activeResource.resourceId !== resourceId) {
|
|
459
|
+
throw new Error(
|
|
460
|
+
`HttpTransport already subscribed to resource ${this.activeResource.resourceId}; call the unsubscribe returned from the previous subscribeToResource before subscribing to ${resourceId}.`
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
this.activeResource.refCount++;
|
|
464
|
+
return this.makeUnsubscriber();
|
|
465
|
+
}
|
|
466
|
+
this.actor.addChannels([...RESOURCE_SCOPED_CHANNELS], resourceId);
|
|
467
|
+
const bridgeSubs = [];
|
|
468
|
+
for (const channel of RESOURCE_SCOPED_CHANNELS) {
|
|
469
|
+
bridgeSubs.push(
|
|
470
|
+
this.actor.on$(channel).subscribe((payload) => {
|
|
471
|
+
for (const bus of this.bridges) {
|
|
472
|
+
bus.get(channel).next(payload);
|
|
473
|
+
}
|
|
474
|
+
})
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
this.activeResource = { resourceId, refCount: 1, bridgeSubs };
|
|
478
|
+
return this.makeUnsubscriber();
|
|
529
479
|
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
480
|
+
makeUnsubscriber() {
|
|
481
|
+
let called = false;
|
|
482
|
+
return () => {
|
|
483
|
+
if (called) return;
|
|
484
|
+
called = true;
|
|
485
|
+
if (!this.activeResource) return;
|
|
486
|
+
this.activeResource.refCount--;
|
|
487
|
+
if (this.activeResource.refCount > 0) return;
|
|
488
|
+
for (const sub of this.activeResource.bridgeSubs) sub.unsubscribe();
|
|
489
|
+
this.actor.removeChannels([...RESOURCE_SCOPED_CHANNELS]);
|
|
490
|
+
this.activeResource = null;
|
|
491
|
+
};
|
|
535
492
|
}
|
|
536
|
-
|
|
537
|
-
return this.
|
|
538
|
-
accept: options?.accept,
|
|
539
|
-
auth: this.getToken()
|
|
540
|
-
});
|
|
493
|
+
get state$() {
|
|
494
|
+
return this.actor.state$;
|
|
541
495
|
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
)
|
|
550
|
-
|
|
496
|
+
dispose() {
|
|
497
|
+
if (this.disposed) return;
|
|
498
|
+
this.disposed = true;
|
|
499
|
+
if (this.activeResource) {
|
|
500
|
+
for (const sub of this.activeResource.bridgeSubs) sub.unsubscribe();
|
|
501
|
+
this.activeResource = null;
|
|
502
|
+
}
|
|
503
|
+
if (this._actor) {
|
|
504
|
+
this._actor.dispose();
|
|
505
|
+
this._actor = null;
|
|
506
|
+
}
|
|
551
507
|
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
{ resourceId, annotationId },
|
|
557
|
-
"browse:annotation-history-result",
|
|
558
|
-
"browse:annotation-history-failed"
|
|
559
|
-
);
|
|
508
|
+
// ── Auth ──────────────────────────────────────────────────────────────
|
|
509
|
+
authHeaders() {
|
|
510
|
+
const token = this.token$.getValue() ?? void 0;
|
|
511
|
+
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
560
512
|
}
|
|
561
|
-
async
|
|
562
|
-
|
|
513
|
+
async authenticatePassword(email, password) {
|
|
514
|
+
return this.http.post(`${this.baseUrl}/api/tokens/password`, {
|
|
515
|
+
json: { email, password },
|
|
516
|
+
headers: this.authHeaders()
|
|
517
|
+
}).json();
|
|
563
518
|
}
|
|
564
|
-
async
|
|
565
|
-
|
|
519
|
+
async authenticateGoogle(credential) {
|
|
520
|
+
return this.http.post(`${this.baseUrl}/api/tokens/google`, {
|
|
521
|
+
json: { credential },
|
|
522
|
+
headers: this.authHeaders()
|
|
523
|
+
}).json();
|
|
566
524
|
}
|
|
567
|
-
async
|
|
568
|
-
|
|
525
|
+
async refreshAccessToken(token) {
|
|
526
|
+
return this.http.post(`${this.baseUrl}/api/tokens/refresh`, {
|
|
527
|
+
json: { refreshToken: token },
|
|
528
|
+
headers: this.authHeaders()
|
|
529
|
+
}).json();
|
|
569
530
|
}
|
|
570
|
-
async
|
|
571
|
-
|
|
572
|
-
this.
|
|
573
|
-
|
|
574
|
-
{ path: dirPath ?? ".", sort: sort ?? "name" },
|
|
575
|
-
"browse:directory-result",
|
|
576
|
-
"browse:directory-failed"
|
|
577
|
-
);
|
|
531
|
+
async logout() {
|
|
532
|
+
await this.http.post(`${this.baseUrl}/api/users/logout`, {
|
|
533
|
+
headers: this.authHeaders()
|
|
534
|
+
}).json();
|
|
578
535
|
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
// - `removeAnnotationDetail` — drops the entry (B13a: entity gone).
|
|
584
|
-
// - `updateAnnotationInPlace` — write-through (B13b: new value known).
|
|
585
|
-
invalidateAnnotationList(resourceId) {
|
|
586
|
-
this.annotationListCache.invalidate(resourceId);
|
|
536
|
+
async acceptTerms() {
|
|
537
|
+
await this.http.post(`${this.baseUrl}/api/users/accept-terms`, {
|
|
538
|
+
headers: this.authHeaders()
|
|
539
|
+
}).json();
|
|
587
540
|
}
|
|
588
|
-
|
|
589
|
-
this.
|
|
590
|
-
|
|
541
|
+
async getCurrentUser() {
|
|
542
|
+
return this.http.get(`${this.baseUrl}/api/users/me`, {
|
|
543
|
+
headers: this.authHeaders()
|
|
544
|
+
}).json();
|
|
591
545
|
}
|
|
592
|
-
|
|
593
|
-
this.
|
|
546
|
+
async generateMcpToken() {
|
|
547
|
+
return this.http.post(`${this.baseUrl}/api/tokens/mcp-generate`, {
|
|
548
|
+
headers: this.authHeaders()
|
|
549
|
+
}).json();
|
|
594
550
|
}
|
|
595
|
-
|
|
596
|
-
this.
|
|
551
|
+
async getMediaToken(resourceId) {
|
|
552
|
+
return this.http.post(`${this.baseUrl}/api/tokens/media`, {
|
|
553
|
+
json: { resourceId },
|
|
554
|
+
headers: this.authHeaders()
|
|
555
|
+
}).json();
|
|
597
556
|
}
|
|
598
|
-
|
|
599
|
-
|
|
557
|
+
// ── Admin ─────────────────────────────────────────────────────────────
|
|
558
|
+
async listUsers() {
|
|
559
|
+
return this.http.get(`${this.baseUrl}/api/admin/users`, {
|
|
560
|
+
headers: this.authHeaders()
|
|
561
|
+
}).json();
|
|
600
562
|
}
|
|
601
|
-
|
|
602
|
-
this.
|
|
563
|
+
async getUserStats() {
|
|
564
|
+
return this.http.get(`${this.baseUrl}/api/admin/users/stats`, {
|
|
565
|
+
headers: this.authHeaders()
|
|
566
|
+
}).json();
|
|
603
567
|
}
|
|
604
|
-
|
|
605
|
-
this.
|
|
568
|
+
async updateUser(id, data) {
|
|
569
|
+
return this.http.patch(`${this.baseUrl}/api/admin/users/${id}`, {
|
|
570
|
+
json: data,
|
|
571
|
+
headers: this.authHeaders()
|
|
572
|
+
}).json();
|
|
606
573
|
}
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
const nextAnnotations = idx >= 0 ? currentList.annotations.map((a, i) => i === idx ? annotation : a) : [...currentList.annotations, annotation];
|
|
612
|
-
this.annotationListCache.set(resourceId, { ...currentList, annotations: nextAnnotations });
|
|
613
|
-
}
|
|
614
|
-
const aId = annotationId(annotation.id);
|
|
615
|
-
this.annotationResources.set(aId, resourceId);
|
|
616
|
-
this.annotationDetailCache.set(aId, annotation);
|
|
574
|
+
async getOAuthConfig() {
|
|
575
|
+
return this.http.get(`${this.baseUrl}/api/admin/oauth/config`, {
|
|
576
|
+
headers: this.authHeaders()
|
|
577
|
+
}).json();
|
|
617
578
|
}
|
|
618
|
-
// ──
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
* `EventMap[K]` without any casts.
|
|
623
|
-
*/
|
|
624
|
-
on(channel, handler) {
|
|
625
|
-
this.eventBus.get(channel).subscribe(handler);
|
|
626
|
-
}
|
|
627
|
-
/**
|
|
628
|
-
* Handler shared by `mark:entity-tag-added` and `mark:entity-tag-removed`.
|
|
629
|
-
* Both events carry the same effect: the annotation list, the
|
|
630
|
-
* resource descriptor, and the event log for that resource all may
|
|
631
|
-
* now reflect different entity tagging, so invalidate all three.
|
|
632
|
-
*/
|
|
633
|
-
onEntityTagChanged = (stored) => {
|
|
634
|
-
if (!stored.resourceId) return;
|
|
635
|
-
this.invalidateAnnotationList(stored.resourceId);
|
|
636
|
-
this.invalidateResourceDetail(stored.resourceId);
|
|
637
|
-
this.invalidateResourceEvents(stored.resourceId);
|
|
638
|
-
};
|
|
639
|
-
/**
|
|
640
|
-
* Handler shared by `mark:archived` and `mark:unarchived`. Both
|
|
641
|
-
* change a resource's archived flag, which is stored on the resource
|
|
642
|
-
* descriptor and affects the resource-list filter.
|
|
643
|
-
*/
|
|
644
|
-
onArchiveToggled = (stored) => {
|
|
645
|
-
if (!stored.resourceId) return;
|
|
646
|
-
this.invalidateResourceDetail(stored.resourceId);
|
|
647
|
-
this.invalidateResourceLists();
|
|
648
|
-
};
|
|
649
|
-
/**
|
|
650
|
-
* Handler shared by `yield:create-ok` and `yield:update-ok`. Both
|
|
651
|
-
* report a resource mutation with the resourceId as a string (not
|
|
652
|
-
* yet branded), so we brand and apply the same effect as
|
|
653
|
-
* `onArchiveToggled`.
|
|
654
|
-
*/
|
|
655
|
-
onYieldResourceMutated = (event) => {
|
|
656
|
-
const rId = resourceId(event.resourceId);
|
|
657
|
-
this.invalidateResourceDetail(rId);
|
|
658
|
-
this.invalidateResourceLists();
|
|
659
|
-
};
|
|
660
|
-
subscribeToEvents() {
|
|
661
|
-
this.on("bus:resume-gap", (event) => {
|
|
662
|
-
const gapScope = event.scope;
|
|
663
|
-
if (gapScope) {
|
|
664
|
-
const rId = gapScope;
|
|
665
|
-
this.invalidateAnnotationList(rId);
|
|
666
|
-
this.invalidateResourceDetail(rId);
|
|
667
|
-
this.invalidateResourceEvents(rId);
|
|
668
|
-
this.invalidateReferencedBy(rId);
|
|
669
|
-
} else {
|
|
670
|
-
this.invalidateResourceLists();
|
|
671
|
-
for (const rId of this.annotationListCache.keys()) this.invalidateAnnotationList(rId);
|
|
672
|
-
for (const rId of this.resourceCache.keys()) this.invalidateResourceDetail(rId);
|
|
673
|
-
for (const rId of this.resourceEventsCache.keys()) this.invalidateResourceEvents(rId);
|
|
674
|
-
for (const rId of this.referencedByCache.keys()) this.invalidateReferencedBy(rId);
|
|
675
|
-
}
|
|
676
|
-
this.invalidateEntityTypes();
|
|
677
|
-
});
|
|
678
|
-
this.on("mark:delete-ok", (event) => {
|
|
679
|
-
this.removeAnnotationDetail(annotationId(event.annotationId));
|
|
680
|
-
});
|
|
681
|
-
this.on("mark:added", (stored) => {
|
|
682
|
-
if (stored.resourceId) {
|
|
683
|
-
this.invalidateAnnotationList(stored.resourceId);
|
|
684
|
-
this.invalidateResourceEvents(stored.resourceId);
|
|
685
|
-
}
|
|
686
|
-
});
|
|
687
|
-
this.on("mark:removed", (stored) => {
|
|
688
|
-
if (stored.resourceId) {
|
|
689
|
-
this.invalidateAnnotationList(stored.resourceId);
|
|
690
|
-
this.invalidateResourceEvents(stored.resourceId);
|
|
691
|
-
}
|
|
692
|
-
this.removeAnnotationDetail(annotationId(stored.payload.annotationId));
|
|
693
|
-
});
|
|
694
|
-
this.on("mark:body-updated", (event) => {
|
|
695
|
-
const enriched = event;
|
|
696
|
-
if (!enriched.resourceId || !enriched.annotation) return;
|
|
697
|
-
this.updateAnnotationInPlace(enriched.resourceId, enriched.annotation);
|
|
698
|
-
this.invalidateResourceEvents(enriched.resourceId);
|
|
699
|
-
});
|
|
700
|
-
this.on("mark:entity-tag-added", this.onEntityTagChanged);
|
|
701
|
-
this.on("mark:entity-tag-removed", this.onEntityTagChanged);
|
|
702
|
-
this.on("replay-window-exceeded", (event) => {
|
|
703
|
-
if (event.resourceId) {
|
|
704
|
-
this.invalidateAnnotationList(event.resourceId);
|
|
705
|
-
}
|
|
579
|
+
// ── Exchange (backup/restore/export/import) ───────────────────────────
|
|
580
|
+
async backupKnowledgeBase() {
|
|
581
|
+
return this.http.post(`${this.baseUrl}/api/admin/exchange/backup`, {
|
|
582
|
+
headers: this.authHeaders()
|
|
706
583
|
});
|
|
707
|
-
this.on("yield:create-ok", this.onYieldResourceMutated);
|
|
708
|
-
this.on("yield:update-ok", this.onYieldResourceMutated);
|
|
709
|
-
this.on("mark:archived", this.onArchiveToggled);
|
|
710
|
-
this.on("mark:unarchived", this.onArchiveToggled);
|
|
711
|
-
this.on("mark:entity-type-added", () => this.invalidateEntityTypes());
|
|
712
|
-
}
|
|
713
|
-
};
|
|
714
|
-
var MarkNamespace = class {
|
|
715
|
-
constructor(http, eventBus, getToken, actor) {
|
|
716
|
-
this.http = http;
|
|
717
|
-
this.eventBus = eventBus;
|
|
718
|
-
this.getToken = getToken;
|
|
719
|
-
this.actor = actor;
|
|
720
|
-
}
|
|
721
|
-
async annotation(resourceId, input) {
|
|
722
|
-
return busRequest(
|
|
723
|
-
this.actor,
|
|
724
|
-
"mark:create-request",
|
|
725
|
-
{ resourceId, request: input },
|
|
726
|
-
"mark:create-ok",
|
|
727
|
-
"mark:create-failed"
|
|
728
|
-
);
|
|
729
|
-
}
|
|
730
|
-
async delete(resourceId, annotationId) {
|
|
731
|
-
await this.actor.emit("mark:delete", { annotationId, resourceId });
|
|
732
|
-
}
|
|
733
|
-
async entityType(type) {
|
|
734
|
-
await this.actor.emit("mark:add-entity-type", { tag: type });
|
|
735
|
-
}
|
|
736
|
-
async entityTypes(types) {
|
|
737
|
-
for (const tag of types) {
|
|
738
|
-
await this.actor.emit("mark:add-entity-type", { tag });
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
async updateResource(resourceId, data) {
|
|
742
|
-
return this.http.updateResource(resourceId, data, { auth: this.getToken() });
|
|
743
|
-
}
|
|
744
|
-
async archive(resourceId) {
|
|
745
|
-
await this.actor.emit("mark:archive", { resourceId });
|
|
746
|
-
}
|
|
747
|
-
async unarchive(resourceId) {
|
|
748
|
-
await this.actor.emit("mark:unarchive", { resourceId });
|
|
749
584
|
}
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
done = true;
|
|
757
|
-
if (pollTimer) {
|
|
758
|
-
clearTimeout(pollTimer);
|
|
759
|
-
pollTimer = null;
|
|
760
|
-
}
|
|
761
|
-
if (pollInterval) {
|
|
762
|
-
clearInterval(pollInterval);
|
|
763
|
-
pollInterval = null;
|
|
764
|
-
}
|
|
765
|
-
};
|
|
766
|
-
const resetPollTimer = (jobId) => {
|
|
767
|
-
if (pollTimer) clearTimeout(pollTimer);
|
|
768
|
-
if (pollInterval) {
|
|
769
|
-
clearInterval(pollInterval);
|
|
770
|
-
pollInterval = null;
|
|
771
|
-
}
|
|
772
|
-
pollTimer = setTimeout(() => {
|
|
773
|
-
if (done) return;
|
|
774
|
-
pollInterval = setInterval(() => {
|
|
775
|
-
if (done) return;
|
|
776
|
-
busRequest(
|
|
777
|
-
this.actor,
|
|
778
|
-
"job:status-requested",
|
|
779
|
-
{ jobId },
|
|
780
|
-
"job:status-result",
|
|
781
|
-
"job:status-failed"
|
|
782
|
-
).then((status) => {
|
|
783
|
-
if (done) return;
|
|
784
|
-
if (status.status === "complete") {
|
|
785
|
-
cleanup();
|
|
786
|
-
subscriber.next({ motivation, resourceId, progress: status.result });
|
|
787
|
-
subscriber.complete();
|
|
788
|
-
} else if (status.status === "failed") {
|
|
789
|
-
cleanup();
|
|
790
|
-
subscriber.error(new Error(status.error ?? "Job failed"));
|
|
791
|
-
}
|
|
792
|
-
}).catch(() => {
|
|
793
|
-
});
|
|
794
|
-
}, 5e3);
|
|
795
|
-
}, 1e4);
|
|
796
|
-
};
|
|
797
|
-
let activeJobId = null;
|
|
798
|
-
const progress$ = this.eventBus.get("job:report-progress").pipe(
|
|
799
|
-
filter((e) => e.jobId === activeJobId)
|
|
800
|
-
);
|
|
801
|
-
const complete$ = this.eventBus.get("job:complete").pipe(
|
|
802
|
-
filter((e) => e.jobId === activeJobId)
|
|
803
|
-
);
|
|
804
|
-
const fail$ = this.eventBus.get("job:fail").pipe(
|
|
805
|
-
filter((e) => e.jobId === activeJobId)
|
|
806
|
-
);
|
|
807
|
-
const progressSub = progress$.pipe(takeUntil(merge(complete$, fail$))).subscribe((e) => {
|
|
808
|
-
subscriber.next(e.progress);
|
|
809
|
-
if (activeJobId) resetPollTimer(activeJobId);
|
|
810
|
-
});
|
|
811
|
-
const completeSub = complete$.subscribe(() => {
|
|
812
|
-
cleanup();
|
|
813
|
-
subscriber.complete();
|
|
814
|
-
});
|
|
815
|
-
const failSub = fail$.subscribe((e) => {
|
|
816
|
-
cleanup();
|
|
817
|
-
subscriber.error(new Error(e.error));
|
|
818
|
-
});
|
|
819
|
-
const auth = this.getToken();
|
|
820
|
-
this.dispatchAssist(resourceId, motivation, options, auth).then(({ jobId }) => {
|
|
821
|
-
if (jobId && !done) {
|
|
822
|
-
activeJobId = jobId;
|
|
823
|
-
resetPollTimer(jobId);
|
|
824
|
-
}
|
|
825
|
-
}).catch((error) => {
|
|
826
|
-
cleanup();
|
|
827
|
-
subscriber.error(error);
|
|
828
|
-
});
|
|
829
|
-
return () => {
|
|
830
|
-
cleanup();
|
|
831
|
-
progressSub.unsubscribe();
|
|
832
|
-
completeSub.unsubscribe();
|
|
833
|
-
failSub.unsubscribe();
|
|
834
|
-
};
|
|
585
|
+
async restoreKnowledgeBase(file, onProgress) {
|
|
586
|
+
const formData = new FormData();
|
|
587
|
+
formData.append("file", file);
|
|
588
|
+
const response = await this.http.post(`${this.baseUrl}/api/admin/exchange/restore`, {
|
|
589
|
+
body: formData,
|
|
590
|
+
headers: this.authHeaders()
|
|
835
591
|
});
|
|
592
|
+
return this.parseSSEStream(response, onProgress);
|
|
836
593
|
}
|
|
837
|
-
async
|
|
838
|
-
const
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
assessing: "assessment-annotation",
|
|
843
|
-
commenting: "comment-annotation"
|
|
844
|
-
};
|
|
845
|
-
const jobType = jobTypeMap[motivation];
|
|
846
|
-
if (!jobType) throw new Error(`Unsupported motivation: ${motivation}`);
|
|
847
|
-
if (motivation === "tagging") {
|
|
848
|
-
if (!options.schemaId || !options.categories?.length) throw new Error("Tag assist requires schemaId and categories");
|
|
849
|
-
} else if (motivation === "linking") {
|
|
850
|
-
if (!options.entityTypes?.length) throw new Error("Reference assist requires entityTypes");
|
|
851
|
-
}
|
|
852
|
-
const params = {};
|
|
853
|
-
if (options.entityTypes) params.entityTypes = options.entityTypes;
|
|
854
|
-
if (options.includeDescriptiveReferences !== void 0) params.includeDescriptiveReferences = options.includeDescriptiveReferences;
|
|
855
|
-
if (options.instructions !== void 0) params.instructions = options.instructions;
|
|
856
|
-
if (options.density !== void 0) params.density = options.density;
|
|
857
|
-
if (options.tone !== void 0) params.tone = options.tone;
|
|
858
|
-
if (options.language !== void 0) params.language = options.language;
|
|
859
|
-
if (options.schemaId !== void 0) params.schemaId = options.schemaId;
|
|
860
|
-
if (options.categories !== void 0) params.categories = options.categories;
|
|
861
|
-
return busRequest(
|
|
862
|
-
this.actor,
|
|
863
|
-
"job:create",
|
|
864
|
-
{ jobType, resourceId, params },
|
|
865
|
-
"job:created",
|
|
866
|
-
"job:create-failed"
|
|
867
|
-
);
|
|
868
|
-
}
|
|
869
|
-
};
|
|
870
|
-
|
|
871
|
-
// src/namespaces/bind.ts
|
|
872
|
-
var BindNamespace = class {
|
|
873
|
-
constructor(actor) {
|
|
874
|
-
this.actor = actor;
|
|
875
|
-
}
|
|
876
|
-
async body(resourceId, annotationId, operations) {
|
|
877
|
-
await this.actor.emit("bind:update-body", {
|
|
878
|
-
correlationId: crypto.randomUUID(),
|
|
879
|
-
annotationId,
|
|
880
|
-
resourceId,
|
|
881
|
-
operations
|
|
594
|
+
async exportKnowledgeBase(params) {
|
|
595
|
+
const searchParams = params?.includeArchived ? new URLSearchParams({ includeArchived: "true" }) : void 0;
|
|
596
|
+
return this.http.post(`${this.baseUrl}/api/moderate/exchange/export`, {
|
|
597
|
+
headers: this.authHeaders(),
|
|
598
|
+
...searchParams ? { searchParams } : {}
|
|
882
599
|
});
|
|
883
600
|
}
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
this.
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
annotation(annotationId, resourceId, options) {
|
|
891
|
-
return new Observable((subscriber) => {
|
|
892
|
-
const correlationId = crypto.randomUUID();
|
|
893
|
-
const complete$ = this.eventBus.get("gather:complete").pipe(
|
|
894
|
-
filter((e) => e.correlationId === correlationId)
|
|
895
|
-
);
|
|
896
|
-
const failed$ = this.eventBus.get("gather:failed").pipe(
|
|
897
|
-
filter((e) => e.correlationId === correlationId)
|
|
898
|
-
);
|
|
899
|
-
const sub = merge(
|
|
900
|
-
this.eventBus.get("gather:annotation-progress").pipe(
|
|
901
|
-
filter((e) => e.annotationId === annotationId),
|
|
902
|
-
map((e) => e)
|
|
903
|
-
),
|
|
904
|
-
complete$.pipe(map((e) => e))
|
|
905
|
-
).pipe(takeUntil(merge(complete$, failed$))).subscribe({
|
|
906
|
-
next: (v) => subscriber.next(v),
|
|
907
|
-
error: (e) => subscriber.error(e)
|
|
908
|
-
});
|
|
909
|
-
const completeSub = complete$.subscribe((e) => {
|
|
910
|
-
subscriber.next(e);
|
|
911
|
-
subscriber.complete();
|
|
912
|
-
});
|
|
913
|
-
const failedSub = failed$.subscribe((e) => {
|
|
914
|
-
subscriber.error(new Error(e.message));
|
|
915
|
-
});
|
|
916
|
-
this.actor.emit("gather:requested", {
|
|
917
|
-
correlationId,
|
|
918
|
-
annotationId,
|
|
919
|
-
resourceId,
|
|
920
|
-
contextWindow: options?.contextWindow ?? 2e3
|
|
921
|
-
}).catch((error) => {
|
|
922
|
-
subscriber.error(error);
|
|
923
|
-
});
|
|
924
|
-
return () => {
|
|
925
|
-
sub.unsubscribe();
|
|
926
|
-
completeSub.unsubscribe();
|
|
927
|
-
failedSub.unsubscribe();
|
|
928
|
-
};
|
|
601
|
+
async importKnowledgeBase(file, onProgress) {
|
|
602
|
+
const formData = new FormData();
|
|
603
|
+
formData.append("file", file);
|
|
604
|
+
const response = await this.http.post(`${this.baseUrl}/api/moderate/exchange/import`, {
|
|
605
|
+
body: formData,
|
|
606
|
+
headers: this.authHeaders()
|
|
929
607
|
});
|
|
608
|
+
return this.parseSSEStream(response, onProgress);
|
|
930
609
|
}
|
|
931
|
-
|
|
932
|
-
|
|
610
|
+
async parseSSEStream(response, onProgress) {
|
|
611
|
+
const reader = response.body.getReader();
|
|
612
|
+
const decoder = new TextDecoder();
|
|
613
|
+
let buffer = "";
|
|
614
|
+
let finalResult = { phase: "unknown" };
|
|
615
|
+
while (true) {
|
|
616
|
+
const { done, value } = await reader.read();
|
|
617
|
+
if (done) break;
|
|
618
|
+
buffer += decoder.decode(value, { stream: true });
|
|
619
|
+
const lines = buffer.split("\n");
|
|
620
|
+
buffer = lines.pop();
|
|
621
|
+
for (const line of lines) {
|
|
622
|
+
if (line.startsWith("data: ")) {
|
|
623
|
+
const event = JSON.parse(line.slice(6));
|
|
624
|
+
onProgress?.(event);
|
|
625
|
+
if (event.phase === "complete" || event.phase === "error" || event.phase === "failed") {
|
|
626
|
+
finalResult = event;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
return finalResult;
|
|
933
632
|
}
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
633
|
+
// ── System status ─────────────────────────────────────────────────────
|
|
634
|
+
async healthCheck() {
|
|
635
|
+
return this.http.get(`${this.baseUrl}/api/health`, {
|
|
636
|
+
headers: this.authHeaders()
|
|
637
|
+
}).json();
|
|
939
638
|
}
|
|
940
|
-
|
|
941
|
-
return
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
filter((e) => e.correlationId === correlationId)
|
|
945
|
-
);
|
|
946
|
-
const failed$ = this.eventBus.get("match:search-failed").pipe(
|
|
947
|
-
filter((e) => e.correlationId === correlationId)
|
|
948
|
-
);
|
|
949
|
-
const resultSub = result$.subscribe((e) => {
|
|
950
|
-
subscriber.next(e);
|
|
951
|
-
subscriber.complete();
|
|
952
|
-
});
|
|
953
|
-
const failedSub = failed$.subscribe((e) => {
|
|
954
|
-
subscriber.error(new Error(e.error));
|
|
955
|
-
});
|
|
956
|
-
this.actor.emit("match:search-requested", {
|
|
957
|
-
correlationId,
|
|
958
|
-
resourceId,
|
|
959
|
-
referenceId,
|
|
960
|
-
context,
|
|
961
|
-
limit: options?.limit ?? 10,
|
|
962
|
-
useSemanticScoring: options?.useSemanticScoring ?? true
|
|
963
|
-
}).catch((error) => {
|
|
964
|
-
subscriber.error(error);
|
|
965
|
-
});
|
|
966
|
-
return () => {
|
|
967
|
-
resultSub.unsubscribe();
|
|
968
|
-
failedSub.unsubscribe();
|
|
969
|
-
};
|
|
970
|
-
});
|
|
639
|
+
async getStatus() {
|
|
640
|
+
return this.http.get(`${this.baseUrl}/api/status`, {
|
|
641
|
+
headers: this.authHeaders()
|
|
642
|
+
}).json();
|
|
971
643
|
}
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
644
|
+
// ── Internal: ky accessor for legacy passthroughs (temporary) ─────────
|
|
645
|
+
/**
|
|
646
|
+
* Temporary escape hatch for the ongoing transport migration: namespaces
|
|
647
|
+
* that still need to issue ad-hoc HTTP calls (e.g. legacy browse/mark
|
|
648
|
+
* HTTP fallbacks) can borrow the configured `ky` instance here. Will be
|
|
649
|
+
* deleted once all namespaces route through bus channels or through
|
|
650
|
+
* typed methods on this transport.
|
|
651
|
+
*/
|
|
652
|
+
get rawHttp() {
|
|
653
|
+
return this.http;
|
|
979
654
|
}
|
|
980
|
-
|
|
981
|
-
|
|
655
|
+
/**
|
|
656
|
+
* Current access token (synchronously read from the BehaviorSubject).
|
|
657
|
+
* Used by content-transport and legacy namespace HTTP fallbacks that
|
|
658
|
+
* need to pass `auth: token` through some code paths.
|
|
659
|
+
*/
|
|
660
|
+
getToken() {
|
|
661
|
+
return this.token$.getValue() ?? void 0;
|
|
982
662
|
}
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
663
|
+
};
|
|
664
|
+
var HttpContentTransport = class {
|
|
665
|
+
constructor(transport) {
|
|
666
|
+
this.transport = transport;
|
|
667
|
+
}
|
|
668
|
+
async putBinary(request, options) {
|
|
669
|
+
const sizeBytes = request.file instanceof File ? request.file.size : request.file.length;
|
|
670
|
+
busLog("PUT", "content", {
|
|
671
|
+
name: request.name,
|
|
672
|
+
format: request.format,
|
|
673
|
+
storageUri: request.storageUri,
|
|
674
|
+
sizeBytes
|
|
675
|
+
});
|
|
676
|
+
return withSpan(
|
|
677
|
+
"content.put",
|
|
678
|
+
async () => {
|
|
679
|
+
const formData = new FormData();
|
|
680
|
+
formData.append("name", request.name);
|
|
681
|
+
formData.append("format", request.format);
|
|
682
|
+
formData.append("storageUri", request.storageUri);
|
|
683
|
+
if (request.file instanceof File) {
|
|
684
|
+
formData.append("file", request.file);
|
|
685
|
+
} else if (Buffer.isBuffer(request.file)) {
|
|
686
|
+
const blob = new Blob([new Uint8Array(request.file)], { type: request.format });
|
|
687
|
+
formData.append("file", blob, request.name);
|
|
688
|
+
} else {
|
|
689
|
+
throw new Error("file must be a File or Buffer");
|
|
997
690
|
}
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
if (pollTimer) clearTimeout(pollTimer);
|
|
1001
|
-
if (pollInterval) {
|
|
1002
|
-
clearInterval(pollInterval);
|
|
1003
|
-
pollInterval = null;
|
|
691
|
+
if (request.entityTypes && request.entityTypes.length > 0) {
|
|
692
|
+
formData.append("entityTypes", JSON.stringify(request.entityTypes));
|
|
1004
693
|
}
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
subscriber.error(new Error(status.error ?? "Generation failed"));
|
|
1024
|
-
}
|
|
1025
|
-
}).catch(() => {
|
|
1026
|
-
});
|
|
1027
|
-
}, 5e3);
|
|
1028
|
-
}, 1e4);
|
|
1029
|
-
};
|
|
1030
|
-
let activeJobId = null;
|
|
1031
|
-
const progress$ = this.eventBus.get("job:report-progress").pipe(
|
|
1032
|
-
filter((e) => e.jobId === activeJobId)
|
|
1033
|
-
);
|
|
1034
|
-
const complete$ = this.eventBus.get("job:complete").pipe(
|
|
1035
|
-
filter((e) => e.jobId === activeJobId)
|
|
1036
|
-
);
|
|
1037
|
-
const fail$ = this.eventBus.get("job:fail").pipe(
|
|
1038
|
-
filter((e) => e.jobId === activeJobId)
|
|
1039
|
-
);
|
|
1040
|
-
const progressSub = progress$.pipe(takeUntil(merge(complete$, fail$))).subscribe((e) => {
|
|
1041
|
-
subscriber.next(e.progress);
|
|
1042
|
-
if (activeJobId) resetPollTimer(activeJobId);
|
|
1043
|
-
});
|
|
1044
|
-
const completeSub = complete$.subscribe(() => {
|
|
1045
|
-
cleanup();
|
|
1046
|
-
subscriber.complete();
|
|
1047
|
-
});
|
|
1048
|
-
const failSub = fail$.subscribe((e) => {
|
|
1049
|
-
cleanup();
|
|
1050
|
-
subscriber.error(new Error(e.error));
|
|
1051
|
-
});
|
|
1052
|
-
busRequest(
|
|
1053
|
-
this.actor,
|
|
1054
|
-
"job:create",
|
|
1055
|
-
{
|
|
1056
|
-
jobType: "generation",
|
|
1057
|
-
resourceId,
|
|
1058
|
-
params: {
|
|
1059
|
-
referenceId: annotationId,
|
|
1060
|
-
title: options.title,
|
|
1061
|
-
prompt: options.prompt,
|
|
1062
|
-
language: options.language,
|
|
1063
|
-
temperature: options.temperature,
|
|
1064
|
-
maxTokens: options.maxTokens,
|
|
1065
|
-
storageUri: options.storageUri,
|
|
1066
|
-
context: options.context
|
|
1067
|
-
}
|
|
1068
|
-
},
|
|
1069
|
-
"job:created",
|
|
1070
|
-
"job:create-failed"
|
|
1071
|
-
).then(({ jobId }) => {
|
|
1072
|
-
if (jobId && !done) {
|
|
1073
|
-
activeJobId = jobId;
|
|
1074
|
-
resetPollTimer(jobId);
|
|
694
|
+
if (request.language) formData.append("language", request.language);
|
|
695
|
+
if (request.creationMethod) formData.append("creationMethod", String(request.creationMethod));
|
|
696
|
+
if (request.sourceAnnotationId) formData.append("sourceAnnotationId", String(request.sourceAnnotationId));
|
|
697
|
+
if (request.sourceResourceId) formData.append("sourceResourceId", String(request.sourceResourceId));
|
|
698
|
+
if (request.generationPrompt) formData.append("generationPrompt", request.generationPrompt);
|
|
699
|
+
if (request.generator) formData.append("generator", JSON.stringify(request.generator));
|
|
700
|
+
if (request.isDraft !== void 0) formData.append("isDraft", String(request.isDraft));
|
|
701
|
+
const result = await this.transport.rawHttp.post(`${this.transport.baseUrl}/resources`, {
|
|
702
|
+
body: formData,
|
|
703
|
+
headers: this.requestHeaders(options?.auth)
|
|
704
|
+
}).json();
|
|
705
|
+
return { resourceId: result.resourceId };
|
|
706
|
+
},
|
|
707
|
+
{
|
|
708
|
+
kind: SpanKind.CLIENT,
|
|
709
|
+
attrs: {
|
|
710
|
+
"content.format": request.format,
|
|
711
|
+
"content.size_bytes": sizeBytes
|
|
1075
712
|
}
|
|
1076
|
-
}
|
|
1077
|
-
cleanup();
|
|
1078
|
-
subscriber.error(error);
|
|
1079
|
-
});
|
|
1080
|
-
return () => {
|
|
1081
|
-
cleanup();
|
|
1082
|
-
progressSub.unsubscribe();
|
|
1083
|
-
completeSub.unsubscribe();
|
|
1084
|
-
failSub.unsubscribe();
|
|
1085
|
-
};
|
|
1086
|
-
});
|
|
1087
|
-
}
|
|
1088
|
-
async cloneToken(resourceId) {
|
|
1089
|
-
return busRequest(
|
|
1090
|
-
this.actor,
|
|
1091
|
-
"yield:clone-token-requested",
|
|
1092
|
-
{ resourceId },
|
|
1093
|
-
"yield:clone-token-generated",
|
|
1094
|
-
"yield:clone-token-failed"
|
|
713
|
+
}
|
|
1095
714
|
);
|
|
1096
715
|
}
|
|
1097
|
-
async
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
"
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
716
|
+
async getBinary(resourceId, options) {
|
|
717
|
+
busLog("GET", "content", { resourceId, accept: options?.accept });
|
|
718
|
+
return withSpan(
|
|
719
|
+
"content.get",
|
|
720
|
+
async () => {
|
|
721
|
+
const response = await this.transport.rawHttp.get(`${this.transport.baseUrl}/resources/${resourceId}`, {
|
|
722
|
+
headers: {
|
|
723
|
+
Accept: options?.accept ?? "text/plain",
|
|
724
|
+
...this.requestHeaders(options?.auth)
|
|
725
|
+
}
|
|
726
|
+
});
|
|
727
|
+
const contentType = response.headers.get("content-type") || "application/octet-stream";
|
|
728
|
+
const data = await response.arrayBuffer();
|
|
729
|
+
return { data, contentType };
|
|
730
|
+
},
|
|
731
|
+
{ kind: SpanKind.CLIENT, attrs: { "resource.id": resourceId } }
|
|
1104
732
|
);
|
|
1105
|
-
return result.sourceResource;
|
|
1106
733
|
}
|
|
1107
|
-
async
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
"
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
734
|
+
async getBinaryStream(resourceId, options) {
|
|
735
|
+
busLog("GET", "content", { resourceId, accept: options?.accept, stream: true });
|
|
736
|
+
return withSpan(
|
|
737
|
+
"content.get",
|
|
738
|
+
async () => {
|
|
739
|
+
const response = await this.transport.rawHttp.get(`${this.transport.baseUrl}/resources/${resourceId}`, {
|
|
740
|
+
headers: {
|
|
741
|
+
Accept: options?.accept ?? "text/plain",
|
|
742
|
+
...this.requestHeaders(options?.auth)
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
const contentType = response.headers.get("content-type") || "application/octet-stream";
|
|
746
|
+
if (!response.body) {
|
|
747
|
+
throw new Error("Response body is null - cannot create stream");
|
|
748
|
+
}
|
|
749
|
+
return { stream: response.body, contentType };
|
|
750
|
+
},
|
|
751
|
+
{
|
|
752
|
+
kind: SpanKind.CLIENT,
|
|
753
|
+
attrs: { "resource.id": resourceId, "content.stream": true }
|
|
754
|
+
}
|
|
1114
755
|
);
|
|
1115
756
|
}
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
// src/namespaces/beckon.ts
|
|
1119
|
-
var BeckonNamespace = class {
|
|
1120
|
-
constructor(actor) {
|
|
1121
|
-
this.actor = actor;
|
|
1122
|
-
}
|
|
1123
|
-
attention(annotationId, resourceId) {
|
|
1124
|
-
this.actor.emit("beckon:focus", { annotationId, resourceId }).catch(() => {
|
|
1125
|
-
});
|
|
1126
|
-
}
|
|
1127
|
-
};
|
|
1128
|
-
|
|
1129
|
-
// src/namespaces/job.ts
|
|
1130
|
-
var JobNamespace = class {
|
|
1131
|
-
constructor(actor) {
|
|
1132
|
-
this.actor = actor;
|
|
1133
|
-
}
|
|
1134
|
-
async status(jobId) {
|
|
1135
|
-
return busRequest(
|
|
1136
|
-
this.actor,
|
|
1137
|
-
"job:status-requested",
|
|
1138
|
-
{ jobId },
|
|
1139
|
-
"job:status-result",
|
|
1140
|
-
"job:status-failed"
|
|
1141
|
-
);
|
|
757
|
+
dispose() {
|
|
1142
758
|
}
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
const
|
|
1146
|
-
const
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
if (
|
|
1151
|
-
return status;
|
|
1152
|
-
}
|
|
1153
|
-
if (Date.now() - startTime > timeout7) {
|
|
1154
|
-
throw new Error(`Job polling timeout after ${timeout7}ms`);
|
|
1155
|
-
}
|
|
1156
|
-
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
759
|
+
/** Auth header + W3C trace propagation for the active span. */
|
|
760
|
+
requestHeaders(override) {
|
|
761
|
+
const token = override ?? this.transport.getToken();
|
|
762
|
+
const headers = token ? { Authorization: `Bearer ${token}` } : {};
|
|
763
|
+
const trace = getActiveTraceparent();
|
|
764
|
+
if (trace) {
|
|
765
|
+
headers["traceparent"] = trace.traceparent;
|
|
766
|
+
if (trace.tracestate) headers["tracestate"] = trace.tracestate;
|
|
1157
767
|
}
|
|
1158
|
-
|
|
1159
|
-
async cancel(jobId, type) {
|
|
1160
|
-
await this.actor.emit("job:cancel-requested", { jobId, type });
|
|
1161
|
-
}
|
|
1162
|
-
};
|
|
1163
|
-
var AuthNamespace = class {
|
|
1164
|
-
constructor(http, getToken) {
|
|
1165
|
-
this.http = http;
|
|
1166
|
-
this.getToken = getToken;
|
|
1167
|
-
}
|
|
1168
|
-
async password(emailStr, passwordStr) {
|
|
1169
|
-
return this.http.authenticatePassword(email(emailStr), passwordStr);
|
|
1170
|
-
}
|
|
1171
|
-
async google(credential) {
|
|
1172
|
-
return this.http.authenticateGoogle(googleCredential(credential));
|
|
1173
|
-
}
|
|
1174
|
-
async refresh(token) {
|
|
1175
|
-
return this.http.refreshToken(refreshToken(token));
|
|
1176
|
-
}
|
|
1177
|
-
async logout() {
|
|
1178
|
-
await this.http.logout({ auth: this.getToken() });
|
|
1179
|
-
}
|
|
1180
|
-
async me() {
|
|
1181
|
-
return this.http.getMe({ auth: this.getToken() });
|
|
1182
|
-
}
|
|
1183
|
-
async acceptTerms() {
|
|
1184
|
-
await this.http.acceptTerms({ auth: this.getToken() });
|
|
1185
|
-
}
|
|
1186
|
-
async mcpToken() {
|
|
1187
|
-
return this.http.generateMCPToken({ auth: this.getToken() });
|
|
1188
|
-
}
|
|
1189
|
-
async mediaToken(resourceId) {
|
|
1190
|
-
return this.http.getMediaToken(resourceId, { auth: this.getToken() });
|
|
1191
|
-
}
|
|
1192
|
-
};
|
|
1193
|
-
|
|
1194
|
-
// src/namespaces/admin.ts
|
|
1195
|
-
var AdminNamespace = class {
|
|
1196
|
-
constructor(http, getToken) {
|
|
1197
|
-
this.http = http;
|
|
1198
|
-
this.getToken = getToken;
|
|
1199
|
-
}
|
|
1200
|
-
async users() {
|
|
1201
|
-
const result = await this.http.listUsers({ auth: this.getToken() });
|
|
1202
|
-
return result.users;
|
|
1203
|
-
}
|
|
1204
|
-
async userStats() {
|
|
1205
|
-
return this.http.getUserStats({ auth: this.getToken() });
|
|
1206
|
-
}
|
|
1207
|
-
async updateUser(userId, data) {
|
|
1208
|
-
const result = await this.http.updateUser(userId, data, { auth: this.getToken() });
|
|
1209
|
-
return result.user;
|
|
1210
|
-
}
|
|
1211
|
-
async oauthConfig() {
|
|
1212
|
-
return this.http.getOAuthConfig({ auth: this.getToken() });
|
|
1213
|
-
}
|
|
1214
|
-
async healthCheck() {
|
|
1215
|
-
return this.http.healthCheck({ auth: this.getToken() });
|
|
1216
|
-
}
|
|
1217
|
-
async status() {
|
|
1218
|
-
return this.http.getStatus({ auth: this.getToken() });
|
|
1219
|
-
}
|
|
1220
|
-
async backup() {
|
|
1221
|
-
return this.http.backupKnowledgeBase({ auth: this.getToken() });
|
|
1222
|
-
}
|
|
1223
|
-
async restore(file, onProgress) {
|
|
1224
|
-
return this.http.restoreKnowledgeBase(file, { auth: this.getToken(), onProgress });
|
|
1225
|
-
}
|
|
1226
|
-
async exportKnowledgeBase(params) {
|
|
1227
|
-
return this.http.exportKnowledgeBase(params, { auth: this.getToken() });
|
|
1228
|
-
}
|
|
1229
|
-
async importKnowledgeBase(file, onProgress) {
|
|
1230
|
-
return this.http.importKnowledgeBase(file, { auth: this.getToken(), onProgress });
|
|
1231
|
-
}
|
|
1232
|
-
};
|
|
1233
|
-
var RESOURCE_SCOPED_CHANNELS = [
|
|
1234
|
-
...PERSISTED_EVENT_TYPES.filter((t) => t !== "mark:entity-type-added"),
|
|
1235
|
-
...RESOURCE_BROADCAST_TYPES
|
|
1236
|
-
];
|
|
1237
|
-
var BUS_RESULT_CHANNELS = [
|
|
1238
|
-
"browse:resources-result",
|
|
1239
|
-
"browse:resources-failed",
|
|
1240
|
-
"browse:resource-result",
|
|
1241
|
-
"browse:resource-failed",
|
|
1242
|
-
"browse:annotations-result",
|
|
1243
|
-
"browse:annotations-failed",
|
|
1244
|
-
"browse:annotation-result",
|
|
1245
|
-
"browse:annotation-failed",
|
|
1246
|
-
"browse:annotation-history-result",
|
|
1247
|
-
"browse:annotation-history-failed",
|
|
1248
|
-
"browse:events-result",
|
|
1249
|
-
"browse:events-failed",
|
|
1250
|
-
"browse:referenced-by-result",
|
|
1251
|
-
"browse:referenced-by-failed",
|
|
1252
|
-
"browse:entity-types-result",
|
|
1253
|
-
"browse:entity-types-failed",
|
|
1254
|
-
"browse:directory-result",
|
|
1255
|
-
"browse:directory-failed",
|
|
1256
|
-
"browse:annotation-context-result",
|
|
1257
|
-
"browse:annotation-context-failed",
|
|
1258
|
-
"mark:delete-ok",
|
|
1259
|
-
"mark:delete-failed",
|
|
1260
|
-
"mark:create-ok",
|
|
1261
|
-
"mark:create-failed",
|
|
1262
|
-
"match:search-results",
|
|
1263
|
-
"match:search-failed",
|
|
1264
|
-
"gather:complete",
|
|
1265
|
-
"gather:failed",
|
|
1266
|
-
"gather:annotation-progress",
|
|
1267
|
-
"gather:annotation-finished",
|
|
1268
|
-
"gather:summary-result",
|
|
1269
|
-
"gather:summary-failed",
|
|
1270
|
-
"bind:body-updated",
|
|
1271
|
-
"bind:body-update-failed",
|
|
1272
|
-
"job:report-progress",
|
|
1273
|
-
"job:complete",
|
|
1274
|
-
"job:fail",
|
|
1275
|
-
"job:status-result",
|
|
1276
|
-
"job:status-failed",
|
|
1277
|
-
"job:created",
|
|
1278
|
-
"job:create-failed",
|
|
1279
|
-
"job:claimed",
|
|
1280
|
-
"job:claim-failed",
|
|
1281
|
-
"yield:clone-token-generated",
|
|
1282
|
-
"yield:clone-token-failed",
|
|
1283
|
-
"yield:clone-resource-result",
|
|
1284
|
-
"yield:clone-resource-failed",
|
|
1285
|
-
"yield:clone-created",
|
|
1286
|
-
"yield:clone-create-failed",
|
|
1287
|
-
"mark:entity-type-added",
|
|
1288
|
-
"beckon:focus",
|
|
1289
|
-
"beckon:sparkle",
|
|
1290
|
-
"bus:resume-gap"
|
|
1291
|
-
];
|
|
1292
|
-
var ACTOR_TO_LOCAL_BRIDGES = BUS_RESULT_CHANNELS;
|
|
1293
|
-
var APIError = class extends Error {
|
|
1294
|
-
constructor(message, status, statusText, details) {
|
|
1295
|
-
super(message);
|
|
1296
|
-
this.status = status;
|
|
1297
|
-
this.statusText = statusText;
|
|
1298
|
-
this.details = details;
|
|
1299
|
-
this.name = "APIError";
|
|
768
|
+
return headers;
|
|
1300
769
|
}
|
|
1301
770
|
};
|
|
1302
|
-
var SemiontApiClient = class {
|
|
1303
|
-
http;
|
|
1304
|
-
baseUrl;
|
|
1305
|
-
/**
|
|
1306
|
-
* Workspace-scoped EventBus — owned by the client, constructed
|
|
1307
|
-
* internally, never accepted from config. Private: all bus access
|
|
1308
|
-
* goes through `client.emit` / `client.on` / `client.stream`.
|
|
1309
|
-
*/
|
|
1310
|
-
eventBus;
|
|
1311
|
-
logger;
|
|
1312
|
-
/**
|
|
1313
|
-
* Observable token source. All auth reads from this.
|
|
1314
|
-
*/
|
|
1315
|
-
token$;
|
|
1316
|
-
_actor = null;
|
|
1317
|
-
// ── Verb-oriented namespace API ──────────────────────────────────────────
|
|
1318
|
-
browse;
|
|
1319
|
-
mark;
|
|
1320
|
-
bind;
|
|
1321
|
-
gather;
|
|
1322
|
-
match;
|
|
1323
|
-
yield;
|
|
1324
|
-
beckon;
|
|
1325
|
-
job;
|
|
1326
|
-
auth;
|
|
1327
|
-
admin;
|
|
1328
|
-
constructor(config) {
|
|
1329
|
-
const { baseUrl: baseUrl3, timeout: timeout7 = 3e4, retry = 2, logger, tokenRefresher } = config;
|
|
1330
|
-
this.eventBus = new EventBus();
|
|
1331
|
-
this.logger = logger;
|
|
1332
|
-
this.baseUrl = baseUrl3.endsWith("/") ? baseUrl3.slice(0, -1) : baseUrl3;
|
|
1333
|
-
const retryConfig = tokenRefresher ? {
|
|
1334
|
-
limit: 1,
|
|
1335
|
-
methods: ["get", "post", "put", "patch", "delete", "head", "options"],
|
|
1336
|
-
statusCodes: [401, 408, 413, 429, 500, 502, 503, 504]
|
|
1337
|
-
} : retry;
|
|
1338
|
-
this.http = ky.create({
|
|
1339
|
-
timeout: timeout7,
|
|
1340
|
-
retry: retryConfig,
|
|
1341
|
-
credentials: "include",
|
|
1342
|
-
hooks: {
|
|
1343
|
-
beforeRequest: [
|
|
1344
|
-
(request) => {
|
|
1345
|
-
if (this.logger) {
|
|
1346
|
-
this.logger.debug("HTTP Request", {
|
|
1347
|
-
type: "http_request",
|
|
1348
|
-
url: request.url,
|
|
1349
|
-
method: request.method,
|
|
1350
|
-
timestamp: Date.now(),
|
|
1351
|
-
hasAuth: request.headers.has("Authorization")
|
|
1352
|
-
});
|
|
1353
|
-
}
|
|
1354
|
-
}
|
|
1355
|
-
],
|
|
1356
|
-
beforeRetry: tokenRefresher ? [
|
|
1357
|
-
async ({ request, error }) => {
|
|
1358
|
-
if (!(error instanceof HTTPError) || error.response.status !== 401) {
|
|
1359
|
-
return void 0;
|
|
1360
|
-
}
|
|
1361
|
-
try {
|
|
1362
|
-
const newToken = await tokenRefresher();
|
|
1363
|
-
if (!newToken) return ky.stop;
|
|
1364
|
-
request.headers.set("Authorization", `Bearer ${newToken}`);
|
|
1365
|
-
return void 0;
|
|
1366
|
-
} catch {
|
|
1367
|
-
return ky.stop;
|
|
1368
|
-
}
|
|
1369
|
-
}
|
|
1370
|
-
] : [],
|
|
1371
|
-
afterResponse: [
|
|
1372
|
-
(request, _options, response) => {
|
|
1373
|
-
if (this.logger) {
|
|
1374
|
-
this.logger.debug("HTTP Response", {
|
|
1375
|
-
type: "http_response",
|
|
1376
|
-
url: request.url,
|
|
1377
|
-
method: request.method,
|
|
1378
|
-
status: response.status,
|
|
1379
|
-
statusText: response.statusText
|
|
1380
|
-
});
|
|
1381
|
-
}
|
|
1382
|
-
return response;
|
|
1383
|
-
}
|
|
1384
|
-
],
|
|
1385
|
-
beforeError: [
|
|
1386
|
-
async (error) => {
|
|
1387
|
-
const { response, request } = error;
|
|
1388
|
-
if (response) {
|
|
1389
|
-
const body = await response.json().catch(() => ({}));
|
|
1390
|
-
if (this.logger) {
|
|
1391
|
-
this.logger.error("HTTP Request Failed", {
|
|
1392
|
-
type: "http_error",
|
|
1393
|
-
url: request.url,
|
|
1394
|
-
method: request.method,
|
|
1395
|
-
status: response.status,
|
|
1396
|
-
statusText: response.statusText,
|
|
1397
|
-
error: body.message || `HTTP ${response.status}: ${response.statusText}`
|
|
1398
|
-
});
|
|
1399
|
-
}
|
|
1400
|
-
throw new APIError(
|
|
1401
|
-
body.message || `HTTP ${response.status}: ${response.statusText}`,
|
|
1402
|
-
response.status,
|
|
1403
|
-
response.statusText,
|
|
1404
|
-
body
|
|
1405
|
-
);
|
|
1406
|
-
}
|
|
1407
|
-
return error;
|
|
1408
|
-
}
|
|
1409
|
-
]
|
|
1410
|
-
}
|
|
1411
|
-
});
|
|
1412
|
-
this.token$ = config.token$ ?? new BehaviorSubject(null);
|
|
1413
|
-
const getToken = () => this.token$.getValue() ?? void 0;
|
|
1414
|
-
this.browse = new BrowseNamespace(this, this.eventBus, getToken, this.actor);
|
|
1415
|
-
this.mark = new MarkNamespace(this, this.eventBus, getToken, this.actor);
|
|
1416
|
-
this.bind = new BindNamespace(this.actor);
|
|
1417
|
-
this.gather = new GatherNamespace(this.eventBus, this.actor);
|
|
1418
|
-
this.match = new MatchNamespace(this.eventBus, this.actor);
|
|
1419
|
-
this.yield = new YieldNamespace(this, this.eventBus, getToken, this.actor);
|
|
1420
|
-
this.beckon = new BeckonNamespace(this.actor);
|
|
1421
|
-
this.job = new JobNamespace(this.actor);
|
|
1422
|
-
this.auth = new AuthNamespace(this, getToken);
|
|
1423
|
-
this.admin = new AdminNamespace(this, getToken);
|
|
1424
|
-
this.token$.subscribe((token) => {
|
|
1425
|
-
if (token && !this._actorStarted) {
|
|
1426
|
-
this._actorStarted = true;
|
|
1427
|
-
this.actor.start();
|
|
1428
|
-
}
|
|
1429
|
-
});
|
|
1430
|
-
}
|
|
1431
|
-
_actorStarted = false;
|
|
1432
|
-
get actor() {
|
|
1433
|
-
if (!this._actor) {
|
|
1434
|
-
this._actor = createActorVM({
|
|
1435
|
-
baseUrl: this.baseUrl,
|
|
1436
|
-
token: () => this.token$.getValue() ?? "",
|
|
1437
|
-
channels: [...BUS_RESULT_CHANNELS]
|
|
1438
|
-
});
|
|
1439
|
-
for (const channel of ACTOR_TO_LOCAL_BRIDGES) {
|
|
1440
|
-
this._actor.on$(channel).subscribe((payload) => {
|
|
1441
|
-
this.eventBus.get(channel).next(payload);
|
|
1442
|
-
});
|
|
1443
|
-
}
|
|
1444
|
-
}
|
|
1445
|
-
return this._actor;
|
|
1446
|
-
}
|
|
1447
|
-
activeResource = null;
|
|
1448
|
-
/**
|
|
1449
|
-
* Subscribe the bus actor to the resource-scoped SSE stream for a single
|
|
1450
|
-
* resource and bridge incoming scoped events into the workspace event bus.
|
|
1451
|
-
*
|
|
1452
|
-
* **One distinct scope at a time**: the client supports subscriptions
|
|
1453
|
-
* to a single resource scope concurrently. Multiple calls with the
|
|
1454
|
-
* **same** resourceId are ref-counted — each returns an independent
|
|
1455
|
-
* unsubscribe; the underlying SSE scope is torn down only when the
|
|
1456
|
-
* last unsubscribe fires. Calling with a **different** resourceId
|
|
1457
|
-
* while a subscription is live throws. Widening this to multiple
|
|
1458
|
-
* concurrent scopes is deferred until a product requirement (e.g.
|
|
1459
|
-
* split-pane viewer, headless fleet-watcher) forces the design.
|
|
1460
|
-
*
|
|
1461
|
-
* @returns a disposer that decrements the ref count (and tears down
|
|
1462
|
-
* the SSE scope + bridges when it reaches zero).
|
|
1463
|
-
*/
|
|
1464
|
-
subscribeToResource(resourceId) {
|
|
1465
|
-
if (this.activeResource) {
|
|
1466
|
-
if (this.activeResource.resourceId !== resourceId) {
|
|
1467
|
-
throw new Error(
|
|
1468
|
-
`SemiontApiClient already subscribed to resource ${this.activeResource.resourceId}; call the unsubscribe returned from the previous subscribeToResource before subscribing to ${resourceId}.`
|
|
1469
|
-
);
|
|
1470
|
-
}
|
|
1471
|
-
this.activeResource.refCount++;
|
|
1472
|
-
return this.makeUnsubscriber();
|
|
1473
|
-
}
|
|
1474
|
-
this.actor.addChannels([...RESOURCE_SCOPED_CHANNELS], resourceId);
|
|
1475
|
-
const bridgeSubs = [];
|
|
1476
|
-
for (const channel of RESOURCE_SCOPED_CHANNELS) {
|
|
1477
|
-
bridgeSubs.push(
|
|
1478
|
-
this.actor.on$(channel).subscribe((payload) => {
|
|
1479
|
-
this.eventBus.get(channel).next(payload);
|
|
1480
|
-
})
|
|
1481
|
-
);
|
|
1482
|
-
}
|
|
1483
|
-
this.activeResource = { resourceId, refCount: 1, bridgeSubs };
|
|
1484
|
-
return this.makeUnsubscriber();
|
|
1485
|
-
}
|
|
1486
|
-
makeUnsubscriber() {
|
|
1487
|
-
let called = false;
|
|
1488
|
-
return () => {
|
|
1489
|
-
if (called) return;
|
|
1490
|
-
called = true;
|
|
1491
|
-
if (!this.activeResource) return;
|
|
1492
|
-
this.activeResource.refCount--;
|
|
1493
|
-
if (this.activeResource.refCount > 0) return;
|
|
1494
|
-
for (const sub of this.activeResource.bridgeSubs) sub.unsubscribe();
|
|
1495
|
-
this.actor.removeChannels([...RESOURCE_SCOPED_CHANNELS]);
|
|
1496
|
-
this.activeResource = null;
|
|
1497
|
-
};
|
|
1498
|
-
}
|
|
1499
|
-
dispose() {
|
|
1500
|
-
if (this.activeResource) {
|
|
1501
|
-
for (const sub of this.activeResource.bridgeSubs) sub.unsubscribe();
|
|
1502
|
-
this.activeResource = null;
|
|
1503
|
-
}
|
|
1504
|
-
if (this._actor) {
|
|
1505
|
-
this._actor.dispose();
|
|
1506
|
-
this._actor = null;
|
|
1507
|
-
}
|
|
1508
|
-
}
|
|
1509
|
-
// ── Event bus surface ─────────────────────────────────────────
|
|
1510
|
-
// The ONE public path to the workspace bus. VMs, session, and
|
|
1511
|
-
// components route through these methods; `this.eventBus` remains
|
|
1512
|
-
// the internal owner and will be privatized once all callers
|
|
1513
|
-
// have migrated.
|
|
1514
|
-
/** Emit an event on the internal bus. */
|
|
1515
|
-
emit(channel, payload) {
|
|
1516
|
-
this.eventBus.get(channel).next(payload);
|
|
1517
|
-
}
|
|
1518
|
-
/** Subscribe to an event on the internal bus; returns unsubscribe. */
|
|
1519
|
-
on(channel, handler) {
|
|
1520
|
-
const sub = this.eventBus.get(channel).subscribe(handler);
|
|
1521
|
-
return () => sub.unsubscribe();
|
|
1522
|
-
}
|
|
1523
|
-
/** Read-only observable for a bus channel. Consumers `.pipe(...)` over this. */
|
|
1524
|
-
stream(channel) {
|
|
1525
|
-
return this.eventBus.get(channel).asObservable();
|
|
1526
|
-
}
|
|
1527
|
-
/**
|
|
1528
|
-
* Build the `Authorization: Bearer <token>` header. If the caller passed
|
|
1529
|
-
* an explicit `{ auth }` it wins (used by session-internal throwaway
|
|
1530
|
-
* clients that need to run a validation request with a specific token).
|
|
1531
|
-
* Otherwise the current value of `this.token$` is used, so external
|
|
1532
|
-
* callers never have to plumb the token themselves.
|
|
1533
|
-
*/
|
|
1534
|
-
authHeaders(options) {
|
|
1535
|
-
const token = options?.auth ?? this.token$.getValue() ?? void 0;
|
|
1536
|
-
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
1537
|
-
}
|
|
1538
|
-
// ============================================================================
|
|
1539
|
-
// AUTHENTICATION
|
|
1540
|
-
// ============================================================================
|
|
1541
|
-
async authenticatePassword(email, password, options) {
|
|
1542
|
-
return this.http.post(`${this.baseUrl}/api/tokens/password`, {
|
|
1543
|
-
json: { email, password },
|
|
1544
|
-
headers: this.authHeaders(options)
|
|
1545
|
-
}).json();
|
|
1546
|
-
}
|
|
1547
|
-
async refreshToken(token, options) {
|
|
1548
|
-
return this.http.post(`${this.baseUrl}/api/tokens/refresh`, {
|
|
1549
|
-
json: { refreshToken: token },
|
|
1550
|
-
headers: this.authHeaders(options)
|
|
1551
|
-
}).json();
|
|
1552
|
-
}
|
|
1553
|
-
async authenticateGoogle(credential, options) {
|
|
1554
|
-
return this.http.post(`${this.baseUrl}/api/tokens/google`, {
|
|
1555
|
-
json: { credential },
|
|
1556
|
-
headers: this.authHeaders(options)
|
|
1557
|
-
}).json();
|
|
1558
|
-
}
|
|
1559
|
-
async generateMCPToken(options) {
|
|
1560
|
-
return this.http.post(`${this.baseUrl}/api/tokens/mcp-generate`, {
|
|
1561
|
-
headers: this.authHeaders(options)
|
|
1562
|
-
}).json();
|
|
1563
|
-
}
|
|
1564
|
-
async getMediaToken(resourceId, options) {
|
|
1565
|
-
return this.http.post(`${this.baseUrl}/api/tokens/media`, {
|
|
1566
|
-
json: { resourceId },
|
|
1567
|
-
headers: this.authHeaders(options)
|
|
1568
|
-
}).json();
|
|
1569
|
-
}
|
|
1570
|
-
// ============================================================================
|
|
1571
|
-
// USERS
|
|
1572
|
-
// ============================================================================
|
|
1573
|
-
async getMe(options) {
|
|
1574
|
-
return this.http.get(`${this.baseUrl}/api/users/me`, {
|
|
1575
|
-
headers: this.authHeaders(options)
|
|
1576
|
-
}).json();
|
|
1577
|
-
}
|
|
1578
|
-
async acceptTerms(options) {
|
|
1579
|
-
return this.http.post(`${this.baseUrl}/api/users/accept-terms`, {
|
|
1580
|
-
headers: this.authHeaders(options)
|
|
1581
|
-
}).json();
|
|
1582
|
-
}
|
|
1583
|
-
async logout(options) {
|
|
1584
|
-
return this.http.post(`${this.baseUrl}/api/users/logout`, {
|
|
1585
|
-
headers: this.authHeaders(options)
|
|
1586
|
-
}).json();
|
|
1587
|
-
}
|
|
1588
|
-
// ============================================================================
|
|
1589
|
-
// RESOURCES
|
|
1590
|
-
// ============================================================================
|
|
1591
|
-
/**
|
|
1592
|
-
* Create a new resource with binary content support
|
|
1593
|
-
*
|
|
1594
|
-
* @param data - Resource creation data
|
|
1595
|
-
* @param data.name - Resource name
|
|
1596
|
-
* @param data.file - File object or Buffer with binary content
|
|
1597
|
-
* @param data.format - MIME type (e.g., 'text/markdown', 'image/png')
|
|
1598
|
-
* @param data.entityTypes - Optional array of entity types
|
|
1599
|
-
* @param data.language - Optional ISO 639-1 language code
|
|
1600
|
-
* @param data.creationMethod - Optional creation method
|
|
1601
|
-
* @param data.sourceAnnotationId - Optional source annotation ID
|
|
1602
|
-
* @param data.sourceResourceId - Optional source resource ID
|
|
1603
|
-
* @param data.generationPrompt - Optional prompt that drove AI generation
|
|
1604
|
-
* @param data.generator - Optional Agent(s) that generated the content
|
|
1605
|
-
* @param options - Request options including auth
|
|
1606
|
-
*/
|
|
1607
|
-
async yieldResource(data, options) {
|
|
1608
|
-
const formData = new FormData();
|
|
1609
|
-
formData.append("name", data.name);
|
|
1610
|
-
formData.append("format", data.format);
|
|
1611
|
-
formData.append("storageUri", data.storageUri);
|
|
1612
|
-
if (data.file instanceof File) {
|
|
1613
|
-
formData.append("file", data.file);
|
|
1614
|
-
} else if (Buffer.isBuffer(data.file)) {
|
|
1615
|
-
const blob = new Blob([new Uint8Array(data.file)], { type: data.format });
|
|
1616
|
-
formData.append("file", blob, data.name);
|
|
1617
|
-
} else {
|
|
1618
|
-
throw new Error("file must be a File or Buffer");
|
|
1619
|
-
}
|
|
1620
|
-
if (data.entityTypes && data.entityTypes.length > 0) {
|
|
1621
|
-
formData.append("entityTypes", JSON.stringify(data.entityTypes));
|
|
1622
|
-
}
|
|
1623
|
-
if (data.language) {
|
|
1624
|
-
formData.append("language", data.language);
|
|
1625
|
-
}
|
|
1626
|
-
if (data.creationMethod) {
|
|
1627
|
-
formData.append("creationMethod", data.creationMethod);
|
|
1628
|
-
}
|
|
1629
|
-
if (data.sourceAnnotationId) {
|
|
1630
|
-
formData.append("sourceAnnotationId", data.sourceAnnotationId);
|
|
1631
|
-
}
|
|
1632
|
-
if (data.sourceResourceId) {
|
|
1633
|
-
formData.append("sourceResourceId", data.sourceResourceId);
|
|
1634
|
-
}
|
|
1635
|
-
if (data.generationPrompt) {
|
|
1636
|
-
formData.append("generationPrompt", data.generationPrompt);
|
|
1637
|
-
}
|
|
1638
|
-
if (data.generator) {
|
|
1639
|
-
formData.append("generator", JSON.stringify(data.generator));
|
|
1640
|
-
}
|
|
1641
|
-
if (data.isDraft !== void 0) {
|
|
1642
|
-
formData.append("isDraft", String(data.isDraft));
|
|
1643
|
-
}
|
|
1644
|
-
return this.http.post(`${this.baseUrl}/resources`, {
|
|
1645
|
-
body: formData,
|
|
1646
|
-
headers: this.authHeaders(options)
|
|
1647
|
-
}).json();
|
|
1648
|
-
}
|
|
1649
|
-
async browseResource(id, _options) {
|
|
1650
|
-
return busRequest(this.actor, "browse:resource-requested", { resourceId: id }, "browse:resource-result", "browse:resource-failed");
|
|
1651
|
-
}
|
|
1652
|
-
/**
|
|
1653
|
-
* Get resource representation using W3C content negotiation
|
|
1654
|
-
* Returns raw binary content (images, PDFs, text, etc.) with content type
|
|
1655
|
-
*
|
|
1656
|
-
* @param resourceUri - Full resource URI
|
|
1657
|
-
* @param options - Options including Accept header for content negotiation and auth
|
|
1658
|
-
* @returns Object with data (ArrayBuffer) and contentType (string)
|
|
1659
|
-
*
|
|
1660
|
-
* @example
|
|
1661
|
-
* ```typescript
|
|
1662
|
-
* // Get markdown representation
|
|
1663
|
-
* const { data, contentType } = await client.getResourceRepresentation(rUri, { accept: 'text/markdown', auth: token });
|
|
1664
|
-
* const markdown = new TextDecoder().decode(data);
|
|
1665
|
-
*
|
|
1666
|
-
* // Get image representation
|
|
1667
|
-
* const { data, contentType } = await client.getResourceRepresentation(rUri, { accept: 'image/png', auth: token });
|
|
1668
|
-
* const blob = new Blob([data], { type: contentType });
|
|
1669
|
-
*
|
|
1670
|
-
* // Get PDF representation
|
|
1671
|
-
* const { data, contentType } = await client.getResourceRepresentation(rUri, { accept: 'application/pdf', auth: token });
|
|
1672
|
-
* ```
|
|
1673
|
-
*/
|
|
1674
|
-
async getResourceRepresentation(id, options) {
|
|
1675
|
-
const response = await this.http.get(`${this.baseUrl}/resources/${id}`, {
|
|
1676
|
-
headers: {
|
|
1677
|
-
Accept: options?.accept || "text/plain",
|
|
1678
|
-
...this.authHeaders(options)
|
|
1679
|
-
}
|
|
1680
|
-
});
|
|
1681
|
-
const contentType = response.headers.get("content-type") || "application/octet-stream";
|
|
1682
|
-
const data = await response.arrayBuffer();
|
|
1683
|
-
return { data, contentType };
|
|
1684
|
-
}
|
|
1685
|
-
/**
|
|
1686
|
-
* Get resource representation as a stream using W3C content negotiation
|
|
1687
|
-
* Returns streaming binary content (for large files: videos, large PDFs, etc.)
|
|
1688
|
-
*
|
|
1689
|
-
* Use this for large files to avoid loading entire content into memory.
|
|
1690
|
-
* The stream is consumed incrementally and the backend connection stays open
|
|
1691
|
-
* until the stream is fully consumed or closed.
|
|
1692
|
-
*
|
|
1693
|
-
* @param resourceUri - Full resource URI
|
|
1694
|
-
* @param options - Options including Accept header for content negotiation and auth
|
|
1695
|
-
* @returns Object with stream (ReadableStream) and contentType (string)
|
|
1696
|
-
*
|
|
1697
|
-
* @example
|
|
1698
|
-
* ```typescript
|
|
1699
|
-
* // Stream large file
|
|
1700
|
-
* const { stream, contentType } = await client.getResourceRepresentationStream(rUri, {
|
|
1701
|
-
* accept: 'video/mp4',
|
|
1702
|
-
* auth: token
|
|
1703
|
-
* });
|
|
1704
|
-
*
|
|
1705
|
-
* // Consume stream chunk by chunk (never loads entire file into memory)
|
|
1706
|
-
* for await (const chunk of stream) {
|
|
1707
|
-
* // Process chunk
|
|
1708
|
-
* console.log(`Received ${chunk.length} bytes`);
|
|
1709
|
-
* }
|
|
1710
|
-
*
|
|
1711
|
-
* // Or pipe to a file in Node.js
|
|
1712
|
-
* const fileStream = fs.createWriteStream('output.mp4');
|
|
1713
|
-
* const reader = stream.getReader();
|
|
1714
|
-
* while (true) {
|
|
1715
|
-
* const { done, value } = await reader.read();
|
|
1716
|
-
* if (done) break;
|
|
1717
|
-
* fileStream.write(value);
|
|
1718
|
-
* }
|
|
1719
|
-
* ```
|
|
1720
|
-
*/
|
|
1721
|
-
async getResourceRepresentationStream(id, options) {
|
|
1722
|
-
const response = await this.http.get(`${this.baseUrl}/resources/${id}`, {
|
|
1723
|
-
headers: {
|
|
1724
|
-
Accept: options?.accept || "text/plain",
|
|
1725
|
-
...this.authHeaders(options)
|
|
1726
|
-
}
|
|
1727
|
-
});
|
|
1728
|
-
const contentType = response.headers.get("content-type") || "application/octet-stream";
|
|
1729
|
-
if (!response.body) {
|
|
1730
|
-
throw new Error("Response body is null - cannot create stream");
|
|
1731
|
-
}
|
|
1732
|
-
return { stream: response.body, contentType };
|
|
1733
|
-
}
|
|
1734
|
-
async browseResources(limit, archived, query, _options) {
|
|
1735
|
-
return busRequest(
|
|
1736
|
-
this.actor,
|
|
1737
|
-
"browse:resources-requested",
|
|
1738
|
-
{ search: query, archived, limit: limit ?? 100, offset: 0 },
|
|
1739
|
-
"browse:resources-result",
|
|
1740
|
-
"browse:resources-failed"
|
|
1741
|
-
);
|
|
1742
|
-
}
|
|
1743
|
-
async updateResource(id, data, options) {
|
|
1744
|
-
await this.http.patch(`${this.baseUrl}/resources/${id}`, {
|
|
1745
|
-
json: data,
|
|
1746
|
-
headers: this.authHeaders(options)
|
|
1747
|
-
}).text();
|
|
1748
|
-
}
|
|
1749
|
-
async getResourceEvents(id, _options) {
|
|
1750
|
-
return busRequest(this.actor, "browse:events-requested", { resourceId: id }, "browse:events-result", "browse:events-failed");
|
|
1751
|
-
}
|
|
1752
|
-
async browseReferences(id, _options) {
|
|
1753
|
-
return busRequest(this.actor, "browse:referenced-by-requested", { resourceId: id }, "browse:referenced-by-result", "browse:referenced-by-failed");
|
|
1754
|
-
}
|
|
1755
|
-
async generateCloneToken(id, _options) {
|
|
1756
|
-
return busRequest(this.actor, "yield:clone-token-requested", { resourceId: id }, "yield:clone-token-generated", "yield:clone-token-failed");
|
|
1757
|
-
}
|
|
1758
|
-
async getResourceByToken(token, _options) {
|
|
1759
|
-
return busRequest(this.actor, "yield:clone-resource-requested", { token }, "yield:clone-resource-result", "yield:clone-resource-failed");
|
|
1760
|
-
}
|
|
1761
|
-
async createResourceFromToken(data, _options) {
|
|
1762
|
-
return busRequest(this.actor, "yield:clone-create", data, "yield:clone-created", "yield:clone-create-failed");
|
|
1763
|
-
}
|
|
1764
|
-
// ============================================================================
|
|
1765
|
-
// ANNOTATIONS
|
|
1766
|
-
// ============================================================================
|
|
1767
|
-
async markAnnotation(id, data, _options) {
|
|
1768
|
-
return busRequest(
|
|
1769
|
-
this.actor,
|
|
1770
|
-
"mark:create-request",
|
|
1771
|
-
{ resourceId: id, request: data },
|
|
1772
|
-
"mark:create-ok",
|
|
1773
|
-
"mark:create-failed"
|
|
1774
|
-
);
|
|
1775
|
-
}
|
|
1776
|
-
async getAnnotation(id, _options) {
|
|
1777
|
-
return busRequest(this.actor, "browse:annotation-requested", { annotationId: id }, "browse:annotation-result", "browse:annotation-failed");
|
|
1778
|
-
}
|
|
1779
|
-
async browseAnnotation(resourceId, annotationId, _options) {
|
|
1780
|
-
return busRequest(this.actor, "browse:annotation-requested", { resourceId, annotationId }, "browse:annotation-result", "browse:annotation-failed");
|
|
1781
|
-
}
|
|
1782
|
-
async browseAnnotations(id, _motivation, _options) {
|
|
1783
|
-
return busRequest(this.actor, "browse:annotations-requested", { resourceId: id }, "browse:annotations-result", "browse:annotations-failed");
|
|
1784
|
-
}
|
|
1785
|
-
async deleteAnnotation(resourceId, annotationId, _options) {
|
|
1786
|
-
await this.actor.emit("mark:delete", { annotationId, resourceId });
|
|
1787
|
-
}
|
|
1788
|
-
async bindAnnotation(resourceId, annotationId, data, _options) {
|
|
1789
|
-
const correlationId = crypto.randomUUID();
|
|
1790
|
-
await this.actor.emit("bind:update-body", { correlationId, annotationId, resourceId, operations: data.operations });
|
|
1791
|
-
return { correlationId };
|
|
1792
|
-
}
|
|
1793
|
-
async getAnnotationHistory(resourceId, annotationId, _options) {
|
|
1794
|
-
return busRequest(this.actor, "browse:annotation-history-requested", { resourceId, annotationId }, "browse:annotation-history-result", "browse:annotation-history-failed");
|
|
1795
|
-
}
|
|
1796
|
-
async annotateReferences(resourceId, data, _options) {
|
|
1797
|
-
const { jobId } = await busRequest(
|
|
1798
|
-
this.actor,
|
|
1799
|
-
"job:create",
|
|
1800
|
-
{ jobType: "reference-annotation", resourceId, params: data },
|
|
1801
|
-
"job:created",
|
|
1802
|
-
"job:create-failed"
|
|
1803
|
-
);
|
|
1804
|
-
return { correlationId: crypto.randomUUID(), jobId };
|
|
1805
|
-
}
|
|
1806
|
-
async annotateHighlights(resourceId, data, _options) {
|
|
1807
|
-
const { jobId } = await busRequest(
|
|
1808
|
-
this.actor,
|
|
1809
|
-
"job:create",
|
|
1810
|
-
{ jobType: "highlight-annotation", resourceId, params: data },
|
|
1811
|
-
"job:created",
|
|
1812
|
-
"job:create-failed"
|
|
1813
|
-
);
|
|
1814
|
-
return { correlationId: crypto.randomUUID(), jobId };
|
|
1815
|
-
}
|
|
1816
|
-
async annotateAssessments(resourceId, data, _options) {
|
|
1817
|
-
const { jobId } = await busRequest(
|
|
1818
|
-
this.actor,
|
|
1819
|
-
"job:create",
|
|
1820
|
-
{ jobType: "assessment-annotation", resourceId, params: data },
|
|
1821
|
-
"job:created",
|
|
1822
|
-
"job:create-failed"
|
|
1823
|
-
);
|
|
1824
|
-
return { correlationId: crypto.randomUUID(), jobId };
|
|
1825
|
-
}
|
|
1826
|
-
async annotateComments(resourceId, data, _options) {
|
|
1827
|
-
const { jobId } = await busRequest(
|
|
1828
|
-
this.actor,
|
|
1829
|
-
"job:create",
|
|
1830
|
-
{ jobType: "comment-annotation", resourceId, params: data },
|
|
1831
|
-
"job:created",
|
|
1832
|
-
"job:create-failed"
|
|
1833
|
-
);
|
|
1834
|
-
return { correlationId: crypto.randomUUID(), jobId };
|
|
1835
|
-
}
|
|
1836
|
-
async annotateTags(resourceId, data, _options) {
|
|
1837
|
-
const { jobId } = await busRequest(
|
|
1838
|
-
this.actor,
|
|
1839
|
-
"job:create",
|
|
1840
|
-
{ jobType: "tag-annotation", resourceId, params: data },
|
|
1841
|
-
"job:created",
|
|
1842
|
-
"job:create-failed"
|
|
1843
|
-
);
|
|
1844
|
-
return { correlationId: crypto.randomUUID(), jobId };
|
|
1845
|
-
}
|
|
1846
|
-
async yieldResourceFromAnnotation(resourceId, annotationId, data, _options) {
|
|
1847
|
-
const { jobId } = await busRequest(
|
|
1848
|
-
this.actor,
|
|
1849
|
-
"job:create",
|
|
1850
|
-
{ jobType: "generation", resourceId, params: { referenceId: annotationId, ...data } },
|
|
1851
|
-
"job:created",
|
|
1852
|
-
"job:create-failed"
|
|
1853
|
-
);
|
|
1854
|
-
return { correlationId: crypto.randomUUID(), jobId };
|
|
1855
|
-
}
|
|
1856
|
-
async gatherAnnotationContext(resourceId, annotationId, data, _options) {
|
|
1857
|
-
await this.actor.emit("gather:requested", {
|
|
1858
|
-
correlationId: data.correlationId,
|
|
1859
|
-
annotationId,
|
|
1860
|
-
resourceId,
|
|
1861
|
-
contextWindow: data.contextWindow ?? 2e3
|
|
1862
|
-
});
|
|
1863
|
-
return { correlationId: data.correlationId };
|
|
1864
|
-
}
|
|
1865
|
-
async matchSearch(resourceId, data, _options) {
|
|
1866
|
-
await this.actor.emit("match:search-requested", {
|
|
1867
|
-
correlationId: data.correlationId,
|
|
1868
|
-
resourceId,
|
|
1869
|
-
referenceId: data.referenceId,
|
|
1870
|
-
context: data.context,
|
|
1871
|
-
limit: data.limit ?? 10,
|
|
1872
|
-
useSemanticScoring: data.useSemanticScoring ?? true
|
|
1873
|
-
});
|
|
1874
|
-
return { correlationId: data.correlationId };
|
|
1875
|
-
}
|
|
1876
|
-
// ============================================================================
|
|
1877
|
-
// ENTITY TYPES
|
|
1878
|
-
// ============================================================================
|
|
1879
|
-
async addEntityType(type, _options) {
|
|
1880
|
-
await this.actor.emit("mark:add-entity-type", { tag: type });
|
|
1881
|
-
}
|
|
1882
|
-
async addEntityTypesBulk(types, _options) {
|
|
1883
|
-
for (const tag of types) {
|
|
1884
|
-
await this.actor.emit("mark:add-entity-type", { tag });
|
|
1885
|
-
}
|
|
1886
|
-
}
|
|
1887
|
-
async listEntityTypes(_options) {
|
|
1888
|
-
return busRequest(this.actor, "browse:entity-types-requested", {}, "browse:entity-types-result", "browse:entity-types-failed");
|
|
1889
|
-
}
|
|
1890
|
-
// ============================================================================
|
|
1891
|
-
// PARTICIPANTS
|
|
1892
|
-
// ============================================================================
|
|
1893
|
-
async beckonAttention(_participantId, data, _options) {
|
|
1894
|
-
await this.actor.emit("beckon:focus", data);
|
|
1895
|
-
return {};
|
|
1896
|
-
}
|
|
1897
|
-
// ============================================================================
|
|
1898
|
-
// ADMIN
|
|
1899
|
-
// ============================================================================
|
|
1900
|
-
async listUsers(options) {
|
|
1901
|
-
return this.http.get(`${this.baseUrl}/api/admin/users`, {
|
|
1902
|
-
headers: this.authHeaders(options)
|
|
1903
|
-
}).json();
|
|
1904
|
-
}
|
|
1905
|
-
async getUserStats(options) {
|
|
1906
|
-
return this.http.get(`${this.baseUrl}/api/admin/users/stats`, {
|
|
1907
|
-
headers: this.authHeaders(options)
|
|
1908
|
-
}).json();
|
|
1909
|
-
}
|
|
1910
|
-
/**
|
|
1911
|
-
* Update a user by ID
|
|
1912
|
-
* Note: Users use DID identifiers (did:web:domain:users:id), not HTTP URIs.
|
|
1913
|
-
*/
|
|
1914
|
-
async updateUser(id, data, options) {
|
|
1915
|
-
return this.http.patch(`${this.baseUrl}/api/admin/users/${id}`, {
|
|
1916
|
-
json: data,
|
|
1917
|
-
headers: this.authHeaders(options)
|
|
1918
|
-
}).json();
|
|
1919
|
-
}
|
|
1920
|
-
async getOAuthConfig(options) {
|
|
1921
|
-
return this.http.get(`${this.baseUrl}/api/admin/oauth/config`, {
|
|
1922
|
-
headers: this.authHeaders(options)
|
|
1923
|
-
}).json();
|
|
1924
|
-
}
|
|
1925
|
-
// ============================================================================
|
|
1926
|
-
// ADMIN — EXCHANGE (Backup/Restore)
|
|
1927
|
-
// ============================================================================
|
|
1928
|
-
/**
|
|
1929
|
-
* Create a backup of the knowledge base. Returns raw Response for streaming download.
|
|
1930
|
-
* Caller should use response.blob() to trigger a file download.
|
|
1931
|
-
*/
|
|
1932
|
-
async backupKnowledgeBase(options) {
|
|
1933
|
-
return this.http.post(`${this.baseUrl}/api/admin/exchange/backup`, {
|
|
1934
|
-
headers: this.authHeaders(options)
|
|
1935
|
-
});
|
|
1936
|
-
}
|
|
1937
|
-
/**
|
|
1938
|
-
* Restore knowledge base from a backup file. Parses SSE progress events and calls onProgress.
|
|
1939
|
-
* Returns the final SSE event (phase: 'complete' or 'error').
|
|
1940
|
-
*/
|
|
1941
|
-
async restoreKnowledgeBase(file, options) {
|
|
1942
|
-
const formData = new FormData();
|
|
1943
|
-
formData.append("file", file);
|
|
1944
|
-
const response = await this.http.post(`${this.baseUrl}/api/admin/exchange/restore`, {
|
|
1945
|
-
body: formData,
|
|
1946
|
-
headers: this.authHeaders(options)
|
|
1947
|
-
});
|
|
1948
|
-
return this.parseSSEStream(response, options?.onProgress);
|
|
1949
|
-
}
|
|
1950
|
-
// ============================================================================
|
|
1951
|
-
// ADMIN — EXCHANGE (Linked Data Export/Import)
|
|
1952
|
-
// ============================================================================
|
|
1953
|
-
/**
|
|
1954
|
-
* Export the knowledge base as a JSON-LD Linked Data archive. Returns raw Response for streaming download.
|
|
1955
|
-
* Caller should use response.blob() to trigger a file download.
|
|
1956
|
-
*/
|
|
1957
|
-
async exportKnowledgeBase(params, options) {
|
|
1958
|
-
const searchParams = params?.includeArchived ? new URLSearchParams({ includeArchived: "true" }) : void 0;
|
|
1959
|
-
return this.http.post(`${this.baseUrl}/api/moderate/exchange/export`, {
|
|
1960
|
-
headers: this.authHeaders(options),
|
|
1961
|
-
...searchParams ? { searchParams } : {}
|
|
1962
|
-
});
|
|
1963
|
-
}
|
|
1964
|
-
/**
|
|
1965
|
-
* Import a JSON-LD Linked Data archive into the knowledge base. Parses SSE progress events and calls onProgress.
|
|
1966
|
-
* Returns the final SSE event (phase: 'complete' or 'error').
|
|
1967
|
-
*/
|
|
1968
|
-
async importKnowledgeBase(file, options) {
|
|
1969
|
-
const formData = new FormData();
|
|
1970
|
-
formData.append("file", file);
|
|
1971
|
-
const response = await this.http.post(`${this.baseUrl}/api/moderate/exchange/import`, {
|
|
1972
|
-
body: formData,
|
|
1973
|
-
headers: this.authHeaders(options)
|
|
1974
|
-
});
|
|
1975
|
-
return this.parseSSEStream(response, options?.onProgress);
|
|
1976
|
-
}
|
|
1977
|
-
async parseSSEStream(response, onProgress) {
|
|
1978
|
-
const reader = response.body.getReader();
|
|
1979
|
-
const decoder = new TextDecoder();
|
|
1980
|
-
let buffer = "";
|
|
1981
|
-
let finalResult = { phase: "unknown" };
|
|
1982
|
-
while (true) {
|
|
1983
|
-
const { done, value } = await reader.read();
|
|
1984
|
-
if (done) break;
|
|
1985
|
-
buffer += decoder.decode(value, { stream: true });
|
|
1986
|
-
const lines = buffer.split("\n");
|
|
1987
|
-
buffer = lines.pop();
|
|
1988
|
-
for (const line of lines) {
|
|
1989
|
-
if (line.startsWith("data: ")) {
|
|
1990
|
-
const event = JSON.parse(line.slice(6));
|
|
1991
|
-
onProgress?.(event);
|
|
1992
|
-
finalResult = event;
|
|
1993
|
-
}
|
|
1994
|
-
}
|
|
1995
|
-
}
|
|
1996
|
-
return finalResult;
|
|
1997
|
-
}
|
|
1998
|
-
// ============================================================================
|
|
1999
|
-
// JOB STATUS
|
|
2000
|
-
// ============================================================================
|
|
2001
|
-
async getJobStatus(id, _options) {
|
|
2002
|
-
return busRequest(this.actor, "job:status-requested", { jobId: id }, "job:status-result", "job:status-failed");
|
|
2003
|
-
}
|
|
2004
|
-
/**
|
|
2005
|
-
* Poll a job until it completes or fails
|
|
2006
|
-
* @param id - The job ID to poll
|
|
2007
|
-
* @param options - Polling options
|
|
2008
|
-
* @returns The final job status
|
|
2009
|
-
*/
|
|
2010
|
-
async pollJobUntilComplete(id, options) {
|
|
2011
|
-
const interval = options?.interval ?? 1e3;
|
|
2012
|
-
const timeout7 = options?.timeout ?? 6e4;
|
|
2013
|
-
const startTime = Date.now();
|
|
2014
|
-
while (true) {
|
|
2015
|
-
const status = await this.getJobStatus(id, { auth: options?.auth });
|
|
2016
|
-
if (options?.onProgress) {
|
|
2017
|
-
options.onProgress(status);
|
|
2018
|
-
}
|
|
2019
|
-
if (status.status === "complete" || status.status === "failed" || status.status === "cancelled") {
|
|
2020
|
-
return status;
|
|
2021
|
-
}
|
|
2022
|
-
if (Date.now() - startTime > timeout7) {
|
|
2023
|
-
throw new Error(`Job polling timeout after ${timeout7}ms`);
|
|
2024
|
-
}
|
|
2025
|
-
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
2026
|
-
}
|
|
2027
|
-
}
|
|
2028
|
-
// ============================================================================
|
|
2029
|
-
// SYSTEM STATUS
|
|
2030
|
-
// ============================================================================
|
|
2031
|
-
async healthCheck(options) {
|
|
2032
|
-
return this.http.get(`${this.baseUrl}/api/health`, {
|
|
2033
|
-
headers: this.authHeaders(options)
|
|
2034
|
-
}).json();
|
|
2035
|
-
}
|
|
2036
|
-
async getStatus(options) {
|
|
2037
|
-
return this.http.get(`${this.baseUrl}/api/status`, {
|
|
2038
|
-
headers: this.authHeaders(options)
|
|
2039
|
-
}).json();
|
|
2040
|
-
}
|
|
2041
|
-
async browseFiles(dirPath, sort, _options) {
|
|
2042
|
-
return busRequest(
|
|
2043
|
-
this.actor,
|
|
2044
|
-
"browse:directory-requested",
|
|
2045
|
-
{ path: dirPath ?? ".", sort: sort ?? "name" },
|
|
2046
|
-
"browse:directory-result",
|
|
2047
|
-
"browse:directory-failed"
|
|
2048
|
-
);
|
|
2049
|
-
}
|
|
2050
|
-
};
|
|
2051
|
-
|
|
2052
|
-
// src/session/storage.ts
|
|
2053
|
-
var SESSION_PREFIX = "semiont.session.";
|
|
2054
|
-
var STORAGE_KEY = "semiont.knowledgeBases";
|
|
2055
|
-
var ACTIVE_KEY = "semiont.activeKnowledgeBaseId";
|
|
2056
|
-
var REFRESH_BEFORE_EXP_MS = 5 * 60 * 1e3;
|
|
2057
|
-
function sessionKey(kbId) {
|
|
2058
|
-
return `${SESSION_PREFIX}${kbId}`;
|
|
2059
|
-
}
|
|
2060
|
-
function getStoredSession(storage, kbId) {
|
|
2061
|
-
const raw = storage.get(sessionKey(kbId));
|
|
2062
|
-
if (!raw) return null;
|
|
2063
|
-
try {
|
|
2064
|
-
const parsed = JSON.parse(raw);
|
|
2065
|
-
if (parsed && typeof parsed.access === "string" && typeof parsed.refresh === "string") {
|
|
2066
|
-
return { access: parsed.access, refresh: parsed.refresh };
|
|
2067
|
-
}
|
|
2068
|
-
} catch {
|
|
2069
|
-
}
|
|
2070
|
-
return null;
|
|
2071
|
-
}
|
|
2072
|
-
function setStoredSession(storage, kbId, session) {
|
|
2073
|
-
storage.set(sessionKey(kbId), JSON.stringify(session));
|
|
2074
|
-
}
|
|
2075
|
-
function clearStoredSession(storage, kbId) {
|
|
2076
|
-
storage.delete(sessionKey(kbId));
|
|
2077
|
-
}
|
|
2078
|
-
function parseJwtExpiry(token) {
|
|
2079
|
-
try {
|
|
2080
|
-
const parts = token.split(".");
|
|
2081
|
-
if (parts.length !== 3 || !parts[1]) return null;
|
|
2082
|
-
const payload = JSON.parse(atob(parts[1]));
|
|
2083
|
-
if (!payload.exp) return null;
|
|
2084
|
-
return new Date(payload.exp * 1e3);
|
|
2085
|
-
} catch {
|
|
2086
|
-
return null;
|
|
2087
|
-
}
|
|
2088
|
-
}
|
|
2089
|
-
function isJwtExpired(token) {
|
|
2090
|
-
const expiry = parseJwtExpiry(token);
|
|
2091
|
-
if (!expiry) return true;
|
|
2092
|
-
return expiry.getTime() < Date.now();
|
|
2093
|
-
}
|
|
2094
|
-
function migrateLegacyEntry(entry) {
|
|
2095
|
-
if (entry.host !== void 0) return entry;
|
|
2096
|
-
try {
|
|
2097
|
-
const url = new URL(entry.backendUrl);
|
|
2098
|
-
return {
|
|
2099
|
-
id: entry.id,
|
|
2100
|
-
label: entry.label,
|
|
2101
|
-
host: url.hostname,
|
|
2102
|
-
port: parseInt(url.port, 10) || (url.protocol === "https:" ? 443 : 80),
|
|
2103
|
-
protocol: url.protocol === "https:" ? "https" : "http",
|
|
2104
|
-
email: ""
|
|
2105
|
-
};
|
|
2106
|
-
} catch {
|
|
2107
|
-
return {
|
|
2108
|
-
id: entry.id,
|
|
2109
|
-
label: entry.label || "Unknown",
|
|
2110
|
-
host: "localhost",
|
|
2111
|
-
port: 4e3,
|
|
2112
|
-
protocol: "http",
|
|
2113
|
-
email: ""
|
|
2114
|
-
};
|
|
2115
|
-
}
|
|
2116
|
-
}
|
|
2117
|
-
function loadKnowledgeBases(storage) {
|
|
2118
|
-
try {
|
|
2119
|
-
const raw = storage.get(STORAGE_KEY);
|
|
2120
|
-
if (!raw) return [];
|
|
2121
|
-
const entries = JSON.parse(raw);
|
|
2122
|
-
return entries.map(migrateLegacyEntry);
|
|
2123
|
-
} catch {
|
|
2124
|
-
return [];
|
|
2125
|
-
}
|
|
2126
|
-
}
|
|
2127
|
-
function saveKnowledgeBases(storage, knowledgeBases) {
|
|
2128
|
-
storage.set(STORAGE_KEY, JSON.stringify(knowledgeBases));
|
|
2129
|
-
}
|
|
2130
|
-
function defaultProtocol(host) {
|
|
2131
|
-
return host === "localhost" || host === "127.0.0.1" ? "http" : "https";
|
|
2132
|
-
}
|
|
2133
|
-
var HOSTNAME_RE = /^(([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?|localhost|\d{1,3}(\.\d{1,3}){3})$/;
|
|
2134
|
-
function isValidHostname(host) {
|
|
2135
|
-
return HOSTNAME_RE.test(host);
|
|
2136
|
-
}
|
|
2137
|
-
function kbBackendUrl(kb) {
|
|
2138
|
-
if (!isValidHostname(kb.host)) {
|
|
2139
|
-
throw new Error(`Invalid KB hostname: "${kb.host}"`);
|
|
2140
|
-
}
|
|
2141
|
-
const url = new URL("http://x");
|
|
2142
|
-
url.protocol = kb.protocol + ":";
|
|
2143
|
-
url.hostname = kb.host;
|
|
2144
|
-
url.port = String(kb.port);
|
|
2145
|
-
return `${kb.protocol}://${url.hostname}:${kb.port}`;
|
|
2146
|
-
}
|
|
2147
|
-
function generateKbId() {
|
|
2148
|
-
return crypto.randomUUID();
|
|
2149
|
-
}
|
|
2150
|
-
|
|
2151
|
-
// src/session/errors.ts
|
|
2152
|
-
var SemiontError = class extends Error {
|
|
2153
|
-
code;
|
|
2154
|
-
kbId;
|
|
2155
|
-
constructor(code, message, kbId = null) {
|
|
2156
|
-
super(message);
|
|
2157
|
-
this.name = "SemiontError";
|
|
2158
|
-
this.code = code;
|
|
2159
|
-
this.kbId = kbId;
|
|
2160
|
-
}
|
|
2161
|
-
};
|
|
2162
|
-
|
|
2163
|
-
// src/session/semiont-session.ts
|
|
2164
|
-
var SemiontSession = class {
|
|
2165
|
-
kb;
|
|
2166
|
-
client;
|
|
2167
|
-
token$;
|
|
2168
|
-
user$;
|
|
2169
|
-
streamState$;
|
|
2170
|
-
/** Resolves after the initial validation round-trip completes (success or failure). */
|
|
2171
|
-
ready;
|
|
2172
|
-
storage;
|
|
2173
|
-
doRefresh;
|
|
2174
|
-
doValidate;
|
|
2175
|
-
onAuthFailed;
|
|
2176
|
-
onError;
|
|
2177
|
-
refreshTimer = null;
|
|
2178
|
-
unsubscribeStorage = null;
|
|
2179
|
-
disposed = false;
|
|
2180
|
-
constructor(config) {
|
|
2181
|
-
this.kb = config.kb;
|
|
2182
|
-
this.storage = config.storage;
|
|
2183
|
-
this.doRefresh = config.refresh;
|
|
2184
|
-
this.doValidate = config.validate;
|
|
2185
|
-
this.onAuthFailed = config.onAuthFailed ?? (() => {
|
|
2186
|
-
});
|
|
2187
|
-
this.onError = config.onError ?? (() => {
|
|
2188
|
-
});
|
|
2189
|
-
const stored = getStoredSession(this.storage, this.kb.id);
|
|
2190
|
-
const initialToken = stored && !isJwtExpired(stored.access) ? accessToken(stored.access) : null;
|
|
2191
|
-
this.token$ = new BehaviorSubject(initialToken);
|
|
2192
|
-
this.user$ = new BehaviorSubject(null);
|
|
2193
|
-
this.client = new SemiontApiClient({
|
|
2194
|
-
baseUrl: baseUrl(kbBackendUrl(this.kb)),
|
|
2195
|
-
token$: this.token$,
|
|
2196
|
-
tokenRefresher: () => this.refresh().then((t) => t ?? null)
|
|
2197
|
-
});
|
|
2198
|
-
this.streamState$ = this.client.actor.state$;
|
|
2199
|
-
if (initialToken) {
|
|
2200
|
-
this.scheduleProactiveRefresh(initialToken);
|
|
2201
|
-
}
|
|
2202
|
-
this.unsubscribeStorage = this.storage.subscribe?.((key, newValue) => {
|
|
2203
|
-
this.handleStorageChange(key, newValue);
|
|
2204
|
-
}) ?? null;
|
|
2205
|
-
this.ready = this.validate(stored);
|
|
2206
|
-
}
|
|
2207
|
-
/**
|
|
2208
|
-
* Run the initial mount-time validation. If a stored access token is
|
|
2209
|
-
* present and unexpired, call the configured `validate` with it to
|
|
2210
|
-
* confirm it still works and populate `user$`. If expired, try
|
|
2211
|
-
* refresh first. On 401 from validate, try refresh once. Surfaces
|
|
2212
|
-
* auth-failed on terminal failure.
|
|
2213
|
-
*
|
|
2214
|
-
* When no `validate` callback is provided (service principals), this
|
|
2215
|
-
* still runs through the refresh-if-expired step so the stored
|
|
2216
|
-
* token is current — it just skips the user-validation round trip.
|
|
2217
|
-
*/
|
|
2218
|
-
async validate(stored) {
|
|
2219
|
-
if (!stored) return;
|
|
2220
|
-
const startToken = isJwtExpired(stored.access) ? await this.doRefresh() : stored.access;
|
|
2221
|
-
if (!startToken) {
|
|
2222
|
-
if (isJwtExpired(stored.access)) {
|
|
2223
|
-
clearStoredSession(this.storage, this.kb.id);
|
|
2224
|
-
}
|
|
2225
|
-
return;
|
|
2226
|
-
}
|
|
2227
|
-
if (startToken !== stored.access) {
|
|
2228
|
-
this.token$.next(accessToken(startToken));
|
|
2229
|
-
this.scheduleProactiveRefresh(startToken);
|
|
2230
|
-
}
|
|
2231
|
-
if (!this.doValidate) return;
|
|
2232
|
-
const attempt = async (token) => {
|
|
2233
|
-
if (this.disposed) return;
|
|
2234
|
-
try {
|
|
2235
|
-
const data = await this.doValidate(accessToken(token));
|
|
2236
|
-
if (this.disposed) return;
|
|
2237
|
-
this.user$.next(data);
|
|
2238
|
-
} catch (err) {
|
|
2239
|
-
if (this.disposed) return;
|
|
2240
|
-
if (err instanceof APIError && err.status === 401) {
|
|
2241
|
-
const refreshed = await this.doRefresh();
|
|
2242
|
-
if (this.disposed) return;
|
|
2243
|
-
if (refreshed) {
|
|
2244
|
-
this.token$.next(accessToken(refreshed));
|
|
2245
|
-
this.scheduleProactiveRefresh(refreshed);
|
|
2246
|
-
await attempt(refreshed);
|
|
2247
|
-
return;
|
|
2248
|
-
}
|
|
2249
|
-
clearStoredSession(this.storage, this.kb.id);
|
|
2250
|
-
this.token$.next(null);
|
|
2251
|
-
this.onAuthFailed("Your session has expired. Please sign in again.");
|
|
2252
|
-
} else {
|
|
2253
|
-
this.onError(
|
|
2254
|
-
new SemiontError(
|
|
2255
|
-
"session.auth-failed",
|
|
2256
|
-
err instanceof Error ? err.message : String(err),
|
|
2257
|
-
this.kb.id
|
|
2258
|
-
)
|
|
2259
|
-
);
|
|
2260
|
-
}
|
|
2261
|
-
}
|
|
2262
|
-
};
|
|
2263
|
-
await attempt(startToken);
|
|
2264
|
-
}
|
|
2265
|
-
/**
|
|
2266
|
-
* Refresh the access token via the configured `refresh` callback.
|
|
2267
|
-
* On success, pushes the new token into `token$` and schedules the
|
|
2268
|
-
* next proactive refresh. On failure, clears persisted state and
|
|
2269
|
-
* fires `onAuthFailed` — the frontend's wiring of that callback is
|
|
2270
|
-
* what surfaces the session-expired modal.
|
|
2271
|
-
*/
|
|
2272
|
-
async refresh() {
|
|
2273
|
-
if (this.disposed) return null;
|
|
2274
|
-
const newAccess = await this.doRefresh();
|
|
2275
|
-
if (this.disposed) return null;
|
|
2276
|
-
if (newAccess) {
|
|
2277
|
-
const tok = accessToken(newAccess);
|
|
2278
|
-
this.token$.next(tok);
|
|
2279
|
-
this.scheduleProactiveRefresh(newAccess);
|
|
2280
|
-
return tok;
|
|
2281
|
-
}
|
|
2282
|
-
this.token$.next(null);
|
|
2283
|
-
clearStoredSession(this.storage, this.kb.id);
|
|
2284
|
-
this.onAuthFailed("Your session has expired. Please sign in again.");
|
|
2285
|
-
this.onError(
|
|
2286
|
-
new SemiontError("session.refresh-exhausted", "Token refresh failed", this.kb.id)
|
|
2287
|
-
);
|
|
2288
|
-
return null;
|
|
2289
|
-
}
|
|
2290
|
-
scheduleProactiveRefresh(token) {
|
|
2291
|
-
this.clearRefreshTimer();
|
|
2292
|
-
const expiresAt = parseJwtExpiry(token);
|
|
2293
|
-
if (!expiresAt) return;
|
|
2294
|
-
const refreshAt = expiresAt.getTime() - REFRESH_BEFORE_EXP_MS;
|
|
2295
|
-
const delay = Math.max(0, refreshAt - Date.now());
|
|
2296
|
-
this.refreshTimer = setTimeout(() => {
|
|
2297
|
-
this.refreshTimer = null;
|
|
2298
|
-
if (!this.disposed) void this.refresh();
|
|
2299
|
-
}, delay);
|
|
2300
|
-
}
|
|
2301
|
-
clearRefreshTimer() {
|
|
2302
|
-
if (this.refreshTimer) {
|
|
2303
|
-
clearTimeout(this.refreshTimer);
|
|
2304
|
-
this.refreshTimer = null;
|
|
2305
|
-
}
|
|
2306
|
-
}
|
|
2307
|
-
/**
|
|
2308
|
-
* Cross-context sync: another tab/process refreshed or signed out this
|
|
2309
|
-
* KB. Mirror the change into our in-memory state.
|
|
2310
|
-
*/
|
|
2311
|
-
handleStorageChange(key, newValue) {
|
|
2312
|
-
if (this.disposed) return;
|
|
2313
|
-
if (key !== sessionKey(this.kb.id)) return;
|
|
2314
|
-
if (!newValue) {
|
|
2315
|
-
this.token$.next(null);
|
|
2316
|
-
this.user$.next(null);
|
|
2317
|
-
this.clearRefreshTimer();
|
|
2318
|
-
return;
|
|
2319
|
-
}
|
|
2320
|
-
try {
|
|
2321
|
-
const parsed = JSON.parse(newValue);
|
|
2322
|
-
if (typeof parsed.access === "string") {
|
|
2323
|
-
this.token$.next(accessToken(parsed.access));
|
|
2324
|
-
this.scheduleProactiveRefresh(parsed.access);
|
|
2325
|
-
}
|
|
2326
|
-
} catch {
|
|
2327
|
-
}
|
|
2328
|
-
}
|
|
2329
|
-
get expiresAt() {
|
|
2330
|
-
const token = this.token$.getValue();
|
|
2331
|
-
return token ? parseJwtExpiry(token) : null;
|
|
2332
|
-
}
|
|
2333
|
-
async dispose() {
|
|
2334
|
-
if (this.disposed) return;
|
|
2335
|
-
this.disposed = true;
|
|
2336
|
-
this.clearRefreshTimer();
|
|
2337
|
-
if (this.unsubscribeStorage) {
|
|
2338
|
-
this.unsubscribeStorage();
|
|
2339
|
-
this.unsubscribeStorage = null;
|
|
2340
|
-
}
|
|
2341
|
-
this.client.dispose();
|
|
2342
|
-
this.token$.complete();
|
|
2343
|
-
this.user$.complete();
|
|
2344
|
-
}
|
|
2345
|
-
};
|
|
2346
|
-
|
|
2347
|
-
// src/session/notify.ts
|
|
2348
|
-
var activeOnSessionExpired = null;
|
|
2349
|
-
var activeOnPermissionDenied = null;
|
|
2350
|
-
function notifySessionExpired(message) {
|
|
2351
|
-
activeOnSessionExpired?.(message);
|
|
2352
|
-
}
|
|
2353
|
-
function notifyPermissionDenied(message) {
|
|
2354
|
-
activeOnPermissionDenied?.(message);
|
|
2355
|
-
}
|
|
2356
|
-
function registerAuthNotifyHandlers(handlers) {
|
|
2357
|
-
activeOnSessionExpired = handlers.onSessionExpired;
|
|
2358
|
-
activeOnPermissionDenied = handlers.onPermissionDenied;
|
|
2359
|
-
return () => {
|
|
2360
|
-
activeOnSessionExpired = null;
|
|
2361
|
-
activeOnPermissionDenied = null;
|
|
2362
|
-
};
|
|
2363
|
-
}
|
|
2364
|
-
var FrontendSessionSignals = class {
|
|
2365
|
-
sessionExpiredAt$;
|
|
2366
|
-
sessionExpiredMessage$;
|
|
2367
|
-
permissionDeniedAt$;
|
|
2368
|
-
permissionDeniedMessage$;
|
|
2369
|
-
constructor() {
|
|
2370
|
-
this.sessionExpiredAt$ = new BehaviorSubject(null);
|
|
2371
|
-
this.sessionExpiredMessage$ = new BehaviorSubject(null);
|
|
2372
|
-
this.permissionDeniedAt$ = new BehaviorSubject(null);
|
|
2373
|
-
this.permissionDeniedMessage$ = new BehaviorSubject(null);
|
|
2374
|
-
}
|
|
2375
|
-
notifySessionExpired(message) {
|
|
2376
|
-
this.sessionExpiredMessage$.next(
|
|
2377
|
-
message ?? "Your session has expired. Please sign in again."
|
|
2378
|
-
);
|
|
2379
|
-
this.sessionExpiredAt$.next(Date.now());
|
|
2380
|
-
}
|
|
2381
|
-
notifyPermissionDenied(message) {
|
|
2382
|
-
this.permissionDeniedMessage$.next(
|
|
2383
|
-
message ?? "You do not have permission to perform this action."
|
|
2384
|
-
);
|
|
2385
|
-
this.permissionDeniedAt$.next(Date.now());
|
|
2386
|
-
}
|
|
2387
|
-
acknowledgeSessionExpired() {
|
|
2388
|
-
this.sessionExpiredAt$.next(null);
|
|
2389
|
-
this.sessionExpiredMessage$.next(null);
|
|
2390
|
-
}
|
|
2391
|
-
acknowledgePermissionDenied() {
|
|
2392
|
-
this.permissionDeniedAt$.next(null);
|
|
2393
|
-
this.permissionDeniedMessage$.next(null);
|
|
2394
|
-
}
|
|
2395
|
-
dispose() {
|
|
2396
|
-
this.sessionExpiredAt$.complete();
|
|
2397
|
-
this.sessionExpiredMessage$.complete();
|
|
2398
|
-
this.permissionDeniedAt$.complete();
|
|
2399
|
-
this.permissionDeniedMessage$.complete();
|
|
2400
|
-
}
|
|
2401
|
-
};
|
|
2402
|
-
|
|
2403
|
-
// src/session/semiont-browser.ts
|
|
2404
|
-
var OPEN_RESOURCES_KEY = "openDocuments";
|
|
2405
|
-
function sortOpenResources(resources) {
|
|
2406
|
-
return [...resources].sort((a, b) => {
|
|
2407
|
-
if (a.order !== void 0 && b.order !== void 0) return a.order - b.order;
|
|
2408
|
-
return a.openedAt - b.openedAt;
|
|
2409
|
-
});
|
|
2410
|
-
}
|
|
2411
|
-
function loadOpenResources(storage) {
|
|
2412
|
-
try {
|
|
2413
|
-
const stored = storage.get(OPEN_RESOURCES_KEY);
|
|
2414
|
-
if (stored) return sortOpenResources(JSON.parse(stored));
|
|
2415
|
-
} catch {
|
|
2416
|
-
}
|
|
2417
|
-
return [];
|
|
2418
|
-
}
|
|
2419
|
-
var SemiontBrowser = class {
|
|
2420
|
-
kbs$;
|
|
2421
|
-
activeKbId$;
|
|
2422
|
-
activeSession$;
|
|
2423
|
-
/**
|
|
2424
|
-
* Modal signals (session-expired / permission-denied) for the
|
|
2425
|
-
* currently-active session. Parallels `activeSession$` — always
|
|
2426
|
-
* non-null when `activeSession$` is non-null, always null when it
|
|
2427
|
-
* is. Extracted from the session itself so headless sessions
|
|
2428
|
-
* (workers, CLIs, tests) don't carry dead modal observables.
|
|
2429
|
-
* See [FrontendSessionSignals](./frontend-session-signals.ts).
|
|
2430
|
-
*/
|
|
2431
|
-
activeSignals$;
|
|
2432
|
-
/**
|
|
2433
|
-
* True while a session is actively being constructed (setActiveKb /
|
|
2434
|
-
* signIn in flight, awaiting `session.ready`). Distinguishes the
|
|
2435
|
-
* "session about to arrive" intermediate state from "session
|
|
2436
|
-
* intentionally null" (after signOut, or when the active KB has no
|
|
2437
|
-
* stored credentials). UIs that want a loading spinner should gate
|
|
2438
|
-
* on this; otherwise they get stuck spinning after every signOut.
|
|
2439
|
-
*/
|
|
2440
|
-
sessionActivating$;
|
|
2441
|
-
openResources$;
|
|
2442
|
-
error$;
|
|
2443
|
-
identityToken$;
|
|
2444
|
-
storage;
|
|
2445
|
-
/**
|
|
2446
|
-
* App-scoped EventBus. Hosts UI-shell events that must work regardless
|
|
2447
|
-
* of whether a KB session is active: panel toggles, sidebar state,
|
|
2448
|
-
* tab reorders, routing, settings, etc. Disjoint from the per-session
|
|
2449
|
-
* bus inside `SemiontApiClient`, which carries KB-content events
|
|
2450
|
-
* (mark:*, beckon:*, gather:*, match:*, bind:*, yield:*, browse:click).
|
|
2451
|
-
*/
|
|
2452
|
-
eventBus = new EventBus();
|
|
2453
|
-
unregisterNotify = null;
|
|
2454
|
-
unsubscribeStorage = null;
|
|
2455
|
-
disposed = false;
|
|
2456
|
-
activating = null;
|
|
2457
|
-
/**
|
|
2458
|
-
* Per-KB in-flight refresh dedup. Simultaneous 401s for the same
|
|
2459
|
-
* KB converge on a single `/api/tokens/refresh` network call.
|
|
2460
|
-
* Was previously module-scoped in `refresh.ts`; moved here when
|
|
2461
|
-
* that file was deleted — SemiontBrowser is a singleton so the
|
|
2462
|
-
* scoping is equivalent.
|
|
2463
|
-
*/
|
|
2464
|
-
inFlightRefreshes = /* @__PURE__ */ new Map();
|
|
2465
|
-
constructor(config) {
|
|
2466
|
-
this.storage = config.storage;
|
|
2467
|
-
const kbs = loadKnowledgeBases(this.storage);
|
|
2468
|
-
const storedActive = this.storage.get(ACTIVE_KEY);
|
|
2469
|
-
const initialActive = storedActive && kbs.some((kb) => kb.id === storedActive) ? storedActive : kbs[0]?.id ?? null;
|
|
2470
|
-
this.kbs$ = new BehaviorSubject(kbs);
|
|
2471
|
-
this.activeKbId$ = new BehaviorSubject(initialActive);
|
|
2472
|
-
this.activeSession$ = new BehaviorSubject(null);
|
|
2473
|
-
this.activeSignals$ = new BehaviorSubject(null);
|
|
2474
|
-
this.sessionActivating$ = new BehaviorSubject(false);
|
|
2475
|
-
this.openResources$ = new BehaviorSubject(loadOpenResources(this.storage));
|
|
2476
|
-
this.error$ = new Subject();
|
|
2477
|
-
this.identityToken$ = new BehaviorSubject(null);
|
|
2478
|
-
this.kbs$.subscribe((next) => saveKnowledgeBases(this.storage, next));
|
|
2479
|
-
this.activeKbId$.subscribe((id) => {
|
|
2480
|
-
if (id) this.storage.set(ACTIVE_KEY, id);
|
|
2481
|
-
else this.storage.delete(ACTIVE_KEY);
|
|
2482
|
-
});
|
|
2483
|
-
this.openResources$.subscribe((list) => {
|
|
2484
|
-
this.storage.set(OPEN_RESOURCES_KEY, JSON.stringify(list));
|
|
2485
|
-
});
|
|
2486
|
-
this.unsubscribeStorage = this.storage.subscribe?.((key, newValue) => {
|
|
2487
|
-
if (key !== OPEN_RESOURCES_KEY || !newValue) return;
|
|
2488
|
-
try {
|
|
2489
|
-
this.openResources$.next(sortOpenResources(JSON.parse(newValue)));
|
|
2490
|
-
} catch {
|
|
2491
|
-
}
|
|
2492
|
-
}) ?? null;
|
|
2493
|
-
this.unregisterNotify = registerAuthNotifyHandlers({
|
|
2494
|
-
onSessionExpired: (message) => {
|
|
2495
|
-
this.activeSignals$.getValue()?.notifySessionExpired(message ?? null);
|
|
2496
|
-
},
|
|
2497
|
-
onPermissionDenied: (message) => {
|
|
2498
|
-
this.activeSignals$.getValue()?.notifyPermissionDenied(message ?? null);
|
|
2499
|
-
}
|
|
2500
|
-
});
|
|
2501
|
-
if (initialActive) {
|
|
2502
|
-
void this.setActiveKb(initialActive);
|
|
2503
|
-
}
|
|
2504
|
-
}
|
|
2505
|
-
// ── App-scoped event bus ──────────────────────────────────────────────
|
|
2506
|
-
/** Emit an event on the browser's app-scoped bus. */
|
|
2507
|
-
emit(channel, payload) {
|
|
2508
|
-
if (this.disposed) return;
|
|
2509
|
-
this.eventBus.get(channel).next(payload);
|
|
2510
|
-
}
|
|
2511
|
-
/** Subscribe to an event; returns unsubscribe. */
|
|
2512
|
-
on(channel, handler) {
|
|
2513
|
-
const sub = this.eventBus.get(channel).subscribe(handler);
|
|
2514
|
-
return () => sub.unsubscribe();
|
|
2515
|
-
}
|
|
2516
|
-
/** Read-only observable for an app-scoped channel. */
|
|
2517
|
-
stream(channel) {
|
|
2518
|
-
return this.eventBus.get(channel).asObservable();
|
|
2519
|
-
}
|
|
2520
|
-
// ── Identity token (NextAuth bridge; D1) ──────────────────────────────
|
|
2521
|
-
/**
|
|
2522
|
-
* Set the app-level identity token (from NextAuth's useSession).
|
|
2523
|
-
* Called at the root layout via a single `useEffect`. No other site
|
|
2524
|
-
* in the codebase should call this.
|
|
2525
|
-
*/
|
|
2526
|
-
setIdentityToken(token) {
|
|
2527
|
-
if (this.disposed) return;
|
|
2528
|
-
this.identityToken$.next(token);
|
|
2529
|
-
}
|
|
2530
|
-
// ── KB list management ────────────────────────────────────────────────
|
|
2531
|
-
addKb(input, access, refresh) {
|
|
2532
|
-
const kb = { id: generateKbId(), ...input };
|
|
2533
|
-
setStoredSession(this.storage, kb.id, { access, refresh });
|
|
2534
|
-
this.kbs$.next([...this.kbs$.getValue(), kb]);
|
|
2535
|
-
void this.setActiveKb(kb.id);
|
|
2536
|
-
return kb;
|
|
2537
|
-
}
|
|
2538
|
-
removeKb(id) {
|
|
2539
|
-
clearStoredSession(this.storage, id);
|
|
2540
|
-
const next = this.kbs$.getValue().filter((kb) => kb.id !== id);
|
|
2541
|
-
this.kbs$.next(next);
|
|
2542
|
-
if (this.activeKbId$.getValue() === id) {
|
|
2543
|
-
void this.setActiveKb(next[0]?.id ?? null);
|
|
2544
|
-
}
|
|
2545
|
-
}
|
|
2546
|
-
updateKb(id, updates) {
|
|
2547
|
-
this.kbs$.next(
|
|
2548
|
-
this.kbs$.getValue().map((kb) => kb.id === id ? { ...kb, ...updates } : kb)
|
|
2549
|
-
);
|
|
2550
|
-
}
|
|
2551
|
-
/**
|
|
2552
|
-
* Read the locally-stored credential status for a KB. Pure / synchronous —
|
|
2553
|
-
* does not subscribe to context changes. Used by KB-list UI to color status
|
|
2554
|
-
* dots without requiring re-renders on every tick.
|
|
2555
|
-
*/
|
|
2556
|
-
getKbSessionStatus(kbId) {
|
|
2557
|
-
const stored = getStoredSession(this.storage, kbId);
|
|
2558
|
-
if (!stored) return "signed-out";
|
|
2559
|
-
return isJwtExpired(stored.access) ? "expired" : "authenticated";
|
|
2560
|
-
}
|
|
2561
|
-
/**
|
|
2562
|
-
* Switch the active KB. Follows the D2 disposal contract:
|
|
2563
|
-
* 1. Synchronously announce the new id on `activeKbId$` and null out
|
|
2564
|
-
* `activeSession$` so views see a safe empty state first.
|
|
2565
|
-
* 2. Serialize overlapping calls — if an activation is in flight, wait
|
|
2566
|
-
* for it before proceeding.
|
|
2567
|
-
* 3. Dispose whatever session is currently live.
|
|
2568
|
-
* 4. Construct the next session and await `session.ready`.
|
|
2569
|
-
* 5. Before emitting, re-check `activeKbId$` — if a newer call superseded
|
|
2570
|
-
* us while we waited, dispose our session and skip the emit.
|
|
2571
|
-
* 6. Emit the new session.
|
|
2572
|
-
*/
|
|
2573
|
-
async setActiveKb(id) {
|
|
2574
|
-
if (this.disposed) return;
|
|
2575
|
-
const prevId = this.activeKbId$.getValue();
|
|
2576
|
-
const prevSession = this.activeSession$.getValue();
|
|
2577
|
-
if (id === prevId && prevSession) return;
|
|
2578
|
-
if (prevId !== id) this.activeKbId$.next(id);
|
|
2579
|
-
if (prevSession) {
|
|
2580
|
-
this.activeSession$.next(null);
|
|
2581
|
-
this.activeSignals$.next(null);
|
|
2582
|
-
}
|
|
2583
|
-
while (this.activating) {
|
|
2584
|
-
const current = this.activating;
|
|
2585
|
-
await current;
|
|
2586
|
-
if (this.disposed) return;
|
|
2587
|
-
if (this.activeKbId$.getValue() !== id) return;
|
|
2588
|
-
}
|
|
2589
|
-
const activation = (async () => {
|
|
2590
|
-
const toDispose = this.activeSession$.getValue();
|
|
2591
|
-
const signalsToDispose = this.activeSignals$.getValue();
|
|
2592
|
-
if (toDispose) {
|
|
2593
|
-
this.activeSession$.next(null);
|
|
2594
|
-
this.activeSignals$.next(null);
|
|
2595
|
-
await toDispose.dispose();
|
|
2596
|
-
signalsToDispose?.dispose();
|
|
2597
|
-
}
|
|
2598
|
-
if (!id) return;
|
|
2599
|
-
const kb = this.kbs$.getValue().find((k) => k.id === id);
|
|
2600
|
-
if (!kb) return;
|
|
2601
|
-
const signals = new FrontendSessionSignals();
|
|
2602
|
-
const session = new SemiontSession({
|
|
2603
|
-
kb,
|
|
2604
|
-
storage: this.storage,
|
|
2605
|
-
refresh: () => this.performRefresh(kb),
|
|
2606
|
-
validate: (token) => this.performValidate(kb, token),
|
|
2607
|
-
onAuthFailed: (msg) => signals.notifySessionExpired(msg),
|
|
2608
|
-
onError: (err) => this.error$.next(err)
|
|
2609
|
-
});
|
|
2610
|
-
try {
|
|
2611
|
-
await session.ready;
|
|
2612
|
-
} catch (err) {
|
|
2613
|
-
this.error$.next(
|
|
2614
|
-
new SemiontError(
|
|
2615
|
-
"session.construct-failed",
|
|
2616
|
-
err instanceof Error ? err.message : String(err),
|
|
2617
|
-
id
|
|
2618
|
-
)
|
|
2619
|
-
);
|
|
2620
|
-
await session.dispose();
|
|
2621
|
-
signals.dispose();
|
|
2622
|
-
return;
|
|
2623
|
-
}
|
|
2624
|
-
if (this.disposed || this.activeKbId$.getValue() !== id) {
|
|
2625
|
-
await session.dispose();
|
|
2626
|
-
signals.dispose();
|
|
2627
|
-
return;
|
|
2628
|
-
}
|
|
2629
|
-
this.activeSession$.next(session);
|
|
2630
|
-
this.activeSignals$.next(signals);
|
|
2631
|
-
})();
|
|
2632
|
-
this.activating = activation;
|
|
2633
|
-
this.sessionActivating$.next(true);
|
|
2634
|
-
try {
|
|
2635
|
-
await activation;
|
|
2636
|
-
} finally {
|
|
2637
|
-
if (this.activating === activation) {
|
|
2638
|
-
this.activating = null;
|
|
2639
|
-
this.sessionActivating$.next(false);
|
|
2640
|
-
}
|
|
2641
|
-
}
|
|
2642
|
-
}
|
|
2643
|
-
/**
|
|
2644
|
-
* Sign in to an existing KB: store the tokens and (re)activate the
|
|
2645
|
-
* session. If the KB is already active, the current session is disposed
|
|
2646
|
-
* and replaced so the new tokens take effect.
|
|
2647
|
-
*/
|
|
2648
|
-
async signIn(id, access, refresh) {
|
|
2649
|
-
if (this.disposed) return;
|
|
2650
|
-
setStoredSession(this.storage, id, { access, refresh });
|
|
2651
|
-
if (this.activeKbId$.getValue() === id) {
|
|
2652
|
-
const prevSession = this.activeSession$.getValue();
|
|
2653
|
-
const prevSignals = this.activeSignals$.getValue();
|
|
2654
|
-
this.activeSession$.next(null);
|
|
2655
|
-
this.activeSignals$.next(null);
|
|
2656
|
-
if (prevSession) await prevSession.dispose();
|
|
2657
|
-
prevSignals?.dispose();
|
|
2658
|
-
await this.setActiveKb(id);
|
|
2659
|
-
return;
|
|
2660
|
-
}
|
|
2661
|
-
await this.setActiveKb(id);
|
|
2662
|
-
}
|
|
2663
|
-
/**
|
|
2664
|
-
* Sign out of a KB: clear stored tokens. If the KB is active, dispose
|
|
2665
|
-
* its session + signals and emit null for both.
|
|
2666
|
-
*/
|
|
2667
|
-
async signOut(id) {
|
|
2668
|
-
if (this.disposed) return;
|
|
2669
|
-
clearStoredSession(this.storage, id);
|
|
2670
|
-
this.kbs$.next([...this.kbs$.getValue()]);
|
|
2671
|
-
if (this.activeKbId$.getValue() === id) {
|
|
2672
|
-
const prevSession = this.activeSession$.getValue();
|
|
2673
|
-
const prevSignals = this.activeSignals$.getValue();
|
|
2674
|
-
this.activeSession$.next(null);
|
|
2675
|
-
this.activeSignals$.next(null);
|
|
2676
|
-
if (prevSession) await prevSession.dispose();
|
|
2677
|
-
prevSignals?.dispose();
|
|
2678
|
-
}
|
|
2679
|
-
}
|
|
2680
|
-
// ── Open resources ────────────────────────────────────────────────────
|
|
2681
|
-
addOpenResource(id, name, mediaType, storageUri) {
|
|
2682
|
-
const existing = this.openResources$.getValue();
|
|
2683
|
-
const idx = existing.findIndex((r) => r.id === id);
|
|
2684
|
-
if (idx >= 0) {
|
|
2685
|
-
const prev = existing[idx];
|
|
2686
|
-
const updated = {
|
|
2687
|
-
...prev,
|
|
2688
|
-
name,
|
|
2689
|
-
...mediaType !== void 0 ? { mediaType } : {},
|
|
2690
|
-
...storageUri !== void 0 ? { storageUri } : {}
|
|
2691
|
-
};
|
|
2692
|
-
const next = [...existing];
|
|
2693
|
-
next[idx] = updated;
|
|
2694
|
-
this.openResources$.next(next);
|
|
2695
|
-
return;
|
|
2696
|
-
}
|
|
2697
|
-
const resource = {
|
|
2698
|
-
id,
|
|
2699
|
-
name,
|
|
2700
|
-
openedAt: Date.now(),
|
|
2701
|
-
order: existing.length,
|
|
2702
|
-
...mediaType !== void 0 ? { mediaType } : {},
|
|
2703
|
-
...storageUri !== void 0 ? { storageUri } : {}
|
|
2704
|
-
};
|
|
2705
|
-
this.openResources$.next([...existing, resource]);
|
|
2706
|
-
}
|
|
2707
|
-
removeOpenResource(id) {
|
|
2708
|
-
this.openResources$.next(this.openResources$.getValue().filter((r) => r.id !== id));
|
|
2709
|
-
}
|
|
2710
|
-
updateOpenResourceName(id, name) {
|
|
2711
|
-
this.openResources$.next(
|
|
2712
|
-
this.openResources$.getValue().map((r) => r.id === id ? { ...r, name } : r)
|
|
2713
|
-
);
|
|
2714
|
-
}
|
|
2715
|
-
reorderOpenResources(oldIndex, newIndex) {
|
|
2716
|
-
const list = [...this.openResources$.getValue()];
|
|
2717
|
-
if (oldIndex < 0 || oldIndex >= list.length || newIndex < 0 || newIndex >= list.length) {
|
|
2718
|
-
return;
|
|
2719
|
-
}
|
|
2720
|
-
const [moved] = list.splice(oldIndex, 1);
|
|
2721
|
-
if (moved) list.splice(newIndex, 0, moved);
|
|
2722
|
-
this.openResources$.next(list);
|
|
2723
|
-
}
|
|
2724
|
-
// ── Auth callbacks bound per session ──────────────────────────────────
|
|
2725
|
-
//
|
|
2726
|
-
// These closures back the `refresh` and `validate` callbacks passed
|
|
2727
|
-
// to `SemiontSession` in `setActiveKb`. Factored out as methods
|
|
2728
|
-
// (rather than inline in the activation closure) so test-doubles
|
|
2729
|
-
// can override them cleanly, and so the in-flight dedup map
|
|
2730
|
-
// survives across activations of the same KB.
|
|
2731
|
-
/**
|
|
2732
|
-
* Refresh the active KB's access token. Returns the new token on
|
|
2733
|
-
* success, null on failure. Concurrent calls for the same KB
|
|
2734
|
-
* dedupe through `inFlightRefreshes`, so simultaneous 401s trigger
|
|
2735
|
-
* only one `/api/tokens/refresh` round trip.
|
|
2736
|
-
*
|
|
2737
|
-
* Uses a throwaway `SemiontApiClient` with no `tokenRefresher` —
|
|
2738
|
-
* a refresh call returning 401 would otherwise re-enter this
|
|
2739
|
-
* function infinitely.
|
|
2740
|
-
*/
|
|
2741
|
-
async performRefresh(kb) {
|
|
2742
|
-
const existing = this.inFlightRefreshes.get(kb.id);
|
|
2743
|
-
if (existing) return existing;
|
|
2744
|
-
const promise = (async () => {
|
|
2745
|
-
const stored = getStoredSession(this.storage, kb.id);
|
|
2746
|
-
if (!stored) return null;
|
|
2747
|
-
const throwaway = new SemiontApiClient({
|
|
2748
|
-
baseUrl: baseUrl(kbBackendUrl(kb))
|
|
2749
|
-
});
|
|
2750
|
-
try {
|
|
2751
|
-
const response = await throwaway.refreshToken(refreshToken(stored.refresh));
|
|
2752
|
-
const newAccess = response.access_token;
|
|
2753
|
-
if (!newAccess) return null;
|
|
2754
|
-
setStoredSession(this.storage, kb.id, { access: newAccess, refresh: stored.refresh });
|
|
2755
|
-
return newAccess;
|
|
2756
|
-
} catch {
|
|
2757
|
-
return null;
|
|
2758
|
-
} finally {
|
|
2759
|
-
throwaway.dispose();
|
|
2760
|
-
}
|
|
2761
|
-
})();
|
|
2762
|
-
this.inFlightRefreshes.set(kb.id, promise);
|
|
2763
|
-
try {
|
|
2764
|
-
return await promise;
|
|
2765
|
-
} finally {
|
|
2766
|
-
this.inFlightRefreshes.delete(kb.id);
|
|
2767
|
-
}
|
|
2768
|
-
}
|
|
2769
|
-
/**
|
|
2770
|
-
* Validate an access token by calling `getMe` on a throwaway
|
|
2771
|
-
* client. The session uses this once at startup to populate
|
|
2772
|
-
* `user$`; 401 triggers a refresh-then-retry inside the session.
|
|
2773
|
-
*/
|
|
2774
|
-
async performValidate(kb, token) {
|
|
2775
|
-
const throwaway = new SemiontApiClient({
|
|
2776
|
-
baseUrl: baseUrl(kbBackendUrl(kb))
|
|
2777
|
-
});
|
|
2778
|
-
try {
|
|
2779
|
-
const data = await throwaway.getMe({ auth: token });
|
|
2780
|
-
return data;
|
|
2781
|
-
} finally {
|
|
2782
|
-
throwaway.dispose();
|
|
2783
|
-
}
|
|
2784
|
-
}
|
|
2785
|
-
// ── Lifecycle ─────────────────────────────────────────────────────────
|
|
2786
|
-
async dispose() {
|
|
2787
|
-
if (this.disposed) return;
|
|
2788
|
-
this.disposed = true;
|
|
2789
|
-
this.unregisterNotify?.();
|
|
2790
|
-
this.unregisterNotify = null;
|
|
2791
|
-
if (this.unsubscribeStorage) {
|
|
2792
|
-
this.unsubscribeStorage();
|
|
2793
|
-
this.unsubscribeStorage = null;
|
|
2794
|
-
}
|
|
2795
|
-
const prevSession = this.activeSession$.getValue();
|
|
2796
|
-
const prevSignals = this.activeSignals$.getValue();
|
|
2797
|
-
this.activeSession$.next(null);
|
|
2798
|
-
this.activeSignals$.next(null);
|
|
2799
|
-
if (prevSession) await prevSession.dispose();
|
|
2800
|
-
prevSignals?.dispose();
|
|
2801
|
-
this.kbs$.complete();
|
|
2802
|
-
this.activeKbId$.complete();
|
|
2803
|
-
this.activeSession$.complete();
|
|
2804
|
-
this.activeSignals$.complete();
|
|
2805
|
-
this.openResources$.complete();
|
|
2806
|
-
this.error$.complete();
|
|
2807
|
-
this.identityToken$.complete();
|
|
2808
|
-
this.eventBus.destroy();
|
|
2809
|
-
}
|
|
2810
|
-
};
|
|
2811
|
-
|
|
2812
|
-
// src/session/registry.ts
|
|
2813
|
-
var instance = null;
|
|
2814
|
-
function getBrowser(options) {
|
|
2815
|
-
if (!instance) {
|
|
2816
|
-
instance = new SemiontBrowser({ storage: options.storage });
|
|
2817
|
-
}
|
|
2818
|
-
return instance;
|
|
2819
|
-
}
|
|
2820
|
-
|
|
2821
|
-
// src/session/session-storage.ts
|
|
2822
|
-
var InMemorySessionStorage = class {
|
|
2823
|
-
map = /* @__PURE__ */ new Map();
|
|
2824
|
-
get(key) {
|
|
2825
|
-
return this.map.has(key) ? this.map.get(key) : null;
|
|
2826
|
-
}
|
|
2827
|
-
set(key, value) {
|
|
2828
|
-
this.map.set(key, value);
|
|
2829
|
-
}
|
|
2830
|
-
delete(key) {
|
|
2831
|
-
this.map.delete(key);
|
|
2832
|
-
}
|
|
2833
|
-
};
|
|
2834
|
-
function createDisposer() {
|
|
2835
|
-
const sub = new Subscription();
|
|
2836
|
-
return {
|
|
2837
|
-
add: (item) => sub.add(typeof item === "function" ? item : () => item.dispose()),
|
|
2838
|
-
dispose: () => sub.unsubscribe()
|
|
2839
|
-
};
|
|
2840
|
-
}
|
|
2841
|
-
function createSearchPipeline(fetch2, options = {}) {
|
|
2842
|
-
const debounceMs = options.debounceMs ?? 250;
|
|
2843
|
-
const initial = options.initialQuery ?? "";
|
|
2844
|
-
const input$ = new Subject();
|
|
2845
|
-
const query$ = input$.pipe(startWith(initial));
|
|
2846
|
-
const state$ = input$.pipe(
|
|
2847
|
-
startWith(initial),
|
|
2848
|
-
debounceTime(debounceMs),
|
|
2849
|
-
distinctUntilChanged$1(),
|
|
2850
|
-
switchMap((q) => {
|
|
2851
|
-
const trimmed = q.trim();
|
|
2852
|
-
if (!trimmed) {
|
|
2853
|
-
return of({ results: [], isSearching: false });
|
|
2854
|
-
}
|
|
2855
|
-
return fetch2(trimmed).pipe(
|
|
2856
|
-
map((results) => ({
|
|
2857
|
-
results: results ?? [],
|
|
2858
|
-
isSearching: results === void 0
|
|
2859
|
-
})),
|
|
2860
|
-
startWith({ results: [], isSearching: true })
|
|
2861
|
-
);
|
|
2862
|
-
})
|
|
2863
|
-
);
|
|
2864
|
-
return {
|
|
2865
|
-
query$,
|
|
2866
|
-
state$,
|
|
2867
|
-
setQuery: (value) => input$.next(value),
|
|
2868
|
-
dispose: () => input$.complete()
|
|
2869
|
-
};
|
|
2870
|
-
}
|
|
2871
|
-
function createBeckonVM(client) {
|
|
2872
|
-
const subs = [];
|
|
2873
|
-
const hovered$ = new BehaviorSubject(null);
|
|
2874
|
-
subs.push(client.stream("beckon:hover").subscribe(({ annotationId }) => {
|
|
2875
|
-
hovered$.next(annotationId);
|
|
2876
|
-
if (annotationId) {
|
|
2877
|
-
client.emit("beckon:sparkle", { annotationId });
|
|
2878
|
-
}
|
|
2879
|
-
}));
|
|
2880
|
-
subs.push(client.stream("browse:click").subscribe(({ annotationId }) => {
|
|
2881
|
-
client.emit("beckon:focus", { annotationId });
|
|
2882
|
-
}));
|
|
2883
|
-
return {
|
|
2884
|
-
hoveredAnnotationId$: hovered$.asObservable(),
|
|
2885
|
-
hover: (annotationId) => client.emit("beckon:hover", { annotationId }),
|
|
2886
|
-
focus: (annotationId) => client.emit("beckon:focus", { annotationId }),
|
|
2887
|
-
sparkle: (annotationId) => client.emit("beckon:sparkle", { annotationId }),
|
|
2888
|
-
dispose() {
|
|
2889
|
-
subs.forEach((s) => s.unsubscribe());
|
|
2890
|
-
hovered$.complete();
|
|
2891
|
-
}
|
|
2892
|
-
};
|
|
2893
|
-
}
|
|
2894
|
-
var HOVER_DELAY_MS = 150;
|
|
2895
|
-
function createHoverHandlers(emit, delayMs) {
|
|
2896
|
-
let currentHover = null;
|
|
2897
|
-
let timer = null;
|
|
2898
|
-
const cancelTimer = () => {
|
|
2899
|
-
if (timer !== null) {
|
|
2900
|
-
clearTimeout(timer);
|
|
2901
|
-
timer = null;
|
|
2902
|
-
}
|
|
2903
|
-
};
|
|
2904
|
-
const handleMouseEnter = (annotationId) => {
|
|
2905
|
-
if (currentHover === annotationId) return;
|
|
2906
|
-
cancelTimer();
|
|
2907
|
-
timer = setTimeout(() => {
|
|
2908
|
-
timer = null;
|
|
2909
|
-
currentHover = annotationId;
|
|
2910
|
-
emit(annotationId);
|
|
2911
|
-
}, delayMs);
|
|
2912
|
-
};
|
|
2913
|
-
const handleMouseLeave = () => {
|
|
2914
|
-
cancelTimer();
|
|
2915
|
-
if (currentHover !== null) {
|
|
2916
|
-
currentHover = null;
|
|
2917
|
-
emit(null);
|
|
2918
|
-
}
|
|
2919
|
-
};
|
|
2920
|
-
return { handleMouseEnter, handleMouseLeave, cleanup: cancelTimer };
|
|
2921
|
-
}
|
|
2922
|
-
var COMMON_PANELS = ["knowledge-base", "user", "settings"];
|
|
2923
|
-
var RESOURCE_PANELS = ["history", "info", "annotations", "collaboration", "jsonld"];
|
|
2924
|
-
var MOTIVATION_TO_TAB = {
|
|
2925
|
-
"linking": "reference",
|
|
2926
|
-
"commenting": "comment",
|
|
2927
|
-
"tagging": "tag",
|
|
2928
|
-
"highlighting": "highlight",
|
|
2929
|
-
"assessing": "assessment"
|
|
2930
|
-
};
|
|
2931
|
-
var tabGenerationCounter = 0;
|
|
2932
|
-
function createShellVM(browser, options) {
|
|
2933
|
-
const subs = [];
|
|
2934
|
-
const activePanel$ = new BehaviorSubject(options?.initialPanel ?? null);
|
|
2935
|
-
const scrollToAnnotationId$ = new BehaviorSubject(null);
|
|
2936
|
-
const panelInitialTab$ = new BehaviorSubject(null);
|
|
2937
|
-
if (options?.onPanelChange) {
|
|
2938
|
-
const cb = options.onPanelChange;
|
|
2939
|
-
subs.push(activePanel$.subscribe(cb));
|
|
2940
|
-
}
|
|
2941
|
-
subs.push(browser.stream("panel:toggle").subscribe(({ panel }) => {
|
|
2942
|
-
const current = activePanel$.getValue();
|
|
2943
|
-
activePanel$.next(current === panel ? null : panel);
|
|
2944
|
-
}));
|
|
2945
|
-
subs.push(browser.stream("panel:open").subscribe(({ panel, scrollToAnnotationId, motivation }) => {
|
|
2946
|
-
if (scrollToAnnotationId) {
|
|
2947
|
-
scrollToAnnotationId$.next(scrollToAnnotationId);
|
|
2948
|
-
}
|
|
2949
|
-
if (motivation) {
|
|
2950
|
-
const tab = MOTIVATION_TO_TAB[motivation] || "highlight";
|
|
2951
|
-
panelInitialTab$.next({ tab, generation: ++tabGenerationCounter });
|
|
2952
|
-
}
|
|
2953
|
-
activePanel$.next(panel);
|
|
2954
|
-
}));
|
|
2955
|
-
subs.push(browser.stream("panel:close").subscribe(() => {
|
|
2956
|
-
activePanel$.next(null);
|
|
2957
|
-
}));
|
|
2958
|
-
return {
|
|
2959
|
-
activePanel$: activePanel$.asObservable(),
|
|
2960
|
-
scrollToAnnotationId$: scrollToAnnotationId$.asObservable(),
|
|
2961
|
-
panelInitialTab$: panelInitialTab$.asObservable(),
|
|
2962
|
-
openPanel: (panel) => browser.emit("panel:open", { panel }),
|
|
2963
|
-
closePanel: () => browser.emit("panel:close", void 0),
|
|
2964
|
-
togglePanel: (panel) => browser.emit("panel:toggle", { panel }),
|
|
2965
|
-
onScrollCompleted: () => scrollToAnnotationId$.next(null),
|
|
2966
|
-
dispose() {
|
|
2967
|
-
subs.forEach((s) => s.unsubscribe());
|
|
2968
|
-
activePanel$.complete();
|
|
2969
|
-
scrollToAnnotationId$.complete();
|
|
2970
|
-
panelInitialTab$.complete();
|
|
2971
|
-
}
|
|
2972
|
-
};
|
|
2973
|
-
}
|
|
2974
|
-
function createGatherVM(client, resourceId) {
|
|
2975
|
-
const subs = [];
|
|
2976
|
-
const context$ = new BehaviorSubject(null);
|
|
2977
|
-
const loading$ = new BehaviorSubject(false);
|
|
2978
|
-
const error$ = new BehaviorSubject(null);
|
|
2979
|
-
const annotationId$ = new BehaviorSubject(null);
|
|
2980
|
-
subs.push(client.stream("gather:requested").subscribe((event) => {
|
|
2981
|
-
loading$.next(true);
|
|
2982
|
-
error$.next(null);
|
|
2983
|
-
context$.next(null);
|
|
2984
|
-
annotationId$.next(annotationId(event.annotationId));
|
|
2985
|
-
const gatherSub = client.gather.annotation(
|
|
2986
|
-
annotationId(event.annotationId),
|
|
2987
|
-
resourceId,
|
|
2988
|
-
{ contextWindow: event.options?.contextWindow ?? 2e3 }
|
|
2989
|
-
).pipe(
|
|
2990
|
-
timeout(6e4)
|
|
2991
|
-
).subscribe({
|
|
2992
|
-
next: (progress) => {
|
|
2993
|
-
if ("response" in progress && progress.response) {
|
|
2994
|
-
context$.next(
|
|
2995
|
-
progress.response.context ?? null
|
|
2996
|
-
);
|
|
2997
|
-
loading$.next(false);
|
|
2998
|
-
}
|
|
2999
|
-
},
|
|
3000
|
-
error: (err) => {
|
|
3001
|
-
error$.next(err instanceof Error ? err : new Error(String(err)));
|
|
3002
|
-
loading$.next(false);
|
|
3003
|
-
},
|
|
3004
|
-
complete: () => {
|
|
3005
|
-
loading$.next(false);
|
|
3006
|
-
}
|
|
3007
|
-
});
|
|
3008
|
-
subs.push(gatherSub);
|
|
3009
|
-
}));
|
|
3010
|
-
return {
|
|
3011
|
-
context$: context$.asObservable(),
|
|
3012
|
-
loading$: loading$.asObservable(),
|
|
3013
|
-
error$: error$.asObservable(),
|
|
3014
|
-
annotationId$: annotationId$.asObservable(),
|
|
3015
|
-
dispose() {
|
|
3016
|
-
subs.forEach((s) => s.unsubscribe());
|
|
3017
|
-
context$.complete();
|
|
3018
|
-
loading$.complete();
|
|
3019
|
-
error$.complete();
|
|
3020
|
-
annotationId$.complete();
|
|
3021
|
-
}
|
|
3022
|
-
};
|
|
3023
|
-
}
|
|
3024
|
-
function createMatchVM(client, _resourceId) {
|
|
3025
|
-
const subs = [];
|
|
3026
|
-
subs.push(client.stream("match:search-requested").subscribe((event) => {
|
|
3027
|
-
const searchSub = client.match.search(
|
|
3028
|
-
resourceId(event.resourceId),
|
|
3029
|
-
event.referenceId,
|
|
3030
|
-
event.context,
|
|
3031
|
-
{ limit: event.limit, useSemanticScoring: event.useSemanticScoring }
|
|
3032
|
-
).pipe(
|
|
3033
|
-
timeout(6e4)
|
|
3034
|
-
).subscribe({
|
|
3035
|
-
next: (result) => client.emit("match:search-results", result),
|
|
3036
|
-
error: (err) => client.emit("match:search-failed", {
|
|
3037
|
-
correlationId: event.correlationId,
|
|
3038
|
-
referenceId: event.referenceId,
|
|
3039
|
-
error: err instanceof Error ? err.message : String(err)
|
|
3040
|
-
})
|
|
3041
|
-
});
|
|
3042
|
-
subs.push(searchSub);
|
|
3043
|
-
}));
|
|
3044
|
-
return {
|
|
3045
|
-
dispose() {
|
|
3046
|
-
subs.forEach((s) => s.unsubscribe());
|
|
3047
|
-
}
|
|
3048
|
-
};
|
|
3049
|
-
}
|
|
3050
|
-
function createYieldVM(client, resourceId$1, locale) {
|
|
3051
|
-
const subs = [];
|
|
3052
|
-
const isGenerating$ = new BehaviorSubject(false);
|
|
3053
|
-
const progress$ = new BehaviorSubject(null);
|
|
3054
|
-
let clearTimer = null;
|
|
3055
|
-
const generate = (referenceId, options) => {
|
|
3056
|
-
const genSub = client.yield.fromAnnotation(
|
|
3057
|
-
resourceId(resourceId$1),
|
|
3058
|
-
annotationId(referenceId),
|
|
3059
|
-
{ ...options, language: options.language || locale }
|
|
3060
|
-
).pipe(
|
|
3061
|
-
timeout({ each: 3e5 })
|
|
3062
|
-
).subscribe({
|
|
3063
|
-
next: (chunk) => {
|
|
3064
|
-
progress$.next(chunk);
|
|
3065
|
-
isGenerating$.next(true);
|
|
3066
|
-
},
|
|
3067
|
-
complete: () => {
|
|
3068
|
-
isGenerating$.next(false);
|
|
3069
|
-
if (clearTimer) clearTimeout(clearTimer);
|
|
3070
|
-
clearTimer = setTimeout(() => {
|
|
3071
|
-
progress$.next(null);
|
|
3072
|
-
clearTimer = null;
|
|
3073
|
-
}, 2e3);
|
|
3074
|
-
},
|
|
3075
|
-
error: () => {
|
|
3076
|
-
progress$.next(null);
|
|
3077
|
-
isGenerating$.next(false);
|
|
3078
|
-
}
|
|
3079
|
-
});
|
|
3080
|
-
subs.push(genSub);
|
|
3081
|
-
};
|
|
3082
|
-
return {
|
|
3083
|
-
isGenerating$: isGenerating$.asObservable(),
|
|
3084
|
-
progress$: progress$.asObservable(),
|
|
3085
|
-
generate,
|
|
3086
|
-
dispose() {
|
|
3087
|
-
subs.forEach((s) => s.unsubscribe());
|
|
3088
|
-
if (clearTimer) clearTimeout(clearTimer);
|
|
3089
|
-
isGenerating$.complete();
|
|
3090
|
-
progress$.complete();
|
|
3091
|
-
}
|
|
3092
|
-
};
|
|
3093
|
-
}
|
|
3094
|
-
function selectionToSelector(selection) {
|
|
3095
|
-
if (selection.svgSelector) return { type: "SvgSelector", value: selection.svgSelector };
|
|
3096
|
-
if (selection.fragmentSelector) {
|
|
3097
|
-
const selectors = [{ type: "FragmentSelector", value: selection.fragmentSelector, ...selection.conformsTo && { conformsTo: selection.conformsTo } }];
|
|
3098
|
-
if (selection.exact) selectors.push({ type: "TextQuoteSelector", exact: selection.exact, ...selection.prefix && { prefix: selection.prefix }, ...selection.suffix && { suffix: selection.suffix } });
|
|
3099
|
-
return selectors;
|
|
3100
|
-
}
|
|
3101
|
-
return { type: "TextQuoteSelector", exact: selection.exact, ...selection.prefix && { prefix: selection.prefix }, ...selection.suffix && { suffix: selection.suffix } };
|
|
3102
|
-
}
|
|
3103
|
-
function createMarkVM(client, resourceId) {
|
|
3104
|
-
const subs = [];
|
|
3105
|
-
const pendingAnnotation$ = new BehaviorSubject(null);
|
|
3106
|
-
const assistingMotivation$ = new BehaviorSubject(null);
|
|
3107
|
-
const progress$ = new BehaviorSubject(null);
|
|
3108
|
-
let progressDismissTimer = null;
|
|
3109
|
-
const clearProgressTimer = () => {
|
|
3110
|
-
if (progressDismissTimer) {
|
|
3111
|
-
clearTimeout(progressDismissTimer);
|
|
3112
|
-
progressDismissTimer = null;
|
|
3113
|
-
}
|
|
3114
|
-
};
|
|
3115
|
-
const handleAnnotationRequested = (pending) => {
|
|
3116
|
-
pendingAnnotation$.next(pending);
|
|
3117
|
-
};
|
|
3118
|
-
subs.push(client.stream("mark:requested").subscribe(handleAnnotationRequested));
|
|
3119
|
-
subs.push(client.stream("mark:select-comment").subscribe((s) => handleAnnotationRequested({ selector: selectionToSelector(s), motivation: "commenting" })));
|
|
3120
|
-
subs.push(client.stream("mark:select-tag").subscribe((s) => handleAnnotationRequested({ selector: selectionToSelector(s), motivation: "tagging" })));
|
|
3121
|
-
subs.push(client.stream("mark:select-assessment").subscribe((s) => handleAnnotationRequested({ selector: selectionToSelector(s), motivation: "assessing" })));
|
|
3122
|
-
subs.push(client.stream("mark:select-reference").subscribe((s) => handleAnnotationRequested({ selector: selectionToSelector(s), motivation: "linking" })));
|
|
3123
|
-
subs.push(client.stream("mark:cancel-pending").subscribe(() => pendingAnnotation$.next(null)));
|
|
3124
|
-
subs.push(client.stream("mark:create-ok").subscribe(() => pendingAnnotation$.next(null)));
|
|
3125
|
-
subs.push(client.stream("mark:submit").subscribe(async (event) => {
|
|
3126
|
-
try {
|
|
3127
|
-
const result = await client.mark.annotation(resourceId, {
|
|
3128
|
-
motivation: event.motivation,
|
|
3129
|
-
target: { source: resourceId, selector: event.selector },
|
|
3130
|
-
body: event.body
|
|
3131
|
-
});
|
|
3132
|
-
client.emit("mark:create-ok", { annotationId: result.annotationId });
|
|
3133
|
-
} catch (error) {
|
|
3134
|
-
client.emit("mark:create-failed", { message: error instanceof Error ? error.message : String(error) });
|
|
3135
|
-
}
|
|
3136
|
-
}));
|
|
3137
|
-
subs.push(client.stream("mark:delete").subscribe(async (event) => {
|
|
3138
|
-
try {
|
|
3139
|
-
await client.mark.delete(resourceId, event.annotationId);
|
|
3140
|
-
client.emit("mark:delete-ok", { annotationId: event.annotationId });
|
|
3141
|
-
} catch (error) {
|
|
3142
|
-
client.emit("mark:delete-failed", { message: error instanceof Error ? error.message : String(error) });
|
|
3143
|
-
}
|
|
3144
|
-
}));
|
|
3145
|
-
subs.push(client.stream("mark:assist-request").subscribe((event) => {
|
|
3146
|
-
clearProgressTimer();
|
|
3147
|
-
assistingMotivation$.next(event.motivation);
|
|
3148
|
-
progress$.next(null);
|
|
3149
|
-
const assistSub = client.mark.assist(resourceId, event.motivation, event.options).pipe(
|
|
3150
|
-
timeout({ each: 18e4 })
|
|
3151
|
-
).subscribe({
|
|
3152
|
-
next: (p) => progress$.next(p),
|
|
3153
|
-
complete: () => {
|
|
3154
|
-
assistingMotivation$.next(null);
|
|
3155
|
-
clearProgressTimer();
|
|
3156
|
-
progressDismissTimer = setTimeout(() => {
|
|
3157
|
-
progress$.next(null);
|
|
3158
|
-
progressDismissTimer = null;
|
|
3159
|
-
}, 5e3);
|
|
3160
|
-
},
|
|
3161
|
-
error: () => {
|
|
3162
|
-
clearProgressTimer();
|
|
3163
|
-
assistingMotivation$.next(null);
|
|
3164
|
-
progress$.next(null);
|
|
3165
|
-
}
|
|
3166
|
-
});
|
|
3167
|
-
subs.push(assistSub);
|
|
3168
|
-
}));
|
|
3169
|
-
subs.push(client.stream("mark:progress-dismiss").subscribe(() => {
|
|
3170
|
-
clearProgressTimer();
|
|
3171
|
-
progress$.next(null);
|
|
3172
|
-
}));
|
|
3173
|
-
return {
|
|
3174
|
-
pendingAnnotation$: pendingAnnotation$.asObservable(),
|
|
3175
|
-
assistingMotivation$: assistingMotivation$.asObservable(),
|
|
3176
|
-
progress$: progress$.asObservable(),
|
|
3177
|
-
dispose() {
|
|
3178
|
-
subs.forEach((s) => s.unsubscribe());
|
|
3179
|
-
clearProgressTimer();
|
|
3180
|
-
pendingAnnotation$.complete();
|
|
3181
|
-
assistingMotivation$.complete();
|
|
3182
|
-
progress$.complete();
|
|
3183
|
-
}
|
|
3184
|
-
};
|
|
3185
|
-
}
|
|
3186
|
-
var RECENT_LIMIT = 10;
|
|
3187
|
-
var SEARCH_LIMIT = 20;
|
|
3188
|
-
function createDiscoverVM(client, browse) {
|
|
3189
|
-
const disposer = createDisposer();
|
|
3190
|
-
const search = createSearchPipeline(
|
|
3191
|
-
(q) => client.browse.resources({ search: q, limit: SEARCH_LIMIT })
|
|
3192
|
-
);
|
|
3193
|
-
disposer.add(search);
|
|
3194
|
-
disposer.add(browse);
|
|
3195
|
-
const recent$ = client.browse.resources({ limit: RECENT_LIMIT, archived: false });
|
|
3196
|
-
const recentResources$ = recent$.pipe(
|
|
3197
|
-
map$1((r) => r ?? [])
|
|
3198
|
-
);
|
|
3199
|
-
const isLoadingRecent$ = recent$.pipe(
|
|
3200
|
-
map$1((r) => r === void 0)
|
|
3201
|
-
);
|
|
3202
|
-
const entityTypes$ = client.browse.entityTypes().pipe(
|
|
3203
|
-
map$1((e) => e ?? [])
|
|
3204
|
-
);
|
|
3205
|
-
return {
|
|
3206
|
-
browse,
|
|
3207
|
-
search,
|
|
3208
|
-
recentResources$,
|
|
3209
|
-
entityTypes$,
|
|
3210
|
-
isLoadingRecent$,
|
|
3211
|
-
dispose: () => disposer.dispose()
|
|
3212
|
-
};
|
|
3213
|
-
}
|
|
3214
|
-
function createEntityTagsVM(client, browse) {
|
|
3215
|
-
const disposer = createDisposer();
|
|
3216
|
-
disposer.add(browse);
|
|
3217
|
-
const newTag$ = new BehaviorSubject("");
|
|
3218
|
-
const error$ = new BehaviorSubject("");
|
|
3219
|
-
const isAdding$ = new BehaviorSubject(false);
|
|
3220
|
-
const raw$ = client.browse.entityTypes();
|
|
3221
|
-
const entityTypes$ = raw$.pipe(map$1((e) => e ?? []));
|
|
3222
|
-
const isLoading$ = raw$.pipe(map$1((e) => e === void 0));
|
|
3223
|
-
const addTag = async () => {
|
|
3224
|
-
const tag = newTag$.getValue().trim();
|
|
3225
|
-
if (!tag) return;
|
|
3226
|
-
error$.next("");
|
|
3227
|
-
isAdding$.next(true);
|
|
3228
|
-
try {
|
|
3229
|
-
await client.mark.entityType(tag);
|
|
3230
|
-
newTag$.next("");
|
|
3231
|
-
} catch (err) {
|
|
3232
|
-
error$.next(err instanceof Error ? err.message : "Failed to add entity type");
|
|
3233
|
-
} finally {
|
|
3234
|
-
isAdding$.next(false);
|
|
3235
|
-
}
|
|
3236
|
-
};
|
|
3237
|
-
return {
|
|
3238
|
-
browse,
|
|
3239
|
-
entityTypes$,
|
|
3240
|
-
isLoading$,
|
|
3241
|
-
newTag$: newTag$.asObservable(),
|
|
3242
|
-
error$: error$.asObservable(),
|
|
3243
|
-
isAdding$: isAdding$.asObservable(),
|
|
3244
|
-
setNewTag: (v) => newTag$.next(v),
|
|
3245
|
-
addTag,
|
|
3246
|
-
dispose: () => {
|
|
3247
|
-
newTag$.complete();
|
|
3248
|
-
error$.complete();
|
|
3249
|
-
isAdding$.complete();
|
|
3250
|
-
disposer.dispose();
|
|
3251
|
-
}
|
|
3252
|
-
};
|
|
3253
|
-
}
|
|
3254
|
-
function createExchangeVM(browse, exportFn, importFn) {
|
|
3255
|
-
const disposer = createDisposer();
|
|
3256
|
-
disposer.add(browse);
|
|
3257
|
-
const selectedFile$ = new BehaviorSubject(null);
|
|
3258
|
-
const preview$ = new BehaviorSubject(null);
|
|
3259
|
-
const importPhase$ = new BehaviorSubject(null);
|
|
3260
|
-
const importMessage$ = new BehaviorSubject(void 0);
|
|
3261
|
-
const importResult$ = new BehaviorSubject(void 0);
|
|
3262
|
-
const isExporting$ = new BehaviorSubject(false);
|
|
3263
|
-
const isImporting$ = new BehaviorSubject(false);
|
|
3264
|
-
const selectFile = (file) => {
|
|
3265
|
-
selectedFile$.next(file);
|
|
3266
|
-
importPhase$.next(null);
|
|
3267
|
-
importMessage$.next(void 0);
|
|
3268
|
-
importResult$.next(void 0);
|
|
3269
|
-
preview$.next({
|
|
3270
|
-
format: file.name.endsWith(".tar.gz") || file.name.endsWith(".gz") ? "semiont-linked-data" : "unknown",
|
|
3271
|
-
version: 1,
|
|
3272
|
-
sourceUrl: "",
|
|
3273
|
-
stats: {}
|
|
3274
|
-
});
|
|
3275
|
-
};
|
|
3276
|
-
const cancelImport = () => {
|
|
3277
|
-
selectedFile$.next(null);
|
|
3278
|
-
preview$.next(null);
|
|
3279
|
-
importPhase$.next(null);
|
|
3280
|
-
importMessage$.next(void 0);
|
|
3281
|
-
importResult$.next(void 0);
|
|
3282
|
-
};
|
|
3283
|
-
const doExport = async () => {
|
|
3284
|
-
isExporting$.next(true);
|
|
3285
|
-
try {
|
|
3286
|
-
const response = await exportFn();
|
|
3287
|
-
if (!response.ok) throw new Error(`Export failed: ${response.status} ${response.statusText}`);
|
|
3288
|
-
const blob = await response.blob();
|
|
3289
|
-
const contentDisposition = response.headers.get("Content-Disposition");
|
|
3290
|
-
const filename = contentDisposition?.match(/filename="(.+?)"/)?.[1] ?? `semiont-export-${Date.now()}.tar.gz`;
|
|
3291
|
-
return { blob, filename };
|
|
3292
|
-
} finally {
|
|
3293
|
-
isExporting$.next(false);
|
|
3294
|
-
}
|
|
3295
|
-
};
|
|
3296
|
-
const doImport = async () => {
|
|
3297
|
-
const file = selectedFile$.getValue();
|
|
3298
|
-
if (!file) return;
|
|
3299
|
-
isImporting$.next(true);
|
|
3300
|
-
importPhase$.next("started");
|
|
3301
|
-
importMessage$.next(void 0);
|
|
3302
|
-
importResult$.next(void 0);
|
|
3303
|
-
try {
|
|
3304
|
-
await importFn(file, {
|
|
3305
|
-
onProgress: (event) => {
|
|
3306
|
-
importPhase$.next(event.phase);
|
|
3307
|
-
importMessage$.next(event.message);
|
|
3308
|
-
if (event.result) importResult$.next(event.result);
|
|
3309
|
-
}
|
|
3310
|
-
});
|
|
3311
|
-
} finally {
|
|
3312
|
-
isImporting$.next(false);
|
|
3313
|
-
}
|
|
3314
|
-
};
|
|
3315
|
-
return {
|
|
3316
|
-
browse,
|
|
3317
|
-
selectedFile$: selectedFile$.asObservable(),
|
|
3318
|
-
preview$: preview$.asObservable(),
|
|
3319
|
-
importPhase$: importPhase$.asObservable(),
|
|
3320
|
-
importMessage$: importMessage$.asObservable(),
|
|
3321
|
-
importResult$: importResult$.asObservable(),
|
|
3322
|
-
isExporting$: isExporting$.asObservable(),
|
|
3323
|
-
isImporting$: isImporting$.asObservable(),
|
|
3324
|
-
selectFile,
|
|
3325
|
-
cancelImport,
|
|
3326
|
-
doExport,
|
|
3327
|
-
doImport,
|
|
3328
|
-
dispose: () => {
|
|
3329
|
-
selectedFile$.complete();
|
|
3330
|
-
preview$.complete();
|
|
3331
|
-
importPhase$.complete();
|
|
3332
|
-
importMessage$.complete();
|
|
3333
|
-
importResult$.complete();
|
|
3334
|
-
isExporting$.complete();
|
|
3335
|
-
isImporting$.complete();
|
|
3336
|
-
disposer.dispose();
|
|
3337
|
-
}
|
|
3338
|
-
};
|
|
3339
|
-
}
|
|
3340
|
-
function createAdminUsersVM(client, browse) {
|
|
3341
|
-
const disposer = createDisposer();
|
|
3342
|
-
disposer.add(browse);
|
|
3343
|
-
const users$ = new BehaviorSubject([]);
|
|
3344
|
-
const stats$ = new BehaviorSubject(null);
|
|
3345
|
-
const usersLoading$ = new BehaviorSubject(true);
|
|
3346
|
-
const statsLoading$ = new BehaviorSubject(true);
|
|
3347
|
-
const fetchUsers = () => {
|
|
3348
|
-
usersLoading$.next(true);
|
|
3349
|
-
client.listUsers().then((data) => {
|
|
3350
|
-
users$.next(data.users ?? []);
|
|
3351
|
-
usersLoading$.next(false);
|
|
3352
|
-
}).catch(() => usersLoading$.next(false));
|
|
3353
|
-
};
|
|
3354
|
-
const fetchStats = () => {
|
|
3355
|
-
statsLoading$.next(true);
|
|
3356
|
-
client.getUserStats().then((data) => {
|
|
3357
|
-
stats$.next(data.stats ?? null);
|
|
3358
|
-
statsLoading$.next(false);
|
|
3359
|
-
}).catch(() => statsLoading$.next(false));
|
|
3360
|
-
};
|
|
3361
|
-
fetchUsers();
|
|
3362
|
-
fetchStats();
|
|
3363
|
-
const updateUser = async (id, data) => {
|
|
3364
|
-
await client.updateUser(userDID(id), data);
|
|
3365
|
-
fetchUsers();
|
|
3366
|
-
fetchStats();
|
|
3367
|
-
};
|
|
3368
|
-
return {
|
|
3369
|
-
browse,
|
|
3370
|
-
users$: users$.asObservable(),
|
|
3371
|
-
stats$: stats$.asObservable(),
|
|
3372
|
-
usersLoading$: usersLoading$.asObservable(),
|
|
3373
|
-
statsLoading$: statsLoading$.asObservable(),
|
|
3374
|
-
updateUser,
|
|
3375
|
-
dispose: () => {
|
|
3376
|
-
users$.complete();
|
|
3377
|
-
stats$.complete();
|
|
3378
|
-
usersLoading$.complete();
|
|
3379
|
-
statsLoading$.complete();
|
|
3380
|
-
disposer.dispose();
|
|
3381
|
-
}
|
|
3382
|
-
};
|
|
3383
|
-
}
|
|
3384
|
-
function createAdminSecurityVM(client, browse) {
|
|
3385
|
-
const disposer = createDisposer();
|
|
3386
|
-
disposer.add(browse);
|
|
3387
|
-
const providers$ = new BehaviorSubject([]);
|
|
3388
|
-
const allowedDomains$ = new BehaviorSubject([]);
|
|
3389
|
-
const isLoading$ = new BehaviorSubject(true);
|
|
3390
|
-
client.getOAuthConfig().then((data) => {
|
|
3391
|
-
const config = data;
|
|
3392
|
-
providers$.next(config.providers ?? []);
|
|
3393
|
-
allowedDomains$.next(config.allowedDomains ?? []);
|
|
3394
|
-
isLoading$.next(false);
|
|
3395
|
-
}).catch(() => isLoading$.next(false));
|
|
3396
|
-
return {
|
|
3397
|
-
browse,
|
|
3398
|
-
providers$: providers$.asObservable(),
|
|
3399
|
-
allowedDomains$: allowedDomains$.asObservable(),
|
|
3400
|
-
isLoading$: isLoading$.asObservable(),
|
|
3401
|
-
dispose: () => {
|
|
3402
|
-
providers$.complete();
|
|
3403
|
-
allowedDomains$.complete();
|
|
3404
|
-
isLoading$.complete();
|
|
3405
|
-
disposer.dispose();
|
|
3406
|
-
}
|
|
3407
|
-
};
|
|
3408
|
-
}
|
|
3409
|
-
function createWelcomeVM(client) {
|
|
3410
|
-
const disposer = createDisposer();
|
|
3411
|
-
const userData$ = new BehaviorSubject(null);
|
|
3412
|
-
const isProcessing$ = new BehaviorSubject(false);
|
|
3413
|
-
client.getMe().then((data) => userData$.next(data)).catch(() => {
|
|
3414
|
-
});
|
|
3415
|
-
const acceptTerms = async () => {
|
|
3416
|
-
isProcessing$.next(true);
|
|
3417
|
-
try {
|
|
3418
|
-
await client.acceptTerms();
|
|
3419
|
-
userData$.next({ ...userData$.getValue(), termsAcceptedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
3420
|
-
} finally {
|
|
3421
|
-
isProcessing$.next(false);
|
|
3422
|
-
}
|
|
3423
|
-
};
|
|
3424
|
-
return {
|
|
3425
|
-
userData$: userData$.asObservable(),
|
|
3426
|
-
isProcessing$: isProcessing$.asObservable(),
|
|
3427
|
-
acceptTerms,
|
|
3428
|
-
dispose: () => {
|
|
3429
|
-
userData$.complete();
|
|
3430
|
-
isProcessing$.complete();
|
|
3431
|
-
disposer.dispose();
|
|
3432
|
-
}
|
|
3433
|
-
};
|
|
3434
|
-
}
|
|
3435
|
-
function createResourceLoaderVM(client, resourceId) {
|
|
3436
|
-
const raw$ = client.browse.resource(resourceId);
|
|
3437
|
-
const resource$ = raw$;
|
|
3438
|
-
const isLoading$ = raw$.pipe(map$1((r) => r === void 0));
|
|
3439
|
-
return {
|
|
3440
|
-
resource$,
|
|
3441
|
-
isLoading$,
|
|
3442
|
-
invalidate: () => client.browse.invalidateResourceDetail(resourceId),
|
|
3443
|
-
dispose: () => {
|
|
3444
|
-
}
|
|
3445
|
-
};
|
|
3446
|
-
}
|
|
3447
|
-
function createSessionVM(client) {
|
|
3448
|
-
const isLoggingOut$ = new BehaviorSubject(false);
|
|
3449
|
-
const logout = async () => {
|
|
3450
|
-
isLoggingOut$.next(true);
|
|
3451
|
-
try {
|
|
3452
|
-
await client.logout();
|
|
3453
|
-
} catch {
|
|
3454
|
-
} finally {
|
|
3455
|
-
isLoggingOut$.next(false);
|
|
3456
|
-
}
|
|
3457
|
-
};
|
|
3458
|
-
return {
|
|
3459
|
-
isLoggingOut$: isLoggingOut$.asObservable(),
|
|
3460
|
-
logout,
|
|
3461
|
-
dispose: () => {
|
|
3462
|
-
isLoggingOut$.complete();
|
|
3463
|
-
}
|
|
3464
|
-
};
|
|
3465
|
-
}
|
|
3466
|
-
var SMELTER_CHANNELS = [
|
|
3467
|
-
"yield:created",
|
|
3468
|
-
"yield:updated",
|
|
3469
|
-
"yield:representation-added",
|
|
3470
|
-
"mark:archived",
|
|
3471
|
-
"mark:added",
|
|
3472
|
-
"mark:removed"
|
|
3473
|
-
];
|
|
3474
|
-
function createSmelterActorVM(options) {
|
|
3475
|
-
const actor = createActorVM({
|
|
3476
|
-
baseUrl: options.baseUrl,
|
|
3477
|
-
token: options.token,
|
|
3478
|
-
channels: [...SMELTER_CHANNELS],
|
|
3479
|
-
reconnectMs: options.reconnectMs
|
|
3480
|
-
});
|
|
3481
|
-
const events$ = merge(
|
|
3482
|
-
...SMELTER_CHANNELS.map(
|
|
3483
|
-
(channel) => actor.on$(channel).pipe(
|
|
3484
|
-
map((payload) => ({
|
|
3485
|
-
type: channel,
|
|
3486
|
-
resourceId: payload.resourceId,
|
|
3487
|
-
payload
|
|
3488
|
-
}))
|
|
3489
|
-
)
|
|
3490
|
-
)
|
|
3491
|
-
);
|
|
3492
|
-
return {
|
|
3493
|
-
events$,
|
|
3494
|
-
state$: actor.state$,
|
|
3495
|
-
emit: (channel, payload) => actor.emit(channel, payload),
|
|
3496
|
-
start: () => actor.start(),
|
|
3497
|
-
stop: () => actor.stop(),
|
|
3498
|
-
dispose: () => actor.dispose()
|
|
3499
|
-
};
|
|
3500
|
-
}
|
|
3501
|
-
function createJobClaimAdapter(options) {
|
|
3502
|
-
const { actor, jobTypes } = options;
|
|
3503
|
-
const activeJob$ = new BehaviorSubject(null);
|
|
3504
|
-
const isProcessing$ = new BehaviorSubject(false);
|
|
3505
|
-
const jobsCompleted$ = new BehaviorSubject(0);
|
|
3506
|
-
const errors$ = new Subject();
|
|
3507
|
-
let jobSubscription = null;
|
|
3508
|
-
let started = false;
|
|
3509
|
-
const claimJob = async (assignment) => {
|
|
3510
|
-
try {
|
|
3511
|
-
const correlationId = crypto.randomUUID();
|
|
3512
|
-
const result$ = merge(
|
|
3513
|
-
actor.on$("job:claimed").pipe(
|
|
3514
|
-
filter$1((e) => e.correlationId === correlationId),
|
|
3515
|
-
map$1((e) => ({ ok: true, response: e.response }))
|
|
3516
|
-
),
|
|
3517
|
-
actor.on$("job:claim-failed").pipe(
|
|
3518
|
-
filter$1((e) => e.correlationId === correlationId),
|
|
3519
|
-
map$1(() => ({ ok: false }))
|
|
3520
|
-
)
|
|
3521
|
-
).pipe(take$1(1), timeout$1(1e4));
|
|
3522
|
-
const resultPromise = firstValueFrom(result$);
|
|
3523
|
-
await actor.emit("job:claim", { correlationId, jobId: assignment.jobId });
|
|
3524
|
-
const result = await resultPromise;
|
|
3525
|
-
if (!result.ok) return null;
|
|
3526
|
-
const job = result.response;
|
|
3527
|
-
return {
|
|
3528
|
-
jobId: assignment.jobId,
|
|
3529
|
-
type: assignment.type,
|
|
3530
|
-
resourceId: assignment.resourceId,
|
|
3531
|
-
userId: job.metadata?.userId ?? "",
|
|
3532
|
-
params: job.params ?? {}
|
|
3533
|
-
};
|
|
3534
|
-
} catch {
|
|
3535
|
-
return null;
|
|
3536
|
-
}
|
|
3537
|
-
};
|
|
3538
|
-
return {
|
|
3539
|
-
activeJob$: activeJob$.asObservable(),
|
|
3540
|
-
isProcessing$: isProcessing$.asObservable(),
|
|
3541
|
-
jobsCompleted$: jobsCompleted$.asObservable(),
|
|
3542
|
-
errors$: errors$.asObservable(),
|
|
3543
|
-
start: () => {
|
|
3544
|
-
if (started) return;
|
|
3545
|
-
started = true;
|
|
3546
|
-
actor.addChannels(["job:queued"]);
|
|
3547
|
-
jobSubscription = actor.on$("job:queued").subscribe((event) => {
|
|
3548
|
-
const jobType = event.jobType;
|
|
3549
|
-
if (jobTypes.length > 0 && !jobTypes.includes(jobType)) return;
|
|
3550
|
-
if (isProcessing$.getValue()) return;
|
|
3551
|
-
isProcessing$.next(true);
|
|
3552
|
-
claimJob({ jobId: event.jobId, type: jobType, resourceId: event.resourceId }).then((job) => {
|
|
3553
|
-
if (job) {
|
|
3554
|
-
activeJob$.next(job);
|
|
3555
|
-
} else {
|
|
3556
|
-
isProcessing$.next(false);
|
|
3557
|
-
}
|
|
3558
|
-
}).catch(() => {
|
|
3559
|
-
isProcessing$.next(false);
|
|
3560
|
-
});
|
|
3561
|
-
});
|
|
3562
|
-
},
|
|
3563
|
-
stop: () => {
|
|
3564
|
-
jobSubscription?.unsubscribe();
|
|
3565
|
-
jobSubscription = null;
|
|
3566
|
-
started = false;
|
|
3567
|
-
},
|
|
3568
|
-
completeJob: () => {
|
|
3569
|
-
activeJob$.next(null);
|
|
3570
|
-
isProcessing$.next(false);
|
|
3571
|
-
jobsCompleted$.next(jobsCompleted$.getValue() + 1);
|
|
3572
|
-
},
|
|
3573
|
-
failJob: (jid, error) => {
|
|
3574
|
-
activeJob$.next(null);
|
|
3575
|
-
isProcessing$.next(false);
|
|
3576
|
-
errors$.next({ jobId: jid, error });
|
|
3577
|
-
},
|
|
3578
|
-
dispose: () => {
|
|
3579
|
-
jobSubscription?.unsubscribe();
|
|
3580
|
-
jobSubscription = null;
|
|
3581
|
-
started = false;
|
|
3582
|
-
activeJob$.complete();
|
|
3583
|
-
isProcessing$.complete();
|
|
3584
|
-
jobsCompleted$.complete();
|
|
3585
|
-
errors$.complete();
|
|
3586
|
-
}
|
|
3587
|
-
};
|
|
3588
|
-
}
|
|
3589
|
-
function createJobQueueVM(client) {
|
|
3590
|
-
const jobs$ = new BehaviorSubject([]);
|
|
3591
|
-
const jobCreated$ = new Subject();
|
|
3592
|
-
const jobCompleted$ = new Subject();
|
|
3593
|
-
const jobFailed$ = new Subject();
|
|
3594
|
-
const pendingByType$ = jobs$.pipe(
|
|
3595
|
-
map$1((all) => {
|
|
3596
|
-
const counts = /* @__PURE__ */ new Map();
|
|
3597
|
-
for (const j of all) {
|
|
3598
|
-
if (j.status === "pending") {
|
|
3599
|
-
counts.set(j.type, (counts.get(j.type) ?? 0) + 1);
|
|
3600
|
-
}
|
|
3601
|
-
}
|
|
3602
|
-
return counts;
|
|
3603
|
-
})
|
|
3604
|
-
);
|
|
3605
|
-
const runningJobs$ = jobs$.pipe(
|
|
3606
|
-
map$1((all) => all.filter((j) => j.status === "running"))
|
|
3607
|
-
);
|
|
3608
|
-
const addOrUpdate = (job) => {
|
|
3609
|
-
const current = jobs$.getValue();
|
|
3610
|
-
const idx = current.findIndex((j) => j.jobId === job.jobId);
|
|
3611
|
-
if (idx >= 0) {
|
|
3612
|
-
const next = [...current];
|
|
3613
|
-
next[idx] = job;
|
|
3614
|
-
jobs$.next(next);
|
|
3615
|
-
} else {
|
|
3616
|
-
jobs$.next([...current, job]);
|
|
3617
|
-
}
|
|
3618
|
-
};
|
|
3619
|
-
const subs = [
|
|
3620
|
-
client.stream("job:queued").subscribe((event) => {
|
|
3621
|
-
const job = {
|
|
3622
|
-
jobId: event.jobId,
|
|
3623
|
-
type: event.jobType,
|
|
3624
|
-
status: "pending",
|
|
3625
|
-
resourceId: event.resourceId ?? "",
|
|
3626
|
-
userId: "",
|
|
3627
|
-
created: (/* @__PURE__ */ new Date()).toISOString()
|
|
3628
|
-
};
|
|
3629
|
-
addOrUpdate(job);
|
|
3630
|
-
jobCreated$.next(job);
|
|
3631
|
-
}),
|
|
3632
|
-
client.stream("job:complete").subscribe((event) => {
|
|
3633
|
-
const job = {
|
|
3634
|
-
jobId: event.jobId,
|
|
3635
|
-
type: event.jobType ?? "",
|
|
3636
|
-
status: "complete",
|
|
3637
|
-
resourceId: event.resourceId ?? "",
|
|
3638
|
-
userId: event.userId ?? "",
|
|
3639
|
-
created: "",
|
|
3640
|
-
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3641
|
-
result: event.result
|
|
3642
|
-
};
|
|
3643
|
-
addOrUpdate(job);
|
|
3644
|
-
jobCompleted$.next(job);
|
|
3645
|
-
}),
|
|
3646
|
-
client.stream("job:fail").subscribe((event) => {
|
|
3647
|
-
const job = {
|
|
3648
|
-
jobId: event.jobId,
|
|
3649
|
-
type: event.jobType ?? "",
|
|
3650
|
-
status: "failed",
|
|
3651
|
-
resourceId: event.resourceId ?? "",
|
|
3652
|
-
userId: event.userId ?? "",
|
|
3653
|
-
created: "",
|
|
3654
|
-
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3655
|
-
error: event.error ?? "Unknown error"
|
|
3656
|
-
};
|
|
3657
|
-
addOrUpdate(job);
|
|
3658
|
-
jobFailed$.next(job);
|
|
3659
|
-
})
|
|
3660
|
-
];
|
|
3661
|
-
return {
|
|
3662
|
-
jobs$: jobs$.asObservable(),
|
|
3663
|
-
pendingByType$,
|
|
3664
|
-
runningJobs$,
|
|
3665
|
-
jobCreated$: jobCreated$.asObservable(),
|
|
3666
|
-
jobCompleted$: jobCompleted$.asObservable(),
|
|
3667
|
-
jobFailed$: jobFailed$.asObservable(),
|
|
3668
|
-
dispose: () => {
|
|
3669
|
-
subs.forEach((s) => s.unsubscribe());
|
|
3670
|
-
jobs$.complete();
|
|
3671
|
-
jobCreated$.complete();
|
|
3672
|
-
jobCompleted$.complete();
|
|
3673
|
-
jobFailed$.complete();
|
|
3674
|
-
}
|
|
3675
|
-
};
|
|
3676
|
-
}
|
|
3677
|
-
|
|
3678
|
-
// src/utils/text-encoding.ts
|
|
3679
|
-
function extractCharset(mediaType) {
|
|
3680
|
-
const charsetMatch = mediaType.match(/charset=([^\s;]+)/i);
|
|
3681
|
-
return (charsetMatch?.[1] || "utf-8").toLowerCase();
|
|
3682
|
-
}
|
|
3683
|
-
function decodeWithCharset(buffer, mediaType) {
|
|
3684
|
-
const charset = extractCharset(mediaType);
|
|
3685
|
-
const decoder = new TextDecoder(charset);
|
|
3686
|
-
return decoder.decode(buffer);
|
|
3687
|
-
}
|
|
3688
|
-
function getBodySource(body) {
|
|
3689
|
-
if (Array.isArray(body)) {
|
|
3690
|
-
for (const item of body) {
|
|
3691
|
-
if (typeof item === "object" && item !== null && "type" in item && "source" in item) {
|
|
3692
|
-
const itemType = item.type;
|
|
3693
|
-
const itemSource = item.source;
|
|
3694
|
-
if (itemType === "SpecificResource" && typeof itemSource === "string") {
|
|
3695
|
-
return itemSource;
|
|
3696
|
-
}
|
|
3697
|
-
}
|
|
3698
|
-
}
|
|
3699
|
-
return null;
|
|
3700
|
-
}
|
|
3701
|
-
if (typeof body === "object" && body !== null && "type" in body && "source" in body) {
|
|
3702
|
-
const bodyType = body.type;
|
|
3703
|
-
const bodySource = body.source;
|
|
3704
|
-
if (bodyType === "SpecificResource" && typeof bodySource === "string") {
|
|
3705
|
-
return bodySource;
|
|
3706
|
-
}
|
|
3707
|
-
}
|
|
3708
|
-
return null;
|
|
3709
|
-
}
|
|
3710
|
-
function getBodyType(body) {
|
|
3711
|
-
if (Array.isArray(body)) {
|
|
3712
|
-
if (body.length === 0) {
|
|
3713
|
-
return null;
|
|
3714
|
-
}
|
|
3715
|
-
if (typeof body[0] === "object" && body[0] !== null && "type" in body[0]) {
|
|
3716
|
-
const firstType = body[0].type;
|
|
3717
|
-
if (firstType === "TextualBody" || firstType === "SpecificResource") {
|
|
3718
|
-
return firstType;
|
|
3719
|
-
}
|
|
3720
|
-
}
|
|
3721
|
-
return null;
|
|
3722
|
-
}
|
|
3723
|
-
if (typeof body === "object" && body !== null && "type" in body) {
|
|
3724
|
-
const bodyType = body.type;
|
|
3725
|
-
if (bodyType === "TextualBody" || bodyType === "SpecificResource") {
|
|
3726
|
-
return bodyType;
|
|
3727
|
-
}
|
|
3728
|
-
}
|
|
3729
|
-
return null;
|
|
3730
|
-
}
|
|
3731
|
-
function isBodyResolved(body) {
|
|
3732
|
-
return getBodySource(body) !== null;
|
|
3733
|
-
}
|
|
3734
|
-
function getTargetSource(target) {
|
|
3735
|
-
if (typeof target === "string") {
|
|
3736
|
-
return target;
|
|
3737
|
-
}
|
|
3738
|
-
return target.source;
|
|
3739
|
-
}
|
|
3740
|
-
function getTargetSelector(target) {
|
|
3741
|
-
if (typeof target === "string") {
|
|
3742
|
-
return void 0;
|
|
3743
|
-
}
|
|
3744
|
-
return target.selector;
|
|
3745
|
-
}
|
|
3746
|
-
function hasTargetSelector(target) {
|
|
3747
|
-
return typeof target !== "string" && target.selector !== void 0;
|
|
3748
|
-
}
|
|
3749
|
-
function isHighlight(annotation) {
|
|
3750
|
-
return annotation.motivation === "highlighting";
|
|
3751
|
-
}
|
|
3752
|
-
function isReference(annotation) {
|
|
3753
|
-
return annotation.motivation === "linking";
|
|
3754
|
-
}
|
|
3755
|
-
function isAssessment(annotation) {
|
|
3756
|
-
return annotation.motivation === "assessing";
|
|
3757
|
-
}
|
|
3758
|
-
function isComment(annotation) {
|
|
3759
|
-
return annotation.motivation === "commenting";
|
|
3760
|
-
}
|
|
3761
|
-
function isTag(annotation) {
|
|
3762
|
-
return annotation.motivation === "tagging";
|
|
3763
|
-
}
|
|
3764
|
-
function getCommentText(annotation) {
|
|
3765
|
-
if (!isComment(annotation)) return void 0;
|
|
3766
|
-
const body = Array.isArray(annotation.body) ? annotation.body[0] : annotation.body;
|
|
3767
|
-
if (body && "value" in body) {
|
|
3768
|
-
return body.value;
|
|
3769
|
-
}
|
|
3770
|
-
return void 0;
|
|
3771
|
-
}
|
|
3772
|
-
function isStubReference(annotation) {
|
|
3773
|
-
return isReference(annotation) && !isBodyResolved(annotation.body);
|
|
3774
|
-
}
|
|
3775
|
-
function isResolvedReference(annotation) {
|
|
3776
|
-
return isReference(annotation) && isBodyResolved(annotation.body);
|
|
3777
|
-
}
|
|
3778
|
-
function getExactText(selector) {
|
|
3779
|
-
if (!selector) {
|
|
3780
|
-
return "";
|
|
3781
|
-
}
|
|
3782
|
-
const selectors = Array.isArray(selector) ? selector : [selector];
|
|
3783
|
-
const quoteSelector = selectors.find((s) => s.type === "TextQuoteSelector");
|
|
3784
|
-
if (quoteSelector) {
|
|
3785
|
-
return quoteSelector.exact;
|
|
3786
|
-
}
|
|
3787
|
-
return "";
|
|
3788
|
-
}
|
|
3789
|
-
function getAnnotationExactText(annotation) {
|
|
3790
|
-
const selector = getTargetSelector(annotation.target);
|
|
3791
|
-
return getExactText(selector);
|
|
3792
|
-
}
|
|
3793
|
-
function getPrimarySelector(selector) {
|
|
3794
|
-
if (Array.isArray(selector)) {
|
|
3795
|
-
if (selector.length === 0) {
|
|
3796
|
-
throw new Error("Empty selector array");
|
|
3797
|
-
}
|
|
3798
|
-
const first = selector[0];
|
|
3799
|
-
if (!first) {
|
|
3800
|
-
throw new Error("Invalid selector array");
|
|
3801
|
-
}
|
|
3802
|
-
return first;
|
|
3803
|
-
}
|
|
3804
|
-
return selector;
|
|
3805
|
-
}
|
|
3806
|
-
function getTextQuoteSelector(selector) {
|
|
3807
|
-
const selectors = Array.isArray(selector) ? selector : [selector];
|
|
3808
|
-
const found = selectors.find((s) => s.type === "TextQuoteSelector");
|
|
3809
|
-
if (!found) return null;
|
|
3810
|
-
return found.type === "TextQuoteSelector" ? found : null;
|
|
3811
|
-
}
|
|
3812
|
-
function extractBoundingBox(svg) {
|
|
3813
|
-
const viewBoxMatch = svg.match(/<svg[^>]*viewBox="([^"]+)"/);
|
|
3814
|
-
if (viewBoxMatch) {
|
|
3815
|
-
const values = viewBoxMatch[1].split(/\s+/).map(parseFloat);
|
|
3816
|
-
if (values.length === 4 && values.every((v) => !isNaN(v))) {
|
|
3817
|
-
return {
|
|
3818
|
-
x: values[0],
|
|
3819
|
-
y: values[1],
|
|
3820
|
-
width: values[2],
|
|
3821
|
-
height: values[3]
|
|
3822
|
-
};
|
|
3823
|
-
}
|
|
3824
|
-
}
|
|
3825
|
-
const svgTagMatch = svg.match(/<svg[^>]*>/);
|
|
3826
|
-
if (svgTagMatch) {
|
|
3827
|
-
const svgTag = svgTagMatch[0];
|
|
3828
|
-
const widthMatch = svgTag.match(/width="([^"]+)"/);
|
|
3829
|
-
const heightMatch = svgTag.match(/height="([^"]+)"/);
|
|
3830
|
-
if (widthMatch && heightMatch) {
|
|
3831
|
-
const width = parseFloat(widthMatch[1]);
|
|
3832
|
-
const height = parseFloat(heightMatch[1]);
|
|
3833
|
-
if (!isNaN(width) && !isNaN(height)) {
|
|
3834
|
-
return { x: 0, y: 0, width, height };
|
|
3835
|
-
}
|
|
3836
|
-
}
|
|
3837
|
-
}
|
|
3838
|
-
return null;
|
|
3839
|
-
}
|
|
3840
|
-
|
|
3841
|
-
// src/view-models/pages/resource-viewer-page-vm.ts
|
|
3842
|
-
var WIZARD_CLOSED = {
|
|
3843
|
-
open: false,
|
|
3844
|
-
annotationId: null,
|
|
3845
|
-
resourceId: null,
|
|
3846
|
-
defaultTitle: "",
|
|
3847
|
-
entityTypes: []
|
|
3848
|
-
};
|
|
3849
|
-
function createResourceViewerPageVM(client, resourceId, locale, browse, options) {
|
|
3850
|
-
const disposer = createDisposer();
|
|
3851
|
-
const beckon = createBeckonVM(client);
|
|
3852
|
-
const mark = createMarkVM(client, resourceId);
|
|
3853
|
-
const gather = createGatherVM(client, resourceId);
|
|
3854
|
-
const matchVM = createMatchVM(client);
|
|
3855
|
-
const yieldVM = createYieldVM(client, resourceId, locale);
|
|
3856
|
-
disposer.add(beckon);
|
|
3857
|
-
disposer.add(browse);
|
|
3858
|
-
disposer.add(mark);
|
|
3859
|
-
disposer.add(gather);
|
|
3860
|
-
disposer.add(matchVM);
|
|
3861
|
-
disposer.add(yieldVM);
|
|
3862
|
-
const annotations$ = client.browse.annotations(resourceId).pipe(
|
|
3863
|
-
map$1((a) => a ?? [])
|
|
3864
|
-
);
|
|
3865
|
-
const annotationGroups$ = annotations$.pipe(
|
|
3866
|
-
map$1((anns) => {
|
|
3867
|
-
const groups = { highlights: [], comments: [], assessments: [], references: [], tags: [] };
|
|
3868
|
-
for (const ann of anns) {
|
|
3869
|
-
if (isHighlight(ann)) groups.highlights.push(ann);
|
|
3870
|
-
else if (isComment(ann)) groups.comments.push(ann);
|
|
3871
|
-
else if (isAssessment(ann)) groups.assessments.push(ann);
|
|
3872
|
-
else if (isReference(ann)) groups.references.push(ann);
|
|
3873
|
-
else if (isTag(ann)) groups.tags.push(ann);
|
|
3874
|
-
}
|
|
3875
|
-
return groups;
|
|
3876
|
-
})
|
|
3877
|
-
);
|
|
3878
|
-
const entityTypes$ = client.browse.entityTypes().pipe(
|
|
3879
|
-
map$1((e) => e ?? [])
|
|
3880
|
-
);
|
|
3881
|
-
const events$ = client.browse.events(resourceId).pipe(
|
|
3882
|
-
map$1((e) => e ?? [])
|
|
3883
|
-
);
|
|
3884
|
-
const referencedBy$ = client.browse.referencedBy(resourceId).pipe(
|
|
3885
|
-
map$1((r) => r ?? [])
|
|
3886
|
-
);
|
|
3887
|
-
const content$ = new BehaviorSubject("");
|
|
3888
|
-
const contentLoading$ = new BehaviorSubject(false);
|
|
3889
|
-
const mediaToken$ = new BehaviorSubject(null);
|
|
3890
|
-
const mediaType = options?.mediaType || "text/plain";
|
|
3891
|
-
const isBinaryType = mediaType.startsWith("image/") || mediaType === "application/pdf";
|
|
3892
|
-
if (!isBinaryType && mediaType) {
|
|
3893
|
-
contentLoading$.next(true);
|
|
3894
|
-
client.browse.resourceRepresentation(resourceId, { accept: mediaType }).then(({ data }) => {
|
|
3895
|
-
content$.next(decodeWithCharset(data, mediaType));
|
|
3896
|
-
contentLoading$.next(false);
|
|
3897
|
-
}).catch(() => {
|
|
3898
|
-
contentLoading$.next(false);
|
|
3899
|
-
});
|
|
3900
|
-
}
|
|
3901
|
-
if (isBinaryType) {
|
|
3902
|
-
client.auth.mediaToken(resourceId).then(({ token }) => mediaToken$.next(token)).catch(() => {
|
|
3903
|
-
});
|
|
3904
|
-
}
|
|
3905
|
-
const wizard$ = new BehaviorSubject(WIZARD_CLOSED);
|
|
3906
|
-
const unsubscribeResource = client.subscribeToResource(resourceId);
|
|
3907
|
-
disposer.add(unsubscribeResource);
|
|
3908
|
-
const bindInitiateSub = client.stream("bind:initiate").subscribe((event) => {
|
|
3909
|
-
wizard$.next({
|
|
3910
|
-
open: true,
|
|
3911
|
-
annotationId: event.annotationId,
|
|
3912
|
-
resourceId: event.resourceId,
|
|
3913
|
-
defaultTitle: event.defaultTitle,
|
|
3914
|
-
entityTypes: event.entityTypes
|
|
3915
|
-
});
|
|
3916
|
-
client.emit("gather:requested", {
|
|
3917
|
-
correlationId: crypto.randomUUID(),
|
|
3918
|
-
annotationId: event.annotationId,
|
|
3919
|
-
resourceId: event.resourceId,
|
|
3920
|
-
options: { contextWindow: 2e3 }
|
|
3921
|
-
});
|
|
3922
|
-
});
|
|
3923
|
-
disposer.add(() => bindInitiateSub.unsubscribe());
|
|
3924
|
-
return {
|
|
3925
|
-
beckon,
|
|
3926
|
-
browse,
|
|
3927
|
-
mark,
|
|
3928
|
-
gather,
|
|
3929
|
-
yield: yieldVM,
|
|
3930
|
-
annotations$,
|
|
3931
|
-
annotationGroups$,
|
|
3932
|
-
entityTypes$,
|
|
3933
|
-
events$,
|
|
3934
|
-
referencedBy$,
|
|
3935
|
-
content$: content$.asObservable(),
|
|
3936
|
-
contentLoading$: contentLoading$.asObservable(),
|
|
3937
|
-
mediaToken$: mediaToken$.asObservable(),
|
|
3938
|
-
wizard$: wizard$.asObservable(),
|
|
3939
|
-
closeWizard: () => wizard$.next(WIZARD_CLOSED),
|
|
3940
|
-
dispose: () => {
|
|
3941
|
-
wizard$.complete();
|
|
3942
|
-
content$.complete();
|
|
3943
|
-
contentLoading$.complete();
|
|
3944
|
-
mediaToken$.complete();
|
|
3945
|
-
disposer.dispose();
|
|
3946
|
-
}
|
|
3947
|
-
};
|
|
3948
|
-
}
|
|
3949
|
-
|
|
3950
|
-
// src/utils/fuzzy-anchor.ts
|
|
3951
|
-
function normalizeText(text) {
|
|
3952
|
-
return text.replace(/\s+/g, " ").replace(/[\u2018\u2019]/g, "'").replace(/[\u201C\u201D]/g, '"').replace(/\u2014/g, "--").replace(/\u2013/g, "-").trim();
|
|
3953
|
-
}
|
|
3954
|
-
function levenshteinDistance(str1, str2) {
|
|
3955
|
-
const len1 = str1.length;
|
|
3956
|
-
const len2 = str2.length;
|
|
3957
|
-
const matrix = [];
|
|
3958
|
-
for (let i = 0; i <= len1; i++) {
|
|
3959
|
-
matrix[i] = [i];
|
|
3960
|
-
}
|
|
3961
|
-
for (let j = 0; j <= len2; j++) {
|
|
3962
|
-
matrix[0][j] = j;
|
|
3963
|
-
}
|
|
3964
|
-
for (let i = 1; i <= len1; i++) {
|
|
3965
|
-
for (let j = 1; j <= len2; j++) {
|
|
3966
|
-
const cost = str1[i - 1] === str2[j - 1] ? 0 : 1;
|
|
3967
|
-
const deletion = matrix[i - 1][j] + 1;
|
|
3968
|
-
const insertion = matrix[i][j - 1] + 1;
|
|
3969
|
-
const substitution = matrix[i - 1][j - 1] + cost;
|
|
3970
|
-
matrix[i][j] = Math.min(deletion, insertion, substitution);
|
|
3971
|
-
}
|
|
3972
|
-
}
|
|
3973
|
-
return matrix[len1][len2];
|
|
3974
|
-
}
|
|
3975
|
-
function buildContentCache(content) {
|
|
3976
|
-
return {
|
|
3977
|
-
normalizedContent: normalizeText(content),
|
|
3978
|
-
lowerContent: content.toLowerCase()
|
|
3979
|
-
};
|
|
3980
|
-
}
|
|
3981
|
-
function findBestTextMatch(content, searchText, positionHint, cache) {
|
|
3982
|
-
const maxFuzzyDistance = Math.max(5, Math.floor(searchText.length * 0.05));
|
|
3983
|
-
const exactIndex = content.indexOf(searchText);
|
|
3984
|
-
if (exactIndex !== -1) {
|
|
3985
|
-
return {
|
|
3986
|
-
start: exactIndex,
|
|
3987
|
-
end: exactIndex + searchText.length,
|
|
3988
|
-
matchQuality: "exact"
|
|
3989
|
-
};
|
|
3990
|
-
}
|
|
3991
|
-
const normalizedSearch = normalizeText(searchText);
|
|
3992
|
-
const normalizedIndex = cache.normalizedContent.indexOf(normalizedSearch);
|
|
3993
|
-
if (normalizedIndex !== -1) {
|
|
3994
|
-
let actualPos = 0;
|
|
3995
|
-
let normalizedPos = 0;
|
|
3996
|
-
while (normalizedPos < normalizedIndex && actualPos < content.length) {
|
|
3997
|
-
const char = content[actualPos];
|
|
3998
|
-
const normalizedChar = normalizeText(char);
|
|
3999
|
-
if (normalizedChar) {
|
|
4000
|
-
normalizedPos += normalizedChar.length;
|
|
4001
|
-
}
|
|
4002
|
-
actualPos++;
|
|
4003
|
-
}
|
|
4004
|
-
return {
|
|
4005
|
-
start: actualPos,
|
|
4006
|
-
end: actualPos + searchText.length,
|
|
4007
|
-
matchQuality: "normalized"
|
|
4008
|
-
};
|
|
4009
|
-
}
|
|
4010
|
-
const lowerSearch = searchText.toLowerCase();
|
|
4011
|
-
const caseInsensitiveIndex = cache.lowerContent.indexOf(lowerSearch);
|
|
4012
|
-
if (caseInsensitiveIndex !== -1) {
|
|
4013
|
-
return {
|
|
4014
|
-
start: caseInsensitiveIndex,
|
|
4015
|
-
end: caseInsensitiveIndex + searchText.length,
|
|
4016
|
-
matchQuality: "case-insensitive"
|
|
4017
|
-
};
|
|
4018
|
-
}
|
|
4019
|
-
const windowSize = searchText.length;
|
|
4020
|
-
const searchRadius = Math.min(500, content.length);
|
|
4021
|
-
const searchStart = positionHint !== void 0 ? Math.max(0, positionHint - searchRadius) : 0;
|
|
4022
|
-
const searchEnd = positionHint !== void 0 ? Math.min(content.length, positionHint + searchRadius) : content.length;
|
|
4023
|
-
let bestMatch = null;
|
|
4024
|
-
for (let i = searchStart; i <= searchEnd - windowSize; i++) {
|
|
4025
|
-
const candidate = content.substring(i, i + windowSize);
|
|
4026
|
-
const distance = levenshteinDistance(searchText, candidate);
|
|
4027
|
-
if (distance <= maxFuzzyDistance) {
|
|
4028
|
-
if (!bestMatch || distance < bestMatch.distance) {
|
|
4029
|
-
bestMatch = { start: i, distance };
|
|
4030
|
-
}
|
|
4031
|
-
}
|
|
4032
|
-
}
|
|
4033
|
-
if (bestMatch) {
|
|
4034
|
-
return {
|
|
4035
|
-
start: bestMatch.start,
|
|
4036
|
-
end: bestMatch.start + windowSize,
|
|
4037
|
-
matchQuality: "fuzzy"
|
|
4038
|
-
};
|
|
4039
|
-
}
|
|
4040
|
-
return null;
|
|
4041
|
-
}
|
|
4042
|
-
function findTextWithContext(content, exact, prefix, suffix, positionHint, cache) {
|
|
4043
|
-
if (!exact) return null;
|
|
4044
|
-
if (positionHint !== void 0 && positionHint >= 0 && positionHint + exact.length <= content.length) {
|
|
4045
|
-
if (content.substring(positionHint, positionHint + exact.length) === exact) {
|
|
4046
|
-
return { start: positionHint, end: positionHint + exact.length };
|
|
4047
|
-
}
|
|
4048
|
-
}
|
|
4049
|
-
const occurrences = [];
|
|
4050
|
-
let index = content.indexOf(exact);
|
|
4051
|
-
while (index !== -1) {
|
|
4052
|
-
occurrences.push(index);
|
|
4053
|
-
index = content.indexOf(exact, index + 1);
|
|
4054
|
-
}
|
|
4055
|
-
if (occurrences.length === 0) {
|
|
4056
|
-
const fuzzyMatch = findBestTextMatch(content, exact, positionHint, cache);
|
|
4057
|
-
if (fuzzyMatch) {
|
|
4058
|
-
return { start: fuzzyMatch.start, end: fuzzyMatch.end };
|
|
4059
|
-
}
|
|
4060
|
-
return null;
|
|
4061
|
-
}
|
|
4062
|
-
if (occurrences.length === 1) {
|
|
4063
|
-
const pos2 = occurrences[0];
|
|
4064
|
-
return { start: pos2, end: pos2 + exact.length };
|
|
4065
|
-
}
|
|
4066
|
-
if (prefix || suffix) {
|
|
4067
|
-
for (const pos2 of occurrences) {
|
|
4068
|
-
const actualPrefixStart = Math.max(0, pos2 - (prefix?.length || 0));
|
|
4069
|
-
const actualPrefix = content.substring(actualPrefixStart, pos2);
|
|
4070
|
-
const actualSuffixEnd = Math.min(content.length, pos2 + exact.length + (suffix?.length || 0));
|
|
4071
|
-
const actualSuffix = content.substring(pos2 + exact.length, actualSuffixEnd);
|
|
4072
|
-
const prefixMatch = !prefix || actualPrefix.endsWith(prefix);
|
|
4073
|
-
const suffixMatch = !suffix || actualSuffix.startsWith(suffix);
|
|
4074
|
-
if (prefixMatch && suffixMatch) {
|
|
4075
|
-
return { start: pos2, end: pos2 + exact.length };
|
|
4076
|
-
}
|
|
4077
|
-
}
|
|
4078
|
-
for (const pos2 of occurrences) {
|
|
4079
|
-
const actualPrefix = content.substring(Math.max(0, pos2 - (prefix?.length || 0)), pos2);
|
|
4080
|
-
const actualSuffix = content.substring(pos2 + exact.length, pos2 + exact.length + (suffix?.length || 0));
|
|
4081
|
-
const fuzzyPrefixMatch = !prefix || actualPrefix.includes(prefix.trim());
|
|
4082
|
-
const fuzzySuffixMatch = !suffix || actualSuffix.includes(suffix.trim());
|
|
4083
|
-
if (fuzzyPrefixMatch && fuzzySuffixMatch) {
|
|
4084
|
-
return { start: pos2, end: pos2 + exact.length };
|
|
4085
|
-
}
|
|
4086
|
-
}
|
|
4087
|
-
}
|
|
4088
|
-
const pos = occurrences[0];
|
|
4089
|
-
return { start: pos, end: pos + exact.length };
|
|
4090
|
-
}
|
|
4091
|
-
function verifyPosition(content, position, expectedExact) {
|
|
4092
|
-
const actualText = content.substring(position.start, position.end);
|
|
4093
|
-
return actualText === expectedExact;
|
|
4094
|
-
}
|
|
4095
|
-
|
|
4096
|
-
// src/utils/locales.ts
|
|
4097
|
-
var LOCALES = [
|
|
4098
|
-
{ code: "ar", nativeName: "\u0627\u0644\u0639\u0631\u0628\u064A\u0629", englishName: "Arabic" },
|
|
4099
|
-
{ code: "bn", nativeName: "\u09AC\u09BE\u0982\u09B2\u09BE", englishName: "Bengali" },
|
|
4100
|
-
{ code: "cs", nativeName: "\u010Ce\u0161tina", englishName: "Czech" },
|
|
4101
|
-
{ code: "da", nativeName: "Dansk", englishName: "Danish" },
|
|
4102
|
-
{ code: "de", nativeName: "Deutsch", englishName: "German" },
|
|
4103
|
-
{ code: "el", nativeName: "\u0395\u03BB\u03BB\u03B7\u03BD\u03B9\u03BA\u03AC", englishName: "Greek" },
|
|
4104
|
-
{ code: "en", nativeName: "English", englishName: "English" },
|
|
4105
|
-
{ code: "es", nativeName: "Espa\xF1ol", englishName: "Spanish" },
|
|
4106
|
-
{ code: "fa", nativeName: "\u0641\u0627\u0631\u0633\u06CC", englishName: "Persian" },
|
|
4107
|
-
{ code: "fi", nativeName: "Suomi", englishName: "Finnish" },
|
|
4108
|
-
{ code: "fr", nativeName: "Fran\xE7ais", englishName: "French" },
|
|
4109
|
-
{ code: "he", nativeName: "\u05E2\u05D1\u05E8\u05D9\u05EA", englishName: "Hebrew" },
|
|
4110
|
-
{ code: "hi", nativeName: "\u0939\u093F\u0928\u094D\u0926\u0940", englishName: "Hindi" },
|
|
4111
|
-
{ code: "id", nativeName: "Bahasa Indonesia", englishName: "Indonesian" },
|
|
4112
|
-
{ code: "it", nativeName: "Italiano", englishName: "Italian" },
|
|
4113
|
-
{ code: "ja", nativeName: "\u65E5\u672C\u8A9E", englishName: "Japanese" },
|
|
4114
|
-
{ code: "ko", nativeName: "\uD55C\uAD6D\uC5B4", englishName: "Korean" },
|
|
4115
|
-
{ code: "ms", nativeName: "Bahasa Melayu", englishName: "Malay" },
|
|
4116
|
-
{ code: "nl", nativeName: "Nederlands", englishName: "Dutch" },
|
|
4117
|
-
{ code: "no", nativeName: "Norsk", englishName: "Norwegian" },
|
|
4118
|
-
{ code: "pl", nativeName: "Polski", englishName: "Polish" },
|
|
4119
|
-
{ code: "pt", nativeName: "Portugu\xEAs", englishName: "Portuguese" },
|
|
4120
|
-
{ code: "ro", nativeName: "Rom\xE2n\u0103", englishName: "Romanian" },
|
|
4121
|
-
{ code: "sv", nativeName: "Svenska", englishName: "Swedish" },
|
|
4122
|
-
{ code: "th", nativeName: "\u0E44\u0E17\u0E22", englishName: "Thai" },
|
|
4123
|
-
{ code: "tr", nativeName: "T\xFCrk\xE7e", englishName: "Turkish" },
|
|
4124
|
-
{ code: "uk", nativeName: "\u0423\u043A\u0440\u0430\u0457\u043D\u0441\u044C\u043A\u0430", englishName: "Ukrainian" },
|
|
4125
|
-
{ code: "vi", nativeName: "Ti\u1EBFng Vi\u1EC7t", englishName: "Vietnamese" },
|
|
4126
|
-
{ code: "zh", nativeName: "\u4E2D\u6587", englishName: "Chinese" }
|
|
4127
|
-
];
|
|
4128
|
-
var localeByCode = new Map(
|
|
4129
|
-
LOCALES.map((locale) => [locale.code.toLowerCase(), locale])
|
|
4130
|
-
);
|
|
4131
|
-
function getLocaleInfo(code) {
|
|
4132
|
-
if (!code) return void 0;
|
|
4133
|
-
return localeByCode.get(code.toLowerCase());
|
|
4134
|
-
}
|
|
4135
|
-
function getLocaleNativeName(code) {
|
|
4136
|
-
return getLocaleInfo(code)?.nativeName;
|
|
4137
|
-
}
|
|
4138
|
-
function getLocaleEnglishName(code) {
|
|
4139
|
-
return getLocaleInfo(code)?.englishName;
|
|
4140
|
-
}
|
|
4141
|
-
function formatLocaleDisplay(code) {
|
|
4142
|
-
if (!code) return void 0;
|
|
4143
|
-
const info = getLocaleInfo(code);
|
|
4144
|
-
if (!info) return code;
|
|
4145
|
-
return `${info.nativeName} (${code.toLowerCase()})`;
|
|
4146
|
-
}
|
|
4147
|
-
function getAllLocaleCodes() {
|
|
4148
|
-
return LOCALES.map((l) => l.code);
|
|
4149
|
-
}
|
|
4150
|
-
|
|
4151
|
-
// src/utils/resources.ts
|
|
4152
|
-
function getResourceId(resource) {
|
|
4153
|
-
if (!resource) return void 0;
|
|
4154
|
-
return resource["@id"] || void 0;
|
|
4155
|
-
}
|
|
4156
|
-
function getPrimaryRepresentation(resource) {
|
|
4157
|
-
if (!resource?.representations) return void 0;
|
|
4158
|
-
const reps = Array.isArray(resource.representations) ? resource.representations : [resource.representations];
|
|
4159
|
-
return reps[0];
|
|
4160
|
-
}
|
|
4161
|
-
function getPrimaryMediaType(resource) {
|
|
4162
|
-
return getPrimaryRepresentation(resource)?.mediaType;
|
|
4163
|
-
}
|
|
4164
|
-
function getChecksum(resource) {
|
|
4165
|
-
return getPrimaryRepresentation(resource)?.checksum;
|
|
4166
|
-
}
|
|
4167
|
-
function getLanguage(resource) {
|
|
4168
|
-
return getPrimaryRepresentation(resource)?.language;
|
|
4169
|
-
}
|
|
4170
|
-
function getStorageUri(resource) {
|
|
4171
|
-
return getPrimaryRepresentation(resource)?.storageUri;
|
|
4172
|
-
}
|
|
4173
|
-
function getCreator(resource) {
|
|
4174
|
-
if (!resource?.wasAttributedTo) return void 0;
|
|
4175
|
-
return Array.isArray(resource.wasAttributedTo) ? resource.wasAttributedTo[0] : resource.wasAttributedTo;
|
|
4176
|
-
}
|
|
4177
|
-
function getDerivedFrom(resource) {
|
|
4178
|
-
if (!resource?.wasDerivedFrom) return void 0;
|
|
4179
|
-
return Array.isArray(resource.wasDerivedFrom) ? resource.wasDerivedFrom[0] : resource.wasDerivedFrom;
|
|
4180
|
-
}
|
|
4181
|
-
function isArchived(resource) {
|
|
4182
|
-
return resource?.archived === true;
|
|
4183
|
-
}
|
|
4184
|
-
function getResourceEntityTypes(resource) {
|
|
4185
|
-
return resource?.entityTypes || [];
|
|
4186
|
-
}
|
|
4187
|
-
function isDraft(resource) {
|
|
4188
|
-
return resource?.isDraft === true;
|
|
4189
|
-
}
|
|
4190
|
-
function getNodeEncoding(charset) {
|
|
4191
|
-
const normalized = charset.toLowerCase().replace(/[-_]/g, "");
|
|
4192
|
-
const charsetMap = {
|
|
4193
|
-
"utf8": "utf8",
|
|
4194
|
-
"iso88591": "latin1",
|
|
4195
|
-
"latin1": "latin1",
|
|
4196
|
-
"ascii": "ascii",
|
|
4197
|
-
"usascii": "ascii",
|
|
4198
|
-
"utf16le": "utf16le",
|
|
4199
|
-
"ucs2": "ucs2",
|
|
4200
|
-
"binary": "binary",
|
|
4201
|
-
"windows1252": "latin1",
|
|
4202
|
-
// Windows-1252 is a superset of Latin-1
|
|
4203
|
-
"cp1252": "latin1"
|
|
4204
|
-
};
|
|
4205
|
-
return charsetMap[normalized] || "utf8";
|
|
4206
|
-
}
|
|
4207
|
-
function decodeRepresentation(buffer, mediaType) {
|
|
4208
|
-
const charsetMatch = mediaType.match(/charset=([^\s;]+)/i);
|
|
4209
|
-
const charset = (charsetMatch?.[1] || "utf-8").toLowerCase();
|
|
4210
|
-
const encoding = getNodeEncoding(charset);
|
|
4211
|
-
return buffer.toString(encoding);
|
|
4212
|
-
}
|
|
4213
|
-
|
|
4214
|
-
// src/utils/svg-utils.ts
|
|
4215
|
-
function createRectangleSvg(start, end) {
|
|
4216
|
-
const x = Math.min(start.x, end.x);
|
|
4217
|
-
const y = Math.min(start.y, end.y);
|
|
4218
|
-
const width = Math.abs(end.x - start.x);
|
|
4219
|
-
const height = Math.abs(end.y - start.y);
|
|
4220
|
-
return `<svg xmlns="http://www.w3.org/2000/svg"><rect x="${x}" y="${y}" width="${width}" height="${height}"/></svg>`;
|
|
4221
|
-
}
|
|
4222
|
-
function createPolygonSvg(points) {
|
|
4223
|
-
if (points.length < 3) {
|
|
4224
|
-
throw new Error("Polygon requires at least 3 points");
|
|
4225
|
-
}
|
|
4226
|
-
const pointsStr = points.map((p) => `${p.x},${p.y}`).join(" ");
|
|
4227
|
-
return `<svg xmlns="http://www.w3.org/2000/svg"><polygon points="${pointsStr}"/></svg>`;
|
|
4228
|
-
}
|
|
4229
|
-
function createCircleSvg(center, radius) {
|
|
4230
|
-
if (radius <= 0) {
|
|
4231
|
-
throw new Error("Circle radius must be positive");
|
|
4232
|
-
}
|
|
4233
|
-
return `<svg xmlns="http://www.w3.org/2000/svg"><circle cx="${center.x}" cy="${center.y}" r="${radius}"/></svg>`;
|
|
4234
|
-
}
|
|
4235
|
-
function parseSvgSelector(svg) {
|
|
4236
|
-
const rectMatch = svg.match(/<rect\s+([^>]+)\/>/);
|
|
4237
|
-
if (rectMatch && rectMatch[1]) {
|
|
4238
|
-
const attrs = rectMatch[1];
|
|
4239
|
-
const x = parseFloat(attrs.match(/x="([^"]+)"/)?.[1] || "0");
|
|
4240
|
-
const y = parseFloat(attrs.match(/y="([^"]+)"/)?.[1] || "0");
|
|
4241
|
-
const width = parseFloat(attrs.match(/width="([^"]+)"/)?.[1] || "0");
|
|
4242
|
-
const height = parseFloat(attrs.match(/height="([^"]+)"/)?.[1] || "0");
|
|
4243
|
-
return {
|
|
4244
|
-
type: "rect",
|
|
4245
|
-
data: { x, y, width, height }
|
|
4246
|
-
};
|
|
4247
|
-
}
|
|
4248
|
-
const polygonMatch = svg.match(/<polygon\s+points="([^"]+)"/);
|
|
4249
|
-
if (polygonMatch && polygonMatch[1]) {
|
|
4250
|
-
const pointsStr = polygonMatch[1];
|
|
4251
|
-
const points = pointsStr.split(/\s+/).map((pair) => {
|
|
4252
|
-
const [x, y] = pair.split(",").map(parseFloat);
|
|
4253
|
-
return { x, y };
|
|
4254
|
-
});
|
|
4255
|
-
return {
|
|
4256
|
-
type: "polygon",
|
|
4257
|
-
data: { points }
|
|
4258
|
-
};
|
|
4259
|
-
}
|
|
4260
|
-
const circleMatch = svg.match(/<circle\s+([^>]+)\/>/);
|
|
4261
|
-
if (circleMatch && circleMatch[1]) {
|
|
4262
|
-
const attrs = circleMatch[1];
|
|
4263
|
-
const cx = parseFloat(attrs.match(/cx="([^"]+)"/)?.[1] || "0");
|
|
4264
|
-
const cy = parseFloat(attrs.match(/cy="([^"]+)"/)?.[1] || "0");
|
|
4265
|
-
const r = parseFloat(attrs.match(/r="([^"]+)"/)?.[1] || "0");
|
|
4266
|
-
return {
|
|
4267
|
-
type: "circle",
|
|
4268
|
-
data: { cx, cy, r }
|
|
4269
|
-
};
|
|
4270
|
-
}
|
|
4271
|
-
return null;
|
|
4272
|
-
}
|
|
4273
|
-
function normalizeCoordinates(point, displayWidth, displayHeight, imageWidth, imageHeight) {
|
|
4274
|
-
return {
|
|
4275
|
-
x: point.x / displayWidth * imageWidth,
|
|
4276
|
-
y: point.y / displayHeight * imageHeight
|
|
4277
|
-
};
|
|
4278
|
-
}
|
|
4279
|
-
function scaleSvgToNative(svg, displayWidth, displayHeight, imageWidth, imageHeight) {
|
|
4280
|
-
const parsed = parseSvgSelector(svg);
|
|
4281
|
-
if (!parsed) return svg;
|
|
4282
|
-
const scaleX = imageWidth / displayWidth;
|
|
4283
|
-
const scaleY = imageHeight / displayHeight;
|
|
4284
|
-
switch (parsed.type) {
|
|
4285
|
-
case "rect": {
|
|
4286
|
-
const { x, y, width, height } = parsed.data;
|
|
4287
|
-
return createRectangleSvg(
|
|
4288
|
-
{ x: x * scaleX, y: y * scaleY },
|
|
4289
|
-
{ x: (x + width) * scaleX, y: (y + height) * scaleY }
|
|
4290
|
-
);
|
|
4291
|
-
}
|
|
4292
|
-
case "circle": {
|
|
4293
|
-
const { cx, cy, r } = parsed.data;
|
|
4294
|
-
return createCircleSvg(
|
|
4295
|
-
{ x: cx * scaleX, y: cy * scaleY },
|
|
4296
|
-
r * Math.min(scaleX, scaleY)
|
|
4297
|
-
);
|
|
4298
|
-
}
|
|
4299
|
-
case "polygon": {
|
|
4300
|
-
const points = parsed.data.points.map((p) => ({
|
|
4301
|
-
x: p.x * scaleX,
|
|
4302
|
-
y: p.y * scaleY
|
|
4303
|
-
}));
|
|
4304
|
-
return createPolygonSvg(points);
|
|
4305
|
-
}
|
|
4306
|
-
}
|
|
4307
|
-
return svg;
|
|
4308
|
-
}
|
|
4309
|
-
|
|
4310
|
-
// src/utils/text-context.ts
|
|
4311
|
-
function extractContext(content, start, end) {
|
|
4312
|
-
const CONTEXT_LENGTH = 64;
|
|
4313
|
-
const MAX_EXTENSION = 32;
|
|
4314
|
-
let prefix;
|
|
4315
|
-
if (start > 0) {
|
|
4316
|
-
let prefixStart = Math.max(0, start - CONTEXT_LENGTH);
|
|
4317
|
-
let extensionCount = 0;
|
|
4318
|
-
while (prefixStart > 0 && extensionCount < MAX_EXTENSION) {
|
|
4319
|
-
const char = content[prefixStart - 1];
|
|
4320
|
-
if (!char || /[\s.,;:!?'"()\[\]{}<>\/\\]/.test(char)) {
|
|
4321
|
-
break;
|
|
4322
|
-
}
|
|
4323
|
-
prefixStart--;
|
|
4324
|
-
extensionCount++;
|
|
4325
|
-
}
|
|
4326
|
-
prefix = content.substring(prefixStart, start);
|
|
4327
|
-
}
|
|
4328
|
-
let suffix;
|
|
4329
|
-
if (end < content.length) {
|
|
4330
|
-
let suffixEnd = Math.min(content.length, end + CONTEXT_LENGTH);
|
|
4331
|
-
let extensionCount = 0;
|
|
4332
|
-
while (suffixEnd < content.length && extensionCount < MAX_EXTENSION) {
|
|
4333
|
-
const char = content[suffixEnd];
|
|
4334
|
-
if (!char || /[\s.,;:!?'"()\[\]{}<>\/\\]/.test(char)) {
|
|
4335
|
-
break;
|
|
4336
|
-
}
|
|
4337
|
-
suffixEnd++;
|
|
4338
|
-
extensionCount++;
|
|
4339
|
-
}
|
|
4340
|
-
suffix = content.substring(end, suffixEnd);
|
|
4341
|
-
}
|
|
4342
|
-
return { prefix, suffix };
|
|
4343
|
-
}
|
|
4344
|
-
function validateAndCorrectOffsets(content, aiStart, aiEnd, exact) {
|
|
4345
|
-
const textAtOffset = content.substring(aiStart, aiEnd);
|
|
4346
|
-
if (textAtOffset === exact) {
|
|
4347
|
-
const context2 = extractContext(content, aiStart, aiEnd);
|
|
4348
|
-
return {
|
|
4349
|
-
start: aiStart,
|
|
4350
|
-
end: aiEnd,
|
|
4351
|
-
exact,
|
|
4352
|
-
prefix: context2.prefix,
|
|
4353
|
-
suffix: context2.suffix,
|
|
4354
|
-
corrected: false,
|
|
4355
|
-
matchQuality: "exact"
|
|
4356
|
-
};
|
|
4357
|
-
}
|
|
4358
|
-
const cache = buildContentCache(content);
|
|
4359
|
-
const match = findBestTextMatch(content, exact, aiStart, cache);
|
|
4360
|
-
if (!match) {
|
|
4361
|
-
throw new Error(
|
|
4362
|
-
"Cannot find acceptable match for text in content. All search strategies failed. Text may be hallucinated."
|
|
4363
|
-
);
|
|
4364
|
-
}
|
|
4365
|
-
const actualText = content.substring(match.start, match.end);
|
|
4366
|
-
const context = extractContext(content, match.start, match.end);
|
|
4367
|
-
return {
|
|
4368
|
-
start: match.start,
|
|
4369
|
-
end: match.end,
|
|
4370
|
-
exact: actualText,
|
|
4371
|
-
// Use actual text from document, not AI's version
|
|
4372
|
-
prefix: context.prefix,
|
|
4373
|
-
suffix: context.suffix,
|
|
4374
|
-
corrected: true,
|
|
4375
|
-
fuzzyMatched: match.matchQuality !== "exact",
|
|
4376
|
-
matchQuality: match.matchQuality
|
|
4377
|
-
};
|
|
4378
|
-
}
|
|
4379
|
-
|
|
4380
|
-
// src/utils/validation.ts
|
|
4381
|
-
var JWTTokenSchema = {
|
|
4382
|
-
parse(token) {
|
|
4383
|
-
if (typeof token !== "string") {
|
|
4384
|
-
throw new Error("Token must be a string");
|
|
4385
|
-
}
|
|
4386
|
-
if (!token || token.length === 0) {
|
|
4387
|
-
throw new Error("Token is required");
|
|
4388
|
-
}
|
|
4389
|
-
const jwtRegex = /^[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]*$/;
|
|
4390
|
-
if (!jwtRegex.test(token)) {
|
|
4391
|
-
throw new Error("Invalid JWT token format");
|
|
4392
|
-
}
|
|
4393
|
-
return token;
|
|
4394
|
-
},
|
|
4395
|
-
safeParse(token) {
|
|
4396
|
-
try {
|
|
4397
|
-
const validated = this.parse(token);
|
|
4398
|
-
return { success: true, data: validated };
|
|
4399
|
-
} catch (error) {
|
|
4400
|
-
return {
|
|
4401
|
-
success: false,
|
|
4402
|
-
error: error instanceof Error ? error.message : "Invalid JWT token"
|
|
4403
|
-
};
|
|
4404
|
-
}
|
|
4405
|
-
}
|
|
4406
|
-
};
|
|
4407
|
-
function validateData(schema, data) {
|
|
4408
|
-
try {
|
|
4409
|
-
const validated = schema.parse(data);
|
|
4410
|
-
return { success: true, data: validated };
|
|
4411
|
-
} catch (error) {
|
|
4412
|
-
return {
|
|
4413
|
-
success: false,
|
|
4414
|
-
error: error instanceof Error ? error.message : "Validation failed"
|
|
4415
|
-
};
|
|
4416
|
-
}
|
|
4417
|
-
}
|
|
4418
|
-
function isValidEmail(email) {
|
|
4419
|
-
if (email.length < 1 || email.length > 255) {
|
|
4420
|
-
return false;
|
|
4421
|
-
}
|
|
4422
|
-
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
4423
|
-
return emailRegex.test(email);
|
|
4424
|
-
}
|
|
4425
|
-
|
|
4426
|
-
// src/view-models/pages/compose-page-vm.ts
|
|
4427
|
-
function createComposePageVM(client, browse, params, auth) {
|
|
4428
|
-
const disposer = createDisposer();
|
|
4429
|
-
disposer.add(browse);
|
|
4430
|
-
const isReferenceMode = Boolean(params.annotationUri && params.sourceDocumentId && params.name);
|
|
4431
|
-
const isCloneMode = params.mode === "clone" && Boolean(params.token);
|
|
4432
|
-
const pageMode = isCloneMode ? "clone" : isReferenceMode ? "reference" : "new";
|
|
4433
|
-
const mode$ = new BehaviorSubject(pageMode);
|
|
4434
|
-
const loading$ = new BehaviorSubject(true);
|
|
4435
|
-
const cloneData$ = new BehaviorSubject(null);
|
|
4436
|
-
const referenceData$ = new BehaviorSubject(null);
|
|
4437
|
-
const gatheredContext$ = new BehaviorSubject(null);
|
|
4438
|
-
const entityTypes$ = client.browse.entityTypes().pipe(
|
|
4439
|
-
map$1((e) => e ?? [])
|
|
4440
|
-
);
|
|
4441
|
-
if (isReferenceMode) {
|
|
4442
|
-
const entityTypes = params.entityTypes ? params.entityTypes.split(",") : [];
|
|
4443
|
-
referenceData$.next({
|
|
4444
|
-
annotationUri: params.annotationUri,
|
|
4445
|
-
sourceDocumentId: params.sourceDocumentId,
|
|
4446
|
-
name: params.name,
|
|
4447
|
-
entityTypes
|
|
4448
|
-
});
|
|
4449
|
-
if (params.storedContext) {
|
|
4450
|
-
try {
|
|
4451
|
-
gatheredContext$.next(JSON.parse(params.storedContext));
|
|
4452
|
-
} catch {
|
|
4453
|
-
}
|
|
4454
|
-
}
|
|
4455
|
-
loading$.next(false);
|
|
4456
|
-
} else if (isCloneMode) {
|
|
4457
|
-
void (async () => {
|
|
4458
|
-
try {
|
|
4459
|
-
const tokenResult = await client.yield.fromToken(params.token);
|
|
4460
|
-
if (tokenResult && auth) {
|
|
4461
|
-
const rId = resourceId(tokenResult["@id"]);
|
|
4462
|
-
const mediaType = getPrimaryMediaType(tokenResult) || "text/plain";
|
|
4463
|
-
const { data } = await client.getResourceRepresentation(rId, {
|
|
4464
|
-
accept: mediaType,
|
|
4465
|
-
auth
|
|
4466
|
-
});
|
|
4467
|
-
const content = decodeWithCharset(data, mediaType);
|
|
4468
|
-
cloneData$.next({ sourceResource: tokenResult, sourceContent: content });
|
|
4469
|
-
}
|
|
4470
|
-
} catch {
|
|
4471
|
-
}
|
|
4472
|
-
loading$.next(false);
|
|
4473
|
-
})();
|
|
4474
|
-
} else {
|
|
4475
|
-
loading$.next(false);
|
|
4476
|
-
}
|
|
4477
|
-
const save = async (saveParams) => {
|
|
4478
|
-
if (saveParams.mode === "clone") {
|
|
4479
|
-
const response2 = await client.yield.createFromToken({
|
|
4480
|
-
token: params.token,
|
|
4481
|
-
name: saveParams.name,
|
|
4482
|
-
content: saveParams.content,
|
|
4483
|
-
archiveOriginal: saveParams.archiveOriginal ?? true
|
|
4484
|
-
});
|
|
4485
|
-
return response2.resourceId;
|
|
4486
|
-
}
|
|
4487
|
-
let fileToUpload;
|
|
4488
|
-
let mimeType;
|
|
4489
|
-
if (saveParams.file) {
|
|
4490
|
-
fileToUpload = saveParams.file;
|
|
4491
|
-
mimeType = saveParams.format ?? "application/octet-stream";
|
|
4492
|
-
} else {
|
|
4493
|
-
const blob = new Blob([saveParams.content || ""], { type: saveParams.format ?? "application/octet-stream" });
|
|
4494
|
-
const extension = saveParams.format === "text/plain" ? ".txt" : saveParams.format === "text/html" ? ".html" : ".md";
|
|
4495
|
-
fileToUpload = new File([blob], saveParams.name + extension, { type: saveParams.format ?? "application/octet-stream" });
|
|
4496
|
-
mimeType = saveParams.format ?? "application/octet-stream";
|
|
4497
|
-
}
|
|
4498
|
-
const format = saveParams.charset && !saveParams.file ? `${mimeType}; charset=${saveParams.charset}` : mimeType;
|
|
4499
|
-
const response = await client.yield.resource({
|
|
4500
|
-
name: saveParams.name,
|
|
4501
|
-
file: fileToUpload,
|
|
4502
|
-
format,
|
|
4503
|
-
entityTypes: saveParams.entityTypes || [],
|
|
4504
|
-
language: saveParams.language,
|
|
4505
|
-
creationMethod: "ui",
|
|
4506
|
-
storageUri: saveParams.storageUri
|
|
4507
|
-
});
|
|
4508
|
-
const newResourceId = response.resourceId;
|
|
4509
|
-
if (saveParams.mode === "reference" && saveParams.annotationUri && saveParams.sourceDocumentId) {
|
|
4510
|
-
await client.bind.body(
|
|
4511
|
-
resourceId(saveParams.sourceDocumentId),
|
|
4512
|
-
annotationId(saveParams.annotationUri),
|
|
4513
|
-
[{ op: "add", item: { type: "SpecificResource", source: newResourceId, purpose: "linking" } }]
|
|
4514
|
-
);
|
|
4515
|
-
}
|
|
4516
|
-
return newResourceId;
|
|
4517
|
-
};
|
|
4518
|
-
return {
|
|
4519
|
-
browse,
|
|
4520
|
-
mode$: mode$.asObservable(),
|
|
4521
|
-
loading$: loading$.asObservable(),
|
|
4522
|
-
cloneData$: cloneData$.asObservable(),
|
|
4523
|
-
referenceData$: referenceData$.asObservable(),
|
|
4524
|
-
gatheredContext$: gatheredContext$.asObservable(),
|
|
4525
|
-
entityTypes$,
|
|
4526
|
-
save,
|
|
4527
|
-
dispose: () => {
|
|
4528
|
-
mode$.complete();
|
|
4529
|
-
loading$.complete();
|
|
4530
|
-
cloneData$.complete();
|
|
4531
|
-
referenceData$.complete();
|
|
4532
|
-
gatheredContext$.complete();
|
|
4533
|
-
disposer.dispose();
|
|
4534
|
-
}
|
|
4535
|
-
};
|
|
4536
|
-
}
|
|
4537
|
-
|
|
4538
|
-
// src/mime-utils.ts
|
|
4539
|
-
function getExtensionForMimeType(mimeType) {
|
|
4540
|
-
const map15 = {
|
|
4541
|
-
"text/plain": "txt",
|
|
4542
|
-
"text/markdown": "md",
|
|
4543
|
-
"image/png": "png",
|
|
4544
|
-
"image/jpeg": "jpg",
|
|
4545
|
-
"application/pdf": "pdf"
|
|
4546
|
-
};
|
|
4547
|
-
return map15[mimeType] || "dat";
|
|
4548
|
-
}
|
|
4549
|
-
function isImageMimeType(mimeType) {
|
|
4550
|
-
return mimeType === "image/png" || mimeType === "image/jpeg";
|
|
4551
|
-
}
|
|
4552
|
-
function isTextMimeType(mimeType) {
|
|
4553
|
-
return mimeType === "text/plain" || mimeType === "text/markdown";
|
|
4554
|
-
}
|
|
4555
|
-
function isPdfMimeType(mimeType) {
|
|
4556
|
-
return mimeType === "application/pdf";
|
|
4557
|
-
}
|
|
4558
|
-
function getMimeCategory(mimeType) {
|
|
4559
|
-
if (isTextMimeType(mimeType)) {
|
|
4560
|
-
return "text";
|
|
4561
|
-
}
|
|
4562
|
-
if (isImageMimeType(mimeType) || isPdfMimeType(mimeType)) {
|
|
4563
|
-
return "image";
|
|
4564
|
-
}
|
|
4565
|
-
return "unsupported";
|
|
4566
|
-
}
|
|
4567
771
|
|
|
4568
|
-
export { APIError,
|
|
772
|
+
export { APIError, DEGRADED_THRESHOLD_MS, HttpContentTransport, HttpTransport, createActorVM };
|
|
4569
773
|
//# sourceMappingURL=index.js.map
|
|
4570
774
|
//# sourceMappingURL=index.js.map
|