@senguoyun/dsh-arkme 0.1.86 → 0.1.87

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.
Files changed (37) hide show
  1. package/docs/recognized-speaker-directory-backend-handoff-2026-09-28.md +2 -0
  2. package/docs/recognized-speaker-entry-and-visits.md +2 -0
  3. package/docs/recognized-speaker-sorting.md +2 -0
  4. package/docs/speaker-directory-integration-2026-09-30.md +53 -0
  5. package/lib/client.js +877 -1053
  6. package/lib/client.js.map +1 -1
  7. package/lib/index.js +7 -1
  8. package/lib/persistent-extension.js +1 -1
  9. package/lib/{persistent-runtime-DfmCJcxH.js → persistent-runtime-0qn1BZ6S.js} +321 -15
  10. package/lib/types/arkme-service.d.ts +6 -0
  11. package/lib/types/arkme-service.d.ts.map +1 -1
  12. package/lib/types/client/ArkmeAvatar.d.ts +2 -1
  13. package/lib/types/client/ArkmeAvatar.d.ts.map +1 -1
  14. package/lib/types/client/ArkmeRecognizedSpeakersSurface.d.ts +3 -30
  15. package/lib/types/client/ArkmeRecognizedSpeakersSurface.d.ts.map +1 -1
  16. package/lib/types/client/api.d.ts +1 -1
  17. package/lib/types/client/api.d.ts.map +1 -1
  18. package/lib/types/client/locales/actions-en.d.ts.map +1 -1
  19. package/lib/types/client/recognized-speaker-directory.d.ts +51 -22
  20. package/lib/types/client/recognized-speaker-directory.d.ts.map +1 -1
  21. package/lib/types/client/recordings/RecognizedSpeakerEntry.d.ts +2 -4
  22. package/lib/types/client/recordings/RecognizedSpeakerEntry.d.ts.map +1 -1
  23. package/lib/types/client/recordings/recording-speaker-options-store.d.ts.map +1 -1
  24. package/lib/types/host-api.d.ts.map +1 -1
  25. package/lib/types/recording-presence.d.ts +1 -1
  26. package/lib/types/services/recording-service.d.ts +4 -0
  27. package/lib/types/services/recording-service.d.ts.map +1 -1
  28. package/lib/types/services/service.d.ts.map +1 -1
  29. package/lib/types/services/speaker-directory-service.d.ts +19 -0
  30. package/lib/types/services/speaker-directory-service.d.ts.map +1 -0
  31. package/lib/types/services/unmarked-speaker-service.d.ts +2 -0
  32. package/lib/types/services/unmarked-speaker-service.d.ts.map +1 -1
  33. package/lib/types/speaker-directory-contract.d.ts +71 -0
  34. package/lib/types/speaker-directory-contract.d.ts.map +1 -0
  35. package/lib/types/types.d.ts +1 -1
  36. package/lib/types/types.d.ts.map +1 -1
  37. package/package.json +5 -5
package/lib/client.js CHANGED
@@ -35187,6 +35187,23 @@ Bot 列表加载失败,点击重试|Could not load Bots. Click to retry
35187
35187
  //#endregion
35188
35188
  //#region src/client/locales/actions-en.ts
35189
35189
  const actionEnglish = {
35190
+ "加载更多说话人": "Load more speakers",
35191
+ "该分类暂无说话人": "No speakers in this category",
35192
+ "没有匹配的说话人": "No matching speakers",
35193
+ "目录有更新,刷新后查看最新人物。": "The directory has updates. Refresh to see the latest people.",
35194
+ "搜索内容过长,请缩短至 256 字节以内。": "Search text is too long. Keep it within 256 bytes.",
35195
+ "搜索名称或编号": "Search by name or number",
35196
+ "查看状态暂未同步,联网后自动重试。": "View status has not synced. It will retry when online.",
35197
+ "正在加载人物详情…": "Loading person details…",
35198
+ "部分录音仍在整理,人物可能继续更新。": "Some recordings are still being processed. People may continue to update.",
35199
+ "正在更新,当前显示已整理的目录。": "Updating. Showing the available directory.",
35200
+ "更新暂未完成,当前显示上次整理的目录。": "The update has not completed. Showing the previous directory.",
35201
+ "正在整理说话人目录…": "Preparing the speaker directory…",
35202
+ "说话人目录暂不可用,请稍后刷新。": "The speaker directory is unavailable. Please refresh later.",
35203
+ "说话人目录暂不可用,请稍后再试。": "The speaker directory is unavailable. Please try again later.",
35204
+ "声纹状态暂不可用": "Voiceprint status unavailable",
35205
+ "暂时无法更新": "Unable to update",
35206
+ "目录暂不可用": "Directory unavailable",
35190
35207
  "发现未发布的草稿": "Unpublished draft found",
35191
35208
  "草稿预览": "Draft preview",
35192
35209
  "预览草稿": "Preview draft",
@@ -55159,7 +55176,7 @@ body[data-ds-dark-theme] .arkme-bot-identity {
55159
55176
  children: fallback.label || "--"
55160
55177
  });
55161
55178
  }
55162
- function ArkmeUserAvatar({ avatarRef, lazy = false, fallback, size = 44, label = "用户头像", senderKind }) {
55179
+ function ArkmeUserAvatar({ avatarRef, lazy = false, fallback, fallbackContent, size = 44, label = "用户头像", senderKind }) {
55163
55180
  const normalizedRef = avatarRef?.trim() ?? "";
55164
55181
  const container = (0, react$1.useRef)(null);
55165
55182
  const imageUrl = useArkmeAvatarImage(normalizedRef, lazy ? container : void 0);
@@ -55177,7 +55194,7 @@ body[data-ds-dark-theme] .arkme-bot-identity {
55177
55194
  }) : fallback?.kind === "phone_default" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PhoneDefaultAvatar, {
55178
55195
  fallback,
55179
55196
  size
55180
- }) : senderKind === "bot" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ArkmeBotAvatarFallback, { size }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DefaultUserAvatar, { size })
55197
+ }) : fallbackContent ?? (senderKind === "bot" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ArkmeBotAvatarFallback, { size }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DefaultUserAvatar, { size }))
55181
55198
  });
55182
55199
  }
55183
55200
  function GroupAvatarMember({ slot, size }) {
@@ -57229,30 +57246,51 @@ body[data-ds-dark-theme] .arkme-bot-identity {
57229
57246
  }
57230
57247
  };
57231
57248
  //#endregion
57232
- //#region src/client/recognized-speaker-tracker.ts
57233
- const empty$4 = { checkedAt: 0 };
57234
- const storageKey$4 = (account) => `dsh-arkme:speaker-visits:v1:${account}`;
57235
- const storage$1 = () => {
57236
- try {
57237
- return window.localStorage;
57238
- } catch {
57239
- return;
57249
+ //#region src/client/recognized-speaker-directory.ts
57250
+ const loaders = {
57251
+ summary: (input, signal) => callArkme("speaker-directory.summary", input, signal),
57252
+ list: (input, signal) => callArkme("speaker-directory.list", { ...input }, signal),
57253
+ seen: (throughCursor, signal) => callArkme("speaker-directory.seen", { throughCursor }, signal),
57254
+ open: (detailRef, signal) => callArkme("speaker-directory.open", { detailRef }, signal),
57255
+ avatars: (detailRefs, signal) => callArkme("speaker-directory.avatars", { detailRefs }, signal)
57256
+ };
57257
+ var SpeakerReadDeferred = class extends Error {
57258
+ retryAt;
57259
+ constructor(retryAt, cause) {
57260
+ super(cause instanceof Error ? cause.message : "说话人目录暂不可用", { cause });
57261
+ this.retryAt = retryAt;
57240
57262
  }
57241
57263
  };
57242
- function markedSpeakerKeys(options) {
57243
- return [...new Set(options.filter((option) => option.kind === "speaker").map((option) => `m:${option.personKey || option.optionKey}`))];
57264
+ function directoryErrorCode(error) {
57265
+ const value = error;
57266
+ return value?.body?.code ?? value?.code ?? "";
57244
57267
  }
57245
- /** Only complete identity snapshots can establish a baseline or produce a precise delta.
57246
- * A removed identity may be a merge/recluster; without lineage we suppress ambiguous
57247
- * additions for that transition rather than presenting a false "new person".
57248
- */
57249
- var RecognizedSpeakerTracker = class {
57250
- getStorage;
57251
- records = /* @__PURE__ */ new Map();
57252
- snapshots = /* @__PURE__ */ new Map();
57268
+ function speakerRetryDelay(error) {
57269
+ if (error instanceof SpeakerReadDeferred) return Math.max(1e3, error.retryAt - Date.now());
57270
+ const value = error;
57271
+ if (directoryErrorCode(error) === "arkme-code-1001" || value?.body?.retryable === false || value?.retryable === false) return void 0;
57272
+ return Math.max(1e3, value?.body?.retryAfterMillis ?? value?.retryAfterMillis ?? 15e3);
57273
+ }
57274
+ const directoryVisible = () => typeof document === "undefined" || document.visibilityState !== "hidden";
57275
+ const normalizeDirectoryQuery = (query) => query.trim().toLowerCase();
57276
+ function directoryQueryValid(query) {
57277
+ return new TextEncoder().encode(normalizeDirectoryQuery(query)).length <= 256;
57278
+ }
57279
+ const queryKey = (query) => JSON.stringify([
57280
+ query.filter,
57281
+ query.sort,
57282
+ normalizeDirectoryQuery(query.query)
57283
+ ]);
57284
+ const empty$4 = Object.freeze({});
57285
+ /** Shared summary and immutable same-version pages. No traversal or local new-person inference. */
57286
+ var RecognizedSpeakerDirectory = class {
57287
+ api;
57288
+ now;
57289
+ accounts = /* @__PURE__ */ new Map();
57253
57290
  listeners = /* @__PURE__ */ new Set();
57254
- constructor(getStorage = storage$1) {
57255
- this.getStorage = getStorage;
57291
+ constructor(api = loaders, now = Date.now) {
57292
+ this.api = api;
57293
+ this.now = now;
57256
57294
  }
57257
57295
  subscribe = (listener) => {
57258
57296
  this.listeners.add(listener);
@@ -57260,439 +57298,347 @@ body[data-ds-dark-theme] .arkme-bot-identity {
57260
57298
  this.listeners.delete(listener);
57261
57299
  };
57262
57300
  };
57263
- get = (account) => account === void 0 ? empty$4 : this.snapshots.get(account) ?? empty$4;
57264
- emit(account, snapshot) {
57265
- this.snapshots.set(account, snapshot);
57266
- for (const listener of this.listeners) listener();
57267
- }
57268
- read(account) {
57269
- if (this.records.has(account)) return this.records.get(account);
57270
- try {
57271
- const value = JSON.parse(this.getStorage()?.getItem(storageKey$4(account)) ?? "null");
57272
- if (value !== null && [
57273
- "known",
57274
- "current",
57275
- "pending"
57276
- ].every((key) => {
57277
- const list = value[key];
57278
- return Array.isArray(list) && list.length <= 2e4 && list.every((item) => typeof item === "string" && item.length <= 256);
57279
- })) {
57280
- this.records.set(account, value);
57281
- return value;
57282
- }
57283
- } catch {}
57284
- }
57285
- save(account, value) {
57286
- this.records.set(account, value);
57287
- try {
57288
- this.getStorage()?.setItem(storageKey$4(account), JSON.stringify(value));
57289
- } catch {}
57290
- }
57291
- setTotal(account, total) {
57292
- if (!Number.isSafeInteger(total) || total < 0) return;
57293
- const previous = this.get(account);
57294
- this.emit(account, previous.total === total ? {
57295
- ...previous,
57296
- total
57297
- } : {
57298
- total,
57299
- checkedAt: 0
57300
- });
57301
+ get = (account) => account === void 0 ? empty$4 : this.accounts.get(account)?.snapshot ?? empty$4;
57302
+ emit() {
57303
+ this.listeners.forEach((listener) => listener());
57301
57304
  }
57302
- uncertain(account) {
57303
- const previous = this.get(account);
57304
- this.emit(account, {
57305
- ...previous.total === void 0 ? {} : { total: previous.total },
57306
- checkedAt: 0
57307
- });
57305
+ account(key) {
57306
+ let found = this.accounts.get(key);
57307
+ if (!found) {
57308
+ found = {
57309
+ controller: new AbortController(),
57310
+ snapshot: empty$4,
57311
+ summaryAt: 0,
57312
+ retryAt: 0,
57313
+ failures: 0,
57314
+ flights: /* @__PURE__ */ new Map(),
57315
+ lists: /* @__PURE__ */ new Map(),
57316
+ pendingSeen: /* @__PURE__ */ new Set(),
57317
+ confirmedSeen: /* @__PURE__ */ new Set(),
57318
+ expiredSeen: /* @__PURE__ */ new Set(),
57319
+ revision: 0
57320
+ };
57321
+ this.accounts.set(key, found);
57322
+ }
57323
+ return found;
57308
57324
  }
57309
- acknowledge(account) {
57310
- const previous = this.read(account);
57311
- if (previous === void 0) return;
57312
- this.save(account, {
57313
- ...previous,
57314
- pending: []
57315
- });
57316
- this.emit(account, {
57317
- ...this.get(account),
57318
- newCount: 0
57325
+ current(account, value) {
57326
+ value.controller.signal.throwIfAborted();
57327
+ if (this.accounts.get(account) !== value) throw new DOMException("账号已切换", "AbortError");
57328
+ }
57329
+ async request(account, key, load) {
57330
+ const value = this.account(account);
57331
+ this.current(account, value);
57332
+ key = `${value.revision}:${key}`;
57333
+ const pending = value.flights.get(key);
57334
+ if (pending) return pending;
57335
+ if (this.now() < value.retryAt) throw new SpeakerReadDeferred(value.retryAt, /* @__PURE__ */ new Error("请求较频繁或网络暂不可用,请稍后重试"));
57336
+ const promise = Promise.resolve().then(() => load(value.controller.signal)).then((result) => {
57337
+ this.current(account, value);
57338
+ value.failures = 0;
57339
+ return result;
57340
+ }).catch((error) => {
57341
+ this.current(account, value);
57342
+ const delay = speakerRetryDelay(error);
57343
+ if (delay !== void 0) {
57344
+ value.failures += 1;
57345
+ value.retryAt = this.now() + Math.max(delay, Math.min(3e5, 1e3 * 2 ** Math.min(8, value.failures)));
57346
+ throw new SpeakerReadDeferred(value.retryAt, error);
57347
+ }
57348
+ throw error;
57349
+ }).finally(() => {
57350
+ if (value.flights.get(key) === promise) value.flights.delete(key);
57319
57351
  });
57352
+ value.flights.set(key, promise);
57353
+ return promise;
57320
57354
  }
57321
- observe(account, options, candidates, viewed = false) {
57322
- const marked = markedSpeakerKeys(options);
57323
- if (candidates.some((candidate) => !candidate.identityKey)) {
57324
- this.uncertain(account);
57325
- return;
57355
+ async summary(account, force = false) {
57356
+ const state = this.account(account), cached = state.snapshot.summary, revision = state.revision;
57357
+ if (!force && cached && (cached.state === "disabled" || this.now() < state.summaryAt)) return cached;
57358
+ try {
57359
+ const result = await this.request(account, "summary", (signal) => this.api.summary(cached && cached.snapshotVersion ? {
57360
+ snapshotVersion: cached.snapshotVersion,
57361
+ seenVersion: cached.seenVersion
57362
+ } : {}, signal));
57363
+ this.current(account, state);
57364
+ if (revision !== state.revision) return this.summary(account, force);
57365
+ state.snapshot = { summary: result };
57366
+ state.summaryAt = this.now() + (result.retryAfterMs > 0 ? result.retryAfterMs : 3e4);
57367
+ this.emit();
57368
+ return result;
57369
+ } catch (error) {
57370
+ this.current(account, state);
57371
+ state.snapshot = {
57372
+ ...state.snapshot,
57373
+ error: error instanceof Error ? error.message : "目录暂不可用"
57374
+ };
57375
+ this.emit();
57376
+ throw error;
57326
57377
  }
57327
- const current = [.../* @__PURE__ */ new Set([...marked, ...candidates.map((candidate) => `u:${candidate.identityKey}`)])];
57328
- const previous = this.read(account);
57329
- const known = new Set(previous?.known ?? []);
57330
- const currentSet = new Set(current);
57331
- const removed = previous?.current.some((key) => !currentSet.has(key)) ?? false;
57332
- const additions = previous === void 0 || removed ? [] : current.filter((key) => !known.has(key));
57333
- const pending = viewed ? [] : [.../* @__PURE__ */ new Set([...(previous?.pending ?? []).filter((key) => currentSet.has(key)), ...additions])];
57334
- current.forEach((key) => known.add(key));
57335
- const record = known.size > 2e4 ? {
57336
- known: current,
57337
- current,
57338
- pending: []
57339
- } : {
57340
- known: [...known],
57341
- current,
57342
- pending
57343
- };
57344
- this.save(account, record);
57345
- this.emit(account, {
57346
- total: current.length,
57347
- newCount: record.pending.length,
57348
- checkedAt: Date.now()
57349
- });
57350
- }
57351
- clearSession() {
57352
- this.snapshots.clear();
57353
- this.records.clear();
57354
- for (const listener of this.listeners) listener();
57355
- }
57356
- };
57357
- const recognizedSpeakerTracker = new RecognizedSpeakerTracker();
57358
- let activeAccount$1 = "";
57359
- arkmeAuthStore.subscribe(() => {
57360
- const auth = arkmeAuthStore.getSnapshot().auth;
57361
- const next = auth?.status === "authenticated" ? `${auth.environment}:${auth.userId}` : "";
57362
- if (next !== activeAccount$1) {
57363
- activeAccount$1 = next;
57364
- recognizedSpeakerTracker.clearSession();
57365
- }
57366
- });
57367
- //#endregion
57368
- //#region src/client/recognized-speaker-order.ts
57369
- const preferenceKey$3 = (accountKey) => `dsh-arkme:recognized-speaker-order:v1:${accountKey}`;
57370
- function browserStorage$1() {
57371
- try {
57372
- return typeof window === "undefined" ? void 0 : window.localStorage;
57373
- } catch {
57374
- return;
57375
57378
  }
57376
- }
57377
- function readRecognizedSpeakerOrder(accountKey, storage = browserStorage$1()) {
57378
- try {
57379
- return storage?.getItem(preferenceKey$3(accountKey)) === "recent" ? "recent" : "frequent";
57380
- } catch {
57381
- return "frequent";
57379
+ peek(account, query) {
57380
+ return this.accounts.get(account)?.lists.get(queryKey(query));
57382
57381
  }
57383
- }
57384
- function writeRecognizedSpeakerOrder(accountKey, order, storage = browserStorage$1()) {
57385
- try {
57386
- storage?.setItem(preferenceKey$3(accountKey), order);
57387
- } catch {}
57388
- }
57389
- function positiveNumber(value) {
57390
- return value !== void 0 && Number.isFinite(value) && value > 0 ? value : 0;
57391
- }
57392
- /** Missing statistics sort last. Ties remain stable across refreshes and filters. */
57393
- function compareRecognizedSpeakers(left, right, order) {
57394
- const leftDays = positiveNumber(left.dayCount), rightDays = positiveNumber(right.dayCount);
57395
- const leftTime = positiveNumber(left.lastSeenAt), rightTime = positiveNumber(right.lastSeenAt);
57396
- return (order === "frequent" ? rightDays - leftDays || rightTime - leftTime : rightTime - leftTime || rightDays - leftDays) || left.name.localeCompare(right.name, void 0, { numeric: true }) || left.key.localeCompare(right.key);
57397
- }
57398
- /** The upstream cursor has a fixed order. Read metadata pages before claiming global order.
57399
- * Bound reads to the host's 2,000-candidate reference budget; never silently call a partial
57400
- * traversal complete. No audio or per-speaker detail is loaded here.
57401
- */
57402
- async function loadSpeakerCandidateSnapshot(loadPage, signal, onProgress) {
57403
- for (let attempt = 0; attempt < 2; attempt += 1) {
57404
- const rows = /* @__PURE__ */ new Map();
57405
- const cursors = /* @__PURE__ */ new Set();
57406
- let cursor = "";
57407
- let snapshot = {
57382
+ async first(account, query, signal, force = false) {
57383
+ if (!directoryQueryValid(query.query)) throw new Error("搜索内容过长,请缩短至 256 字节以内");
57384
+ signal.throwIfAborted();
57385
+ const state = this.account(account), revision = state.revision;
57386
+ const summary = await this.summary(account, force);
57387
+ signal.throwIfAborted();
57388
+ const key = queryKey(query), cached = state.lists.get(key);
57389
+ if (!force && cached && cached.snapshotVersion === summary.snapshotVersion && !state.expiredSeen.has(cached.throughCursor)) return cached;
57390
+ const input = {
57391
+ ...query,
57392
+ query: normalizeDirectoryQuery(query.query),
57393
+ cursor: "",
57394
+ snapshotVersion: summary.snapshotVersion,
57395
+ limit: 50
57396
+ };
57397
+ const page = summary.state === "disabled" || summary.coverage !== "complete" ? {
57398
+ ...summary,
57408
57399
  items: [],
57409
- complete: false,
57410
- projectionState: void 0
57400
+ hasMore: false,
57401
+ nextCursor: "",
57402
+ throughCursor: ""
57403
+ } : await this.request(account, `list:${JSON.stringify(input)}`, (inner) => this.api.list(input, inner));
57404
+ signal.throwIfAborted();
57405
+ this.current(account, state);
57406
+ if (revision !== state.revision) throw new DOMException("目录已更新", "AbortError");
57407
+ if (page.state === "snapshot_expired") {
57408
+ state.summaryAt = 0;
57409
+ state.lists.delete(key);
57410
+ if (page.retryAfterMs > 0) {
57411
+ state.retryAt = Math.max(state.retryAt, this.now() + page.retryAfterMs);
57412
+ throw new SpeakerReadDeferred(state.retryAt, /* @__PURE__ */ new Error("目录正在更新,请稍后重试"));
57413
+ }
57414
+ if (force) throw new Error("目录正在更新,请稍后刷新");
57415
+ return this.first(account, query, signal, true);
57416
+ }
57417
+ if (page.state === "disabled") {
57418
+ state.snapshot = { summary: {
57419
+ ...summary,
57420
+ state: "disabled"
57421
+ } };
57422
+ this.emit();
57423
+ }
57424
+ const result = {
57425
+ ...page,
57426
+ query: {
57427
+ ...query,
57428
+ query: input.query
57429
+ },
57430
+ requestedCursors: [""]
57411
57431
  };
57412
- for (let pageIndex = 0; pageIndex < 40; pageIndex += 1) {
57413
- signal.throwIfAborted();
57414
- const page = await loadPage(cursor, signal);
57415
- signal.throwIfAborted();
57416
- if (page.section !== "unmarked-speakers") throw new Error("未标记说话人列表响应无效");
57417
- if (page.cursorStale) {
57418
- snapshot = {
57419
- items: [],
57420
- complete: false,
57421
- projectionState: "stale"
57422
- };
57423
- onProgress(snapshot);
57424
- if (attempt === 0) break;
57425
- return snapshot;
57426
- }
57427
- for (const item of page.items) if (item.kind === "unmarked-speaker" && rows.size < 2e3) rows.set(item.candidateRef, item);
57428
- snapshot = {
57429
- items: [...rows.values()],
57430
- complete: !page.hasMore && page.coverage !== "partial" && (page.projectionState === void 0 || page.projectionState === "fresh"),
57431
- projectionState: page.projectionState
57432
- };
57433
- onProgress(snapshot);
57434
- if (!page.hasMore || rows.size >= 2e3 || page.projectionState !== void 0 && page.projectionState !== "fresh") return snapshot;
57435
- const next = page.nextCursor ?? "";
57436
- if (next === "" || cursors.has(next)) return snapshot;
57437
- cursors.add(next);
57438
- cursor = next;
57439
- if (pageIndex === 39) return snapshot;
57432
+ if (page.coverage === "complete" && page.state !== "disabled") {
57433
+ state.lists.delete(key);
57434
+ state.lists.set(key, result);
57435
+ while (state.lists.size > 12) state.lists.delete(state.lists.keys().next().value);
57440
57436
  }
57437
+ return result;
57441
57438
  }
57442
- return {
57443
- items: [],
57444
- complete: false,
57445
- projectionState: "stale"
57446
- };
57447
- }
57448
- //#endregion
57449
- //#region src/client/recognized-speaker-directory.ts
57450
- const readSpeakerOptions = (signal) => callArkme("recordings.speaker.options", {}, signal);
57451
- const readSpeakerPresence = (signal) => callArkme("recordings.speaker.presence", {}, signal);
57452
- const readSpeakerPage = (cursor, signal) => callArkme("directory.list", {
57453
- section: "unmarked-speakers",
57454
- limit: 50,
57455
- ...cursor === "" ? {} : { cursor }
57456
- }, signal);
57457
- var SpeakerReadDeferred = class extends Error {
57458
- retryAt;
57459
- constructor(retryAt, cause) {
57460
- super(cause instanceof Error ? cause.message : "说话人列表暂时无法加载", { cause });
57461
- this.retryAt = retryAt;
57462
- }
57463
- };
57464
- function speakerRetryDelay(error) {
57465
- return error instanceof SpeakerReadDeferred ? Math.max(1e3, error.retryAt - Date.now()) : void 0;
57466
- }
57467
- /** Per-account shared read. A short release grace lets entry -> list hand off the same
57468
- * request; leaving both cancels it. A late/aborted response can never update the cache.
57469
- */
57470
- var SharedRead = class {
57471
- ttl;
57472
- load;
57473
- now;
57474
- value;
57475
- freshUntil = 0;
57476
- retryAt = 0;
57477
- failures = 0;
57478
- error;
57479
- pending;
57480
- listeners = /* @__PURE__ */ new Set();
57481
- releaseTimer;
57482
- constructor(ttl, load, now) {
57483
- this.ttl = ttl;
57484
- this.load = load;
57485
- this.now = now;
57486
- }
57487
- read(signal, progress = () => {}) {
57439
+ async more(account, previous, signal) {
57488
57440
  signal.throwIfAborted();
57489
- if (this.value !== void 0) progress(this.value);
57490
- if (this.now() < this.retryAt) return Promise.reject(new SpeakerReadDeferred(this.retryAt, this.error));
57491
- if (this.value !== void 0 && this.now() < this.freshUntil) return Promise.resolve(this.value);
57492
- clearTimeout(this.releaseTimer);
57493
- if (this.pending?.controller.signal.aborted) this.pending = void 0;
57494
- if (this.pending === void 0) {
57495
- const controller = new AbortController();
57496
- const emit = (value) => {
57497
- controller.signal.throwIfAborted();
57498
- this.value = value;
57499
- for (const listener of this.listeners) listener(value);
57500
- };
57501
- const promise = Promise.resolve().then(async () => {
57502
- controller.signal.throwIfAborted();
57503
- const value = await this.load(controller.signal, emit);
57504
- controller.signal.throwIfAborted();
57505
- emit(value);
57506
- this.freshUntil = this.now() + this.ttl;
57507
- this.failures = 0;
57508
- this.retryAt = 0;
57509
- this.error = void 0;
57510
- return value;
57511
- }).catch((error) => {
57512
- if (controller.signal.aborted) throw error;
57513
- this.failures += 1;
57514
- const limited = /(?:\b429\b|rate.?limit|too many requests)/i.test(error instanceof Error ? error.message : String(error));
57515
- this.retryAt = this.now() + Math.min(5 * 6e4, (limited ? 6e4 : 15e3) * 2 ** Math.min(4, this.failures - 1));
57516
- this.error = error;
57517
- throw new SpeakerReadDeferred(this.retryAt, error);
57518
- }).finally(() => {
57519
- if (this.pending?.controller === controller) this.pending = void 0;
57520
- });
57521
- this.pending = {
57522
- controller,
57523
- promise
57441
+ if (!previous.hasMore) return previous;
57442
+ const state = this.account(account), revision = state.revision;
57443
+ if (!state.lists.has(queryKey(previous.query))) return this.first(account, previous.query, signal, true);
57444
+ const input = {
57445
+ ...previous.query,
57446
+ limit: 50,
57447
+ cursor: previous.nextCursor,
57448
+ snapshotVersion: previous.snapshotVersion
57449
+ };
57450
+ const page = await this.request(account, `list:${JSON.stringify(input)}`, (inner) => this.api.list(input, inner));
57451
+ signal.throwIfAborted();
57452
+ this.current(account, state);
57453
+ if (revision !== state.revision) throw new DOMException("目录已更新", "AbortError");
57454
+ if (page.state === "snapshot_expired") {
57455
+ state.lists.delete(queryKey(previous.query));
57456
+ state.summaryAt = 0;
57457
+ if (page.retryAfterMs > 0) {
57458
+ state.retryAt = Math.max(state.retryAt, this.now() + page.retryAfterMs);
57459
+ throw new SpeakerReadDeferred(state.retryAt, /* @__PURE__ */ new Error("目录正在更新,请稍后刷新"));
57460
+ }
57461
+ return this.first(account, previous.query, signal, true);
57462
+ }
57463
+ if (page.state === "disabled") {
57464
+ const summary = state.snapshot.summary;
57465
+ if (summary) {
57466
+ state.snapshot = { summary: {
57467
+ ...summary,
57468
+ state: "disabled"
57469
+ } };
57470
+ this.emit();
57471
+ }
57472
+ return {
57473
+ ...previous,
57474
+ state: "disabled",
57475
+ hasMore: false
57524
57476
  };
57525
57477
  }
57526
- const pending = this.pending;
57527
- return new Promise((resolve, reject) => {
57528
- const listener = (value) => {
57529
- if (!signal.aborted) progress(value);
57530
- };
57531
- this.listeners.add(listener);
57532
- const cleanup = () => {
57533
- signal.removeEventListener("abort", abort);
57534
- this.listeners.delete(listener);
57535
- if (this.listeners.size === 0 && this.pending === pending) this.releaseTimer = setTimeout(() => {
57536
- if (this.listeners.size === 0 && this.pending === pending) pending.controller.abort();
57537
- }, 250);
57538
- };
57539
- const abort = () => {
57540
- cleanup();
57541
- reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
57542
- };
57543
- signal.addEventListener("abort", abort, { once: true });
57544
- pending.promise.then((value) => {
57545
- cleanup();
57546
- if (!signal.aborted) resolve(value);
57547
- }, (error) => {
57548
- cleanup();
57549
- if (!signal.aborted) reject(error);
57550
- });
57551
- });
57552
- }
57553
- invalidate() {
57554
- clearTimeout(this.releaseTimer);
57555
- this.pending?.controller.abort();
57556
- this.pending = void 0;
57557
- this.freshUntil = 0;
57558
- }
57559
- dispose() {
57560
- this.invalidate();
57561
- this.listeners.clear();
57562
- this.value = void 0;
57563
- }
57564
- };
57565
- function pause(milliseconds, signal) {
57566
- if (milliseconds <= 0) return Promise.resolve();
57567
- return new Promise((resolve, reject) => {
57568
- const abort = () => {
57569
- clearTimeout(timer);
57570
- reject(signal.reason);
57478
+ if (page.snapshotVersion !== previous.snapshotVersion || page.coverage !== "complete" || page.hasMore && (page.nextCursor === previous.nextCursor || previous.requestedCursors.includes(page.nextCursor))) throw new Error("目录版本已变化,请刷新列表");
57479
+ const keys = new Set(previous.items.map((item) => item.personKey));
57480
+ const result = {
57481
+ ...page,
57482
+ query: previous.query,
57483
+ requestedCursors: [...previous.requestedCursors, previous.nextCursor],
57484
+ items: [...previous.items, ...page.items.filter((item) => {
57485
+ if (keys.has(item.personKey)) return false;
57486
+ keys.add(item.personKey);
57487
+ return true;
57488
+ })]
57571
57489
  };
57572
- const timer = setTimeout(() => {
57573
- signal.removeEventListener("abort", abort);
57574
- resolve();
57575
- }, milliseconds);
57576
- signal.addEventListener("abort", abort, { once: true });
57577
- if (signal.aborted) abort();
57578
- });
57579
- }
57580
- var RecognizedSpeakerDirectory = class {
57581
- loaders;
57582
- pageGapMs;
57583
- now;
57584
- accounts = /* @__PURE__ */ new Map();
57585
- constructor(loaders = {
57586
- marked: readSpeakerOptions,
57587
- page: readSpeakerPage,
57588
- presence: readSpeakerPresence
57589
- }, pageGapMs = 750, now = Date.now) {
57590
- this.loaders = loaders;
57591
- this.pageGapMs = pageGapMs;
57592
- this.now = now;
57490
+ state.lists.set(queryKey(previous.query), result);
57491
+ return result;
57593
57492
  }
57594
- account(account) {
57595
- const found = this.accounts.get(account);
57596
- if (found !== void 0) return found;
57597
- const pages = /* @__PURE__ */ new Map();
57598
- let startedAt = 0, lastRequestAt = 0, completed = false;
57599
- let state;
57600
- state = {
57601
- pages,
57602
- marked: new SharedRead(6e4, (signal) => this.loaders.marked(signal), this.now),
57603
- presence: new SharedRead(6e4, (signal) => this.loaders.presence(signal), this.now),
57604
- candidates: new SharedRead(5 * 6e4, async (signal, emit) => {
57605
- if (completed || this.now() - startedAt >= 5 * 6e4) pages.clear();
57606
- if (pages.size === 0) startedAt = this.now();
57607
- completed = false;
57608
- let total;
57609
- const result = await loadSpeakerCandidateSnapshot(async (cursor) => {
57610
- const cached = pages.get(cursor);
57611
- if (cached !== void 0) return cached;
57612
- await pause(Math.max(0, lastRequestAt + this.pageGapMs - this.now()), signal);
57613
- signal.throwIfAborted();
57614
- lastRequestAt = this.now();
57615
- const page = await this.loaders.page(cursor, signal);
57616
- signal.throwIfAborted();
57617
- if (page.cursorStale) {
57618
- pages.clear();
57619
- startedAt = this.now();
57620
- } else if (page.projectionState === void 0 || page.projectionState === "fresh") pages.set(cursor, page);
57621
- return page;
57622
- }, signal, (snapshot) => {
57623
- const first = pages.get("");
57624
- total = first?.coverage !== "partial" ? first?.total : void 0;
57625
- if (!state.candidates.value?.complete || snapshot.complete) emit({
57626
- ...snapshot,
57627
- total
57628
- });
57629
- });
57630
- completed = result.complete;
57631
- if (!result.complete) {
57632
- if (!state.candidates.value?.complete) emit({
57633
- ...result,
57634
- total
57635
- });
57636
- throw new Error("说话人列表尚未完整,请稍后重试");
57493
+ async confirmDisplayed(account, list) {
57494
+ if (!directoryVisible() || list.query.filter !== "all" || list.query.query !== "" || list.coverage !== "complete" || ![
57495
+ "fresh",
57496
+ "stale",
57497
+ "failed"
57498
+ ].includes(list.state) || !list.throughCursor) return;
57499
+ const state = this.account(account);
57500
+ if (state.confirmedSeen.has(list.throughCursor) || state.expiredSeen.has(list.throughCursor) || state.snapshot.summary?.state === "disabled") return;
57501
+ state.pendingSeen.add(list.throughCursor);
57502
+ await this.flushSeen(account);
57503
+ }
57504
+ async flushSeen(account) {
57505
+ const state = this.account(account);
57506
+ if (!directoryVisible() || state.snapshot.summary?.state === "disabled") return;
57507
+ for (const cursor of state.pendingSeen) {
57508
+ let result;
57509
+ try {
57510
+ result = await this.request(account, `seen:${cursor}`, (signal) => this.api.seen(cursor, signal));
57511
+ } catch (error) {
57512
+ if (directoryErrorCode(error) === "arkme-code-1001") {
57513
+ state.pendingSeen.delete(cursor);
57514
+ state.expiredSeen.add(cursor);
57515
+ state.lists.clear();
57516
+ state.summaryAt = 0;
57517
+ this.emit();
57518
+ continue;
57637
57519
  }
57638
- return {
57639
- ...result,
57640
- total
57641
- };
57642
- }, this.now)
57643
- };
57644
- this.accounts.set(account, state);
57645
- return state;
57646
- }
57647
- peekMarked(account) {
57648
- return this.accounts.get(account)?.marked.value;
57649
- }
57650
- peekPresence(account) {
57651
- return this.accounts.get(account)?.presence.value;
57520
+ throw error;
57521
+ }
57522
+ this.current(account, state);
57523
+ if (result.success) {
57524
+ state.pendingSeen.delete(cursor);
57525
+ state.confirmedSeen.add(cursor);
57526
+ await Promise.allSettled([...state.flights.entries()].filter(([key]) => key.endsWith(":summary")).map(([, promise]) => promise));
57527
+ await this.summary(account, true);
57528
+ } else if (result.state === "snapshot_expired") {
57529
+ state.pendingSeen.delete(cursor);
57530
+ state.expiredSeen.add(cursor);
57531
+ state.lists.clear();
57532
+ state.summaryAt = 0;
57533
+ if (result.retryAfterMs > 0) state.retryAt = Math.max(state.retryAt, this.now() + result.retryAfterMs);
57534
+ this.emit();
57535
+ } else {
57536
+ state.pendingSeen.clear();
57537
+ if (state.snapshot.summary) state.snapshot = { summary: {
57538
+ ...state.snapshot.summary,
57539
+ state: "disabled"
57540
+ } };
57541
+ this.emit();
57542
+ return;
57543
+ }
57544
+ }
57652
57545
  }
57653
- peekCandidates(account) {
57654
- return this.accounts.get(account)?.candidates.value;
57546
+ hasPendingSeen(account) {
57547
+ return (this.accounts.get(account)?.pendingSeen.size ?? 0) > 0;
57655
57548
  }
57656
- readMarked(account, signal) {
57657
- return this.account(account).marked.read(signal);
57549
+ needsReload(account, list) {
57550
+ return this.accounts.get(account)?.expiredSeen.has(list.throughCursor) === true;
57658
57551
  }
57659
- readPresence(account, signal) {
57660
- return this.account(account).presence.read(signal);
57552
+ open(account, ref, signal) {
57553
+ signal.throwIfAborted();
57554
+ return this.request(account, `open:${ref}`, (inner) => this.api.open(ref, inner)).then((value) => {
57555
+ signal.throwIfAborted();
57556
+ return value;
57557
+ });
57661
57558
  }
57662
- refreshPresence(account, signal) {
57663
- this.account(account).presence.invalidate();
57664
- return this.readPresence(account, signal);
57559
+ async avatars(account, detailRefs, signal) {
57560
+ const state = this.account(account);
57561
+ const result = await this.api.avatars(detailRefs, AbortSignal.any([state.controller.signal, signal]));
57562
+ signal.throwIfAborted();
57563
+ this.current(account, state);
57564
+ return result;
57665
57565
  }
57666
- readCandidates(account, signal, progress) {
57667
- return this.account(account).candidates.read(signal, progress);
57566
+ /** Visible consumers share one lightweight summary request, including online/focus recovery. */
57567
+ watch(account, changed = () => {}) {
57568
+ let disposed = false, busy = false, timer;
57569
+ const run = async () => {
57570
+ if (disposed || busy || !directoryVisible()) return;
57571
+ busy = true;
57572
+ let delay = 3e4;
57573
+ try {
57574
+ const summary = await this.summary(account);
57575
+ if (summary.state === "disabled") {
57576
+ changed();
57577
+ return;
57578
+ }
57579
+ await this.flushSeen(account);
57580
+ changed();
57581
+ delay = summary.retryAfterMs > 0 ? summary.retryAfterMs : 3e4;
57582
+ } catch (error) {
57583
+ delay = speakerRetryDelay(error) ?? 0;
57584
+ changed();
57585
+ } finally {
57586
+ busy = false;
57587
+ if (!disposed && delay > 0 && this.get(account).summary?.state !== "disabled") {
57588
+ clearTimeout(timer);
57589
+ timer = setTimeout(() => {
57590
+ run();
57591
+ }, Math.max(1e3, delay));
57592
+ }
57593
+ }
57594
+ };
57595
+ const wake = () => {
57596
+ if (directoryVisible()) {
57597
+ const value = this.account(account);
57598
+ value.summaryAt = 0;
57599
+ run();
57600
+ }
57601
+ };
57602
+ run();
57603
+ if (typeof document !== "undefined") document.addEventListener("visibilitychange", wake);
57604
+ if (typeof window !== "undefined") {
57605
+ window.addEventListener("online", wake);
57606
+ window.addEventListener("focus", wake);
57607
+ }
57608
+ return () => {
57609
+ disposed = true;
57610
+ clearTimeout(timer);
57611
+ if (typeof document !== "undefined") document.removeEventListener("visibilitychange", wake);
57612
+ if (typeof window !== "undefined") {
57613
+ window.removeEventListener("online", wake);
57614
+ window.removeEventListener("focus", wake);
57615
+ }
57616
+ };
57668
57617
  }
57669
57618
  invalidate(account) {
57670
57619
  const state = this.accounts.get(account);
57671
- if (state === void 0) return;
57672
- state.pages.clear();
57673
- state.marked.invalidate();
57674
- state.presence.invalidate();
57675
- state.candidates.invalidate();
57620
+ if (!state) return;
57621
+ state.revision += 1;
57622
+ state.summaryAt = 0;
57623
+ state.lists.clear();
57624
+ this.emit();
57676
57625
  }
57677
57626
  clear() {
57678
- for (const state of this.accounts.values()) {
57679
- state.marked.dispose();
57680
- state.presence.dispose();
57681
- state.candidates.dispose();
57682
- }
57627
+ this.accounts.forEach((state) => state.controller.abort());
57683
57628
  this.accounts.clear();
57629
+ this.emit();
57684
57630
  }
57685
57631
  };
57686
57632
  const recognizedSpeakerDirectory = new RecognizedSpeakerDirectory();
57687
- const accountKey = () => {
57633
+ const currentAccount$1 = () => {
57688
57634
  const auth = arkmeAuthStore.getSnapshot().auth;
57689
57635
  return auth?.status === "authenticated" ? `${auth.environment}:${auth.userId}` : "";
57690
57636
  };
57691
- let activeAccount = accountKey();
57637
+ let account$1 = currentAccount$1();
57692
57638
  arkmeAuthStore.subscribe(() => {
57693
- const next = accountKey();
57694
- if (next !== activeAccount) {
57695
- activeAccount = next;
57639
+ const next = currentAccount$1();
57640
+ if (next !== account$1) {
57641
+ account$1 = next;
57696
57642
  recognizedSpeakerDirectory.clear();
57697
57643
  }
57698
57644
  });
@@ -57752,10 +57698,7 @@ body[data-ds-dark-theme] .arkme-bot-identity {
57752
57698
  throw error;
57753
57699
  } finally {
57754
57700
  if (context.current()) {
57755
- if (binding.account !== void 0) {
57756
- recognizedSpeakerTracker.uncertain(binding.account);
57757
- recognizedSpeakerDirectory.invalidate(binding.account);
57758
- }
57701
+ if (binding.account !== void 0) recognizedSpeakerDirectory.invalidate(binding.account);
57759
57702
  recordingSpeakerOptions.reset();
57760
57703
  recordingSpeakerOptions.invalidate();
57761
57704
  recordingSpeakerItemContexts.reset((candidateKey) => candidateKey === key || !recordingSpeakerItemContexts.get(candidateKey).mutating);
@@ -126905,11 +126848,11 @@ body[data-ds-dark-theme] .arkme-mention-all-avatar [data-arkme-theme-image="dark
126905
126848
  }
126906
126849
  const MAX_PREFERENCE = 4096;
126907
126850
  const PADDING = 12;
126908
- const preferenceKey$2 = (account) => `dsh-arkme:calendar-height:v1:${encodeURIComponent(account)}`;
126851
+ const preferenceKey$3 = (account) => `dsh-arkme:calendar-height:v1:${encodeURIComponent(account)}`;
126909
126852
  function readCalendarHeight(account) {
126910
126853
  if (!account) return void 0;
126911
126854
  try {
126912
- const value = Number(localStorage.getItem(preferenceKey$2(account)));
126855
+ const value = Number(localStorage.getItem(preferenceKey$3(account)));
126913
126856
  return Number.isFinite(value) && value >= 360 && value <= MAX_PREFERENCE ? value : void 0;
126914
126857
  } catch {
126915
126858
  return;
@@ -126918,7 +126861,7 @@ body[data-ds-dark-theme] .arkme-mention-all-avatar [data-arkme-theme-image="dark
126918
126861
  function writeCalendarHeight(account, height) {
126919
126862
  if (!account || !Number.isFinite(height)) return;
126920
126863
  try {
126921
- localStorage.setItem(preferenceKey$2(account), String(Math.round(Math.max(360, Math.min(MAX_PREFERENCE, height)))));
126864
+ localStorage.setItem(preferenceKey$3(account), String(Math.round(Math.max(360, Math.min(MAX_PREFERENCE, height)))));
126922
126865
  } catch {}
126923
126866
  }
126924
126867
  function calendarPopoverLayout(anchor, width, height, preferred) {
@@ -138440,14 +138383,14 @@ body:has([data-arkme-self-topic-menu]) > [role="menu"] { z-index: 10030; }
138440
138383
  return;
138441
138384
  }
138442
138385
  }
138443
- function preferenceKey$1(userId) {
138386
+ function preferenceKey$2(userId) {
138444
138387
  return Number.isSafeInteger(userId) && (userId ?? 0) > 0 ? `${STORAGE_KEY_PREFIX$3}${String(userId)}` : void 0;
138445
138388
  }
138446
138389
  function isSelfTopicSort(value) {
138447
138390
  return value === "latest" || value === "most" || value === "custom";
138448
138391
  }
138449
138392
  function readSelfTopicSortPreference(userId, storage) {
138450
- const key = preferenceKey$1(userId);
138393
+ const key = preferenceKey$2(userId);
138451
138394
  const target = preferenceStorage$1(storage);
138452
138395
  if (key === void 0 || target === void 0) return DEFAULT_SELF_TOPIC_SORT;
138453
138396
  try {
@@ -138458,7 +138401,7 @@ body:has([data-arkme-self-topic-menu]) > [role="menu"] { z-index: 10030; }
138458
138401
  }
138459
138402
  }
138460
138403
  function writeSelfTopicSortPreference(userId, sort, storage) {
138461
- const key = preferenceKey$1(userId);
138404
+ const key = preferenceKey$2(userId);
138462
138405
  const target = preferenceStorage$1(storage);
138463
138406
  if (key === void 0 || target === void 0) return;
138464
138407
  try {
@@ -143882,7 +143825,7 @@ body:has([data-arkme-self-topic-menu]) > [role="menu"] { z-index: 10030; }
143882
143825
  return;
143883
143826
  }
143884
143827
  }
143885
- function preferenceKey(userId) {
143828
+ function preferenceKey$1(userId) {
143886
143829
  return Number.isSafeInteger(userId) && (userId ?? 0) > 0 ? `${STORAGE_KEY_PREFIX$2}user:${String(userId)}` : `${STORAGE_KEY_PREFIX$2}anonymous`;
143887
143830
  }
143888
143831
  function isMarketplaceSort(value) {
@@ -143892,7 +143835,7 @@ body:has([data-arkme-self-topic-menu]) > [role="menu"] { z-index: 10030; }
143892
143835
  const target = preferenceStorage(storage);
143893
143836
  if (target === void 0) return DEFAULT_MARKETPLACE_SORT;
143894
143837
  try {
143895
- const value = target.getItem(preferenceKey(userId));
143838
+ const value = target.getItem(preferenceKey$1(userId));
143896
143839
  return isMarketplaceSort(value) ? value : DEFAULT_MARKETPLACE_SORT;
143897
143840
  } catch {
143898
143841
  return DEFAULT_MARKETPLACE_SORT;
@@ -143902,7 +143845,7 @@ body:has([data-arkme-self-topic-menu]) > [role="menu"] { z-index: 10030; }
143902
143845
  const target = preferenceStorage(storage);
143903
143846
  if (target === void 0) return;
143904
143847
  try {
143905
- target.setItem(preferenceKey(userId), sort);
143848
+ target.setItem(preferenceKey$1(userId), sort);
143906
143849
  } catch {}
143907
143850
  }
143908
143851
  //#endregion
@@ -155445,7 +155388,7 @@ body:has([data-arkme-self-topic-menu]) > [role="menu"] { z-index: 10030; }
155445
155388
  * Wrap `React.useLayoutEffect` which will not throw warning message in test env
155446
155389
  */
155447
155390
  const useInternalLayoutEffect = canUseDom() ? react.useLayoutEffect : react.useEffect;
155448
- const useLayoutEffect$24 = (callback, deps) => {
155391
+ const useLayoutEffect$25 = (callback, deps) => {
155449
155392
  const firstMountRef = react.useRef(true);
155450
155393
  useInternalLayoutEffect(() => {
155451
155394
  return callback(firstMountRef.current);
@@ -155489,7 +155432,7 @@ body:has([data-arkme-self-topic-menu]) > [role="menu"] { z-index: 10030; }
155489
155432
  function useControlledState(defaultStateValue, value) {
155490
155433
  const [innerValue, setInnerValue] = (0, react.useState)(defaultStateValue);
155491
155434
  const mergedValue = value !== void 0 ? value : innerValue;
155492
- useLayoutEffect$24((mount) => {
155435
+ useLayoutEffect$25((mount) => {
155493
155436
  if (!mount) setInnerValue(value);
155494
155437
  }, [value]);
155495
155438
  return [mergedValue, setInnerValue];
@@ -155552,7 +155495,7 @@ body:has([data-arkme-self-topic-menu]) > [role="menu"] { z-index: 10030; }
155552
155495
  };
155553
155496
  //#endregion
155554
155497
  //#region node_modules/@rc-component/util/es/hooks/useMemo.js
155555
- function useMemo$37(getValue, condition, shouldUpdate) {
155498
+ function useMemo$36(getValue, condition, shouldUpdate) {
155556
155499
  const cacheRef = react.useRef({});
155557
155500
  if (!("value" in cacheRef.current) || shouldUpdate(cacheRef.current.condition, condition)) {
155558
155501
  cacheRef.current.value = getValue();
@@ -155665,7 +155608,7 @@ body:has([data-arkme-self-topic-menu]) > [role="menu"] { z-index: 10030; }
155665
155608
  };
155666
155609
  };
155667
155610
  const useComposeRef = (...refs) => {
155668
- return useMemo$37(() => composeRef(...refs), refs, (prev, next) => prev.length !== next.length || prev.every((ref, i) => ref !== next[i]));
155611
+ return useMemo$36(() => composeRef(...refs), refs, (prev, next) => prev.length !== next.length || prev.every((ref, i) => ref !== next[i]));
155669
155612
  };
155670
155613
  const supportRef = (nodeOrComponent) => {
155671
155614
  if (!nodeOrComponent) return false;
@@ -156555,13 +156498,13 @@ ${height ? `height: ${webkitScrollbarStyle.height};` : ""}
156555
156498
  ele.parentElement?.removeChild(ele);
156556
156499
  appendedRef.current = false;
156557
156500
  }
156558
- useLayoutEffect$24(() => {
156501
+ useLayoutEffect$25(() => {
156559
156502
  if (render) if (queueCreate) queueCreate(append);
156560
156503
  else append();
156561
156504
  else cleanup();
156562
156505
  return cleanup;
156563
156506
  }, [render]);
156564
- useLayoutEffect$24(() => {
156507
+ useLayoutEffect$25(() => {
156565
156508
  if (queue.length) {
156566
156509
  queue.forEach((appendFn) => appendFn());
156567
156510
  setQueue(EMPTY_LIST);
@@ -156587,7 +156530,7 @@ ${height ? `height: ${webkitScrollbarStyle.height};` : ""}
156587
156530
  uuid$1 += 1;
156588
156531
  return `${UNIQUE_ID}_${uuid$1}`;
156589
156532
  });
156590
- useLayoutEffect$24(() => {
156533
+ useLayoutEffect$25(() => {
156591
156534
  if (mergedLock) {
156592
156535
  const scrollbarSize = getTargetScrollBarSize(document.body).width;
156593
156536
  updateCSS(`
@@ -157530,7 +157473,7 @@ html body {
157530
157473
  ]);
157531
157474
  const getPopupContainerNeedParams = getPopupContainer?.length > 0;
157532
157475
  const [show, setShow] = react.useState(!getPopupContainer || !getPopupContainerNeedParams);
157533
- useLayoutEffect$24(() => {
157476
+ useLayoutEffect$25(() => {
157534
157477
  if (!show && getPopupContainerNeedParams && target) setShow(true);
157535
157478
  }, [
157536
157479
  show,
@@ -158082,8 +158025,8 @@ html body {
158082
158025
  ready: false
158083
158026
  }));
158084
158027
  };
158085
- useLayoutEffect$24(resetReady, [placement]);
158086
- useLayoutEffect$24(() => {
158028
+ useLayoutEffect$25(resetReady, [placement]);
158029
+ useLayoutEffect$25(() => {
158087
158030
  if (!open) resetReady();
158088
158031
  }, [open]);
158089
158032
  return [
@@ -158127,7 +158070,7 @@ html body {
158127
158070
  //#endregion
158128
158071
  //#region node_modules/@rc-component/trigger/es/hooks/useWatch.js
158129
158072
  function useWatch(open, target, popup, onAlign, onScroll) {
158130
- useLayoutEffect$24(() => {
158073
+ useLayoutEffect$25(() => {
158131
158074
  if (open && target && popup) {
158132
158075
  const targetElement = target;
158133
158076
  const popupElement = popup;
@@ -158282,7 +158225,7 @@ html body {
158282
158225
  id,
158283
158226
  onEsc
158284
158227
  }));
158285
- useLayoutEffect$24(() => {
158228
+ useLayoutEffect$25(() => {
158286
158229
  if (uniqueContext && unique && targetEle && !openUncontrolled && !parentContext) if (mergedOpen) uniqueContext.show(getUniqueOptions(mouseEnterDelay), isOpen);
158287
158230
  else uniqueContext.hide(mouseLeaveDelay);
158288
158231
  }, [mergedOpen, targetEle]);
@@ -158318,7 +158261,7 @@ html body {
158318
158261
  if (top) triggerOpen(false);
158319
158262
  }
158320
158263
  const [inMotion, setInMotion] = react.useState(false);
158321
- useLayoutEffect$24((firstMount) => {
158264
+ useLayoutEffect$25((firstMount) => {
158322
158265
  if (!firstMount || mergedOpen) setInMotion(true);
158323
158266
  }, [mergedOpen]);
158324
158267
  const [motionPrepareResolve, setMotionPrepareResolve] = react.useState(null);
@@ -158337,10 +158280,10 @@ html body {
158337
158280
  if (openRef.current && alignPoint && clickToHide) triggerOpen(false);
158338
158281
  };
158339
158282
  useWatch(mergedOpen, targetEle, popupEle, triggerAlign, onScroll);
158340
- useLayoutEffect$24(() => {
158283
+ useLayoutEffect$25(() => {
158341
158284
  triggerAlign();
158342
158285
  }, [mousePos, popupPlacement]);
158343
- useLayoutEffect$24(() => {
158286
+ useLayoutEffect$25(() => {
158344
158287
  if (mergedOpen && !builtinPlacements?.[popupPlacement]) triggerAlign();
158345
158288
  }, [JSON.stringify(popupAlign)]);
158346
158289
  const alignedClassName = react.useMemo(() => {
@@ -158380,7 +158323,7 @@ html body {
158380
158323
  syncTargetSize();
158381
158324
  setMotionPrepareResolve(() => resolve);
158382
158325
  });
158383
- useLayoutEffect$24(() => {
158326
+ useLayoutEffect$25(() => {
158384
158327
  if (motionPrepareResolve) {
158385
158328
  onAlign();
158386
158329
  motionPrepareResolve();
@@ -158587,7 +158530,7 @@ html body {
158587
158530
  }
158588
158531
  function useTarget(target, open, gap, scrollIntoViewOptions, inlineMode, placeholderRef) {
158589
158532
  const [targetElement, setTargetElement] = (0, react.useState)(void 0);
158590
- useLayoutEffect$24(() => {
158533
+ useLayoutEffect$25(() => {
158591
158534
  const nextElement = typeof target === "function" ? target() : target;
158592
158535
  setTargetElement(nextElement || null);
158593
158536
  });
@@ -158617,7 +158560,7 @@ html body {
158617
158560
  } else setPosInfo(null);
158618
158561
  });
158619
158562
  const getGapOffset = (index) => (Array.isArray(gap?.offset) ? gap?.offset[index] : gap?.offset) ?? 6;
158620
- useLayoutEffect$24(() => {
158563
+ useLayoutEffect$25(() => {
158621
158564
  updatePos();
158622
158565
  window.addEventListener("resize", updatePos);
158623
158566
  window.addEventListener("scroll", updatePos);
@@ -158907,7 +158850,7 @@ html body {
158907
158850
  const mergedOpen = mergedCurrent < 0 || mergedCurrent >= steps.length ? false : internalOpen ?? true;
158908
158851
  const [hasOpened, setHasOpened] = react.useState(mergedOpen);
158909
158852
  const openRef = react.useRef(mergedOpen);
158910
- useLayoutEffect$24(() => {
158853
+ useLayoutEffect$25(() => {
158911
158854
  if (mergedOpen) {
158912
158855
  if (!openRef.current) setMergedCurrent(0);
158913
158856
  setHasOpened(true);
@@ -158924,7 +158867,7 @@ html body {
158924
158867
  const mergedPlacement = getPlacement(targetElement, placement, stepPlacement);
158925
158868
  const mergedArrow = targetElement ? typeof stepArrow === "undefined" ? arrow : stepArrow : false;
158926
158869
  const arrowPointAtCenter = typeof mergedArrow === "object" ? mergedArrow.pointAtCenter : false;
158927
- useLayoutEffect$24(() => {
158870
+ useLayoutEffect$25(() => {
158928
158871
  triggerRef.current?.forceAlign();
158929
158872
  }, [arrowPointAtCenter, mergedCurrent]);
158930
158873
  const onInternalChange = (nextCurrent) => {
@@ -158962,7 +158905,7 @@ html body {
158962
158905
  return;
158963
158906
  }
158964
158907
  });
158965
- useLayoutEffect$24(() => {
158908
+ useLayoutEffect$25(() => {
158966
158909
  if (!mergedOpen) return;
158967
158910
  window.addEventListener("keydown", keyboardHandler);
158968
158911
  return () => {
@@ -166447,7 +166390,7 @@ html body {
166447
166390
  //#endregion
166448
166391
  //#region src/client/self-topic-expansion-preference.ts
166449
166392
  const EMPTY$1 = /* @__PURE__ */ new Set();
166450
- function browserStorage() {
166393
+ function browserStorage$1() {
166451
166394
  try {
166452
166395
  return typeof window === "undefined" ? void 0 : window.localStorage;
166453
166396
  } catch {
@@ -166466,7 +166409,7 @@ html body {
166466
166409
  storage;
166467
166410
  value;
166468
166411
  listeners = /* @__PURE__ */ new Set();
166469
- constructor(key, storage = browserStorage()) {
166412
+ constructor(key, storage = browserStorage$1()) {
166470
166413
  this.key = key;
166471
166414
  this.storage = storage;
166472
166415
  this.value = this.read();
@@ -179003,73 +178946,19 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
179003
178946
  const p$2 = e$36;
179004
178947
  //#endregion
179005
178948
  //#region src/client/recordings/RecognizedSpeakerEntry.tsx
179006
- function RecognizedSpeakerEntry({ accountKey, active, onOpen, style, readMarked = readSpeakerOptions, readPage = readSpeakerPage, directory: providedDirectory }) {
178949
+ function RecognizedSpeakerEntry({ accountKey, active, onOpen, style, directory = recognizedSpeakerDirectory }) {
179007
178950
  useArkmeLocale();
179008
- const directory = (0, react$1.useMemo)(() => providedDirectory ?? (readMarked === readSpeakerOptions && readPage === readSpeakerPage ? recognizedSpeakerDirectory : new RecognizedSpeakerDirectory({
179009
- marked: readMarked,
179010
- page: readPage,
179011
- presence: readSpeakerPresence
179012
- }, 0)), [
179013
- providedDirectory,
179014
- readMarked,
179015
- readPage
179016
- ]);
179017
- const snapshot = (0, react$1.useSyncExternalStore)(recognizedSpeakerTracker.subscribe, () => recognizedSpeakerTracker.get(accountKey), () => recognizedSpeakerTracker.get(accountKey));
178951
+ const snapshot = (0, react$1.useSyncExternalStore)(directory.subscribe, () => directory.get(accountKey), () => directory.get(accountKey));
179018
178952
  (0, react$1.useEffect)(() => {
179019
- if (!active || accountKey === void 0) return;
179020
- const controller = new AbortController();
179021
- let busy = false;
179022
- let retryTimer;
179023
- const refresh = async () => {
179024
- if (busy || controller.signal.aborted || typeof document !== "undefined" && document.visibilityState === "hidden") return;
179025
- busy = true;
179026
- try {
179027
- let marked;
179028
- const updateTotal = () => {
179029
- const value = directory.peekCandidates(accountKey);
179030
- if (marked !== void 0 && value?.total !== void 0 && !controller.signal.aborted) recognizedSpeakerTracker.setTotal(accountKey, markedSpeakerKeys(marked).length + value.total);
179031
- };
179032
- const [, result] = await Promise.all([directory.readMarked(accountKey, controller.signal).then((value) => {
179033
- marked = value;
179034
- updateTotal();
179035
- }), directory.readCandidates(accountKey, controller.signal, updateTotal)]);
179036
- if (controller.signal.aborted) return;
179037
- if (result.complete && marked !== void 0) recognizedSpeakerTracker.observe(accountKey, marked, result.items);
179038
- else recognizedSpeakerTracker.uncertain(accountKey);
179039
- } catch (error) {
179040
- if (!controller.signal.aborted) {
179041
- recognizedSpeakerTracker.uncertain(accountKey);
179042
- const delay = speakerRetryDelay(error);
179043
- if (delay !== void 0) {
179044
- clearTimeout(retryTimer);
179045
- retryTimer = setTimeout(() => {
179046
- refresh();
179047
- }, delay);
179048
- }
179049
- }
179050
- } finally {
179051
- busy = false;
179052
- }
179053
- };
179054
- refresh();
179055
- const timer = setInterval(() => {
179056
- refresh();
179057
- }, 6e4);
179058
- const onVisible = () => {
179059
- refresh();
179060
- };
179061
- if (typeof document !== "undefined") document.addEventListener("visibilitychange", onVisible);
179062
- return () => {
179063
- controller.abort();
179064
- clearInterval(timer);
179065
- clearTimeout(retryTimer);
179066
- if (typeof document !== "undefined") document.removeEventListener("visibilitychange", onVisible);
179067
- };
178953
+ if (active && accountKey !== void 0) return directory.watch(accountKey);
179068
178954
  }, [
179069
178955
  accountKey,
179070
178956
  active,
179071
178957
  directory
179072
178958
  ]);
178959
+ const summary = snapshot.summary;
178960
+ const count = summary?.state === "disabled" ? null : summary?.totalCount;
178961
+ const hint = summary?.state === "disabled" ? "目录暂不可用" : count == null ? summary?.state === "building" ? "整理中…" : snapshot.error || summary?.state === "failed" ? "暂时无法更新" : "" : (summary?.unseenCount ?? 0) > 0 ? tr$1("新识别 {v0} 个", { v0: summary.unseenCount }) : "";
179073
178962
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
179074
178963
  "data-arkme-feedback": "recording-action",
179075
178964
  type: "button",
@@ -179103,12 +178992,12 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
179103
178992
  },
179104
178993
  children: tr$1("已识别说话人")
179105
178994
  }),
179106
- snapshot.total !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
178995
+ count != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
179107
178996
  style: {
179108
178997
  flexShrink: 0,
179109
178998
  fontVariantNumeric: "tabular-nums"
179110
178999
  },
179111
- children: snapshot.total
179000
+ children: count
179112
179001
  }),
179113
179002
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(s$21, {
179114
179003
  size: 12,
@@ -179116,14 +179005,14 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
179116
179005
  "aria-hidden": true
179117
179006
  })
179118
179007
  ]
179119
- }), (snapshot.newCount ?? 0) > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
179008
+ }), hint && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
179120
179009
  style: {
179121
179010
  fontSize: 11,
179122
179011
  lineHeight: "15px",
179123
179012
  fontWeight: 400
179124
179013
  },
179125
179014
  "aria-live": "polite",
179126
- children: tr$1("新识别 {v0} 个", { v0: snapshot.newCount })
179015
+ children: tr$1(hint)
179127
179016
  })]
179128
179017
  });
179129
179018
  }
@@ -191460,6 +191349,28 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
191460
191349
  });
191461
191350
  }
191462
191351
  //#endregion
191352
+ //#region src/client/recognized-speaker-order.ts
191353
+ const preferenceKey = (accountKey) => `dsh-arkme:recognized-speaker-order:v1:${accountKey}`;
191354
+ function browserStorage() {
191355
+ try {
191356
+ return typeof window === "undefined" ? void 0 : window.localStorage;
191357
+ } catch {
191358
+ return;
191359
+ }
191360
+ }
191361
+ function readRecognizedSpeakerOrder(accountKey, storage = browserStorage()) {
191362
+ try {
191363
+ return storage?.getItem(preferenceKey(accountKey)) === "recent" ? "recent" : "frequent";
191364
+ } catch {
191365
+ return "frequent";
191366
+ }
191367
+ }
191368
+ function writeRecognizedSpeakerOrder(accountKey, order, storage = browserStorage()) {
191369
+ try {
191370
+ storage?.setItem(preferenceKey(accountKey), order);
191371
+ } catch {}
191372
+ }
191373
+ //#endregion
191463
191374
  //#region src/client/recordings/SpeakerSelfGuide.tsx
191464
191375
  const loadVoiceprint = (signal) => callArkme("voiceprint.status", {}, signal);
191465
191376
  function SpeakerSelfGuide({ onOpenRecordings, readVoiceprint = loadVoiceprint }) {
@@ -191482,7 +191393,7 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
191482
191393
  if (state.loading) return null;
191483
191394
  const pending = state.value?.enrollmentPending === true;
191484
191395
  const hasVoiceprint = state.value?.hasVoiceprint === true;
191485
- const title = pending ? "你的声纹正在处理中" : hasVoiceprint ? "已录入声纹,暂未在录音中匹配到你" : "还没有标记你的声音";
191396
+ const title = state.value === void 0 ? "声纹状态暂不可用" : pending ? "你的声纹正在处理中" : hasVoiceprint ? "已录入声纹,暂未在录音中匹配到你" : "还没有标记你的声音";
191486
191397
  const actionStyle = {
191487
191398
  border: 0,
191488
191399
  padding: 0,
@@ -191537,39 +191448,6 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
191537
191448
  }
191538
191449
  //#endregion
191539
191450
  //#region src/client/ArkmeRecognizedSpeakersSurface.tsx
191540
- /** Group only by a stable owner identity, never by a shared name or speaker number. */
191541
- function identifiedSpeakerRows(options, candidates) {
191542
- const marked = /* @__PURE__ */ new Map();
191543
- for (const option of options) {
191544
- if (option.kind !== "speaker") continue;
191545
- const key = option.personKey || option.optionKey;
191546
- if (marked.has(key)) continue;
191547
- marked.set(key, {
191548
- kind: "marked",
191549
- key,
191550
- optionKey: option.optionKey,
191551
- speakerRef: option.speakerRef,
191552
- name: option.label.trim() || "未命名说话人",
191553
- ...option.avatarRef === void 0 ? {} : { avatarRef: option.avatarRef },
191554
- isCurrentUser: option.isCurrentUser
191555
- });
191556
- }
191557
- const unmarked = /* @__PURE__ */ new Map();
191558
- for (const candidate of candidates) {
191559
- if (unmarked.has(candidate.candidateRef)) continue;
191560
- unmarked.set(candidate.candidateRef, {
191561
- kind: "unmarked",
191562
- key: candidate.candidateRef,
191563
- name: tr$1("说话人 {v0}", { v0: candidate.speakerToken?.trim() || candidate.displayName }),
191564
- token: candidate.speakerToken?.trim() || "",
191565
- subtitle: candidate.subtitle,
191566
- candidateRef: candidate.candidateRef,
191567
- dayCount: candidate.appearanceDays,
191568
- lastSeenAt: candidate.latestAtMillis
191569
- });
191570
- }
191571
- return [...[...marked.values()].sort((left, right) => left.name.localeCompare(right.name)), ...unmarked.values()];
191572
- }
191573
191451
  const styles$20 = {
191574
191452
  root: {
191575
191453
  flex: 1,
@@ -191757,87 +191635,133 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
191757
191635
  borderBottom: `1px solid ${arkmeTheme.borderSoft}`
191758
191636
  }
191759
191637
  };
191760
- const defaultLoadMarkedMembers = async (speakerRef, signal, expectedVersion) => await callArkme("recordings.speaker.members", {
191638
+ const defaultLoadMarkedMembers = (speakerRef, signal, expectedVersion) => callArkme("recordings.speaker.members", {
191761
191639
  speakerRef,
191762
191640
  ...expectedVersion === void 0 ? {} : { expectedVersion }
191763
191641
  }, signal);
191764
- function failureMessage(error) {
191765
- return error instanceof Error && error.message.trim() !== "" ? error.message : tr$1("说话人列表暂时无法加载");
191766
- }
191767
- function markedPresenceLabel(stat, result, loading, error) {
191768
- if (loading || result?.state === "building") return tr$1("出现统计整理中");
191769
- if (error !== "" || result?.state === "failed" || result === void 0) return tr$1("出现统计暂不可用");
191770
- if (result.state === "stale") return tr$1("出现统计更新中");
191771
- if (stat !== void 0) {
191772
- const date = new Date(stat.lastSeenAt);
191773
- if (!Number.isNaN(date.getTime())) {
191774
- const two = (value) => String(value).padStart(2, "0");
191775
- const recent = `${date.getFullYear()}-${two(date.getMonth() + 1)}-${two(date.getDate())} ${two(date.getHours())}:${two(date.getMinutes())}`;
191776
- return tr$1("出现 {v0} 天 · 最近 {v1}", {
191777
- v0: stat.dayCount,
191778
- v1: recent
191779
- });
191780
- }
191781
- }
191782
- return tr$1("暂无可统计的录音片段");
191783
- }
191784
- function MarkedSpeakerDetail({ accountKey, speaker, loadMembers, version, onVersionChanged }) {
191785
- const [state, setState] = (0, react$1.useState)({
191786
- loading: true,
191787
- error: ""
191642
+ const failureMessage = (error) => error instanceof Error ? error.message : tr$1("说话人列表暂时无法加载");
191643
+ const nameSegmenter = new Intl.Segmenter("zh-CN", { granularity: "grapheme" });
191644
+ function speakerNameAvatarFallback(row) {
191645
+ const initial = nameSegmenter.segment(row.displayName.trim())[Symbol.iterator]().next().value?.segment;
191646
+ if (!initial) return void 0;
191647
+ let hash = 0;
191648
+ for (const character of row.personKey) hash = Math.imul(hash, 31) + character.codePointAt(0) >>> 0;
191649
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
191650
+ "aria-hidden": true,
191651
+ style: {
191652
+ width: "100%",
191653
+ height: "100%",
191654
+ display: "grid",
191655
+ placeItems: "center",
191656
+ borderRadius: "50%",
191657
+ background: `color-mix(in srgb, ${recordingSpeakerColor(hash % 15)} 30%, ${arkmeTheme.base})`,
191658
+ color: arkmeTheme.text,
191659
+ fontSize: 17,
191660
+ fontWeight: 600,
191661
+ lineHeight: 1
191662
+ },
191663
+ children: initial
191788
191664
  });
191665
+ }
191666
+ function personStats(row) {
191667
+ const recent = row.lastSeenAt > 0 ? new Intl.DateTimeFormat(arkmeIntlLocale(), {
191668
+ year: "numeric",
191669
+ month: "2-digit",
191670
+ day: "2-digit",
191671
+ hour: "2-digit",
191672
+ minute: "2-digit",
191673
+ hour12: false
191674
+ }).format(row.lastSeenAt) : tr$1("时间未知");
191675
+ return tr$1("出现 {v0} 天 · 最近 {v1}", {
191676
+ v0: row.dayCount,
191677
+ v1: recent
191678
+ });
191679
+ }
191680
+ function statusText(status) {
191681
+ if (status?.state === "disabled") return tr$1("说话人目录暂不可用,请稍后再试。");
191682
+ if (status?.coverage === "unknown") return status.state === "failed" ? tr$1("说话人目录暂不可用,请稍后刷新。") : tr$1("正在整理说话人目录…");
191683
+ if (status?.state === "failed") return tr$1("更新暂未完成,当前显示上次整理的目录。");
191684
+ if (status?.state === "stale") return tr$1("正在更新,当前显示已整理的目录。");
191685
+ if (status && (status.processingSessionCount > 0 || status.pendingIdentityAggregationCount > 0 || status.insufficientEvidenceCount > 0 || status.scanTruncated)) return tr$1("部分录音仍在整理,人物可能继续更新。");
191686
+ return "";
191687
+ }
191688
+ function PersonDetail({ row, accountKey, directory, loadMembers, onChanged }) {
191689
+ const [detail, setDetail] = (0, react$1.useState)();
191690
+ const [members, setMembers] = (0, react$1.useState)();
191691
+ const [error, setError] = (0, react$1.useState)("");
191692
+ const [revision, setRevision] = (0, react$1.useState)(0);
191789
191693
  (0, react$1.useEffect)(() => {
191790
191694
  const controller = new AbortController();
191791
- setState({
191792
- loading: true,
191793
- error: ""
191794
- });
191795
- let timer;
191796
- const load = async () => {
191797
- try {
191798
- const result = await loadMembers(speaker.speakerRef, controller.signal, version);
191695
+ let retry;
191696
+ setDetail(void 0);
191697
+ setMembers(void 0);
191698
+ setError("");
191699
+ directory.open(accountKey, row.detailRef, controller.signal).then(async (value) => {
191700
+ if (controller.signal.aborted) return;
191701
+ setDetail(value);
191702
+ if (value.type === "speaker") {
191703
+ const result = await loadMembers(value.speakerRef, controller.signal, value.expectedVersion);
191799
191704
  if (controller.signal.aborted) return;
191800
- setState({
191801
- loading: false,
191802
- result,
191803
- error: ""
191804
- });
191805
- if (result.state === "stale" && result.version !== void 0 && result.version !== version) onVersionChanged();
191806
- if (result.state !== "fresh") timer = setTimeout(() => {
191807
- load();
191705
+ setMembers(result);
191706
+ if (result.state === "stale") onChanged();
191707
+ else if (result.state !== "fresh") retry = setTimeout(() => {
191708
+ if (directoryVisible()) setRevision((value) => value + 1);
191808
191709
  }, Math.max(5e3, result.retryAfterMs ?? 1e4));
191809
- } catch (error) {
191810
- if (!controller.signal.aborted) setState({
191811
- loading: false,
191812
- error: failureMessage(error)
191813
- });
191814
191710
  }
191815
- };
191816
- load();
191711
+ }).catch((error) => {
191712
+ if (controller.signal.aborted) return;
191713
+ if (directoryErrorCode(error) === "unmarked-candidate-not-found") onChanged();
191714
+ else setError(failureMessage(error));
191715
+ });
191817
191716
  return () => {
191818
- clearTimeout(timer);
191819
191717
  controller.abort();
191718
+ clearTimeout(retry);
191820
191719
  };
191821
191720
  }, [
191721
+ row.detailRef,
191822
191722
  accountKey,
191823
- speaker.speakerRef,
191723
+ directory,
191824
191724
  loadMembers,
191825
- version,
191826
- onVersionChanged
191725
+ onChanged,
191726
+ revision
191827
191727
  ]);
191728
+ if (error) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
191729
+ role: "alert",
191730
+ style: styles$20.error,
191731
+ children: [
191732
+ error,
191733
+ " ",
191734
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
191735
+ style: styles$20.button,
191736
+ onClick: () => setRevision((value) => value + 1),
191737
+ children: tr$1("重试")
191738
+ })
191739
+ ]
191740
+ });
191741
+ if (!detail) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
191742
+ role: "status",
191743
+ style: styles$20.state,
191744
+ children: tr$1("正在加载人物详情…")
191745
+ });
191746
+ if (detail.type === "candidate") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UnmarkedSpeakerDetail, {
191747
+ accountKey,
191748
+ candidateRef: detail.candidateRef,
191749
+ onDirectoryRefresh: onChanged,
191750
+ onCandidateCleared: onChanged
191751
+ });
191828
191752
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
191829
191753
  "aria-label": tr$1("已标记说话人详情"),
191830
191754
  children: [
191831
191755
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h2", {
191832
191756
  style: styles$20.detailTitle,
191833
- children: [speaker.name, speaker.isCurrentUser ? ` · ${tr$1("我")}` : ""]
191757
+ children: [row.displayName, row.isSelf ? ` · ${tr$1("我")}` : ""]
191834
191758
  }),
191835
191759
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
191836
191760
  style: {
191837
191761
  ...styles$20.meta,
191838
191762
  margin: 0
191839
191763
  },
191840
- children: tr$1("查看已确认归属的原始识别身份")
191764
+ children: personStats(row)
191841
191765
  }),
191842
191766
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
191843
191767
  style: styles$20.detailSection,
@@ -191846,30 +191770,20 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
191846
191770
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
191847
191771
  style: {
191848
191772
  ...styles$20.meta,
191849
- margin: "0 0 8px",
191850
191773
  whiteSpace: "normal"
191851
191774
  },
191852
191775
  children: tr$1("展示全部历史已转写片段中可核实的关联。")
191853
191776
  }),
191854
- state.loading || state.result?.state === "building" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
191777
+ !members || members.state !== "fresh" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
191855
191778
  role: "status",
191856
191779
  style: styles$20.state,
191857
- children: tr$1("正在查找关联说话人…")
191858
- }) : state.error !== "" || state.result?.state === "failed" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
191859
- role: "alert",
191860
- style: styles$20.error,
191861
- children: state.error || tr$1("出现统计暂不可用")
191862
- }) : state.result?.state === "stale" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
191863
- role: "status",
191864
- style: styles$20.state,
191865
- children: tr$1("出现统计更新中")
191866
- }) : state.result?.items.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
191867
- role: "status",
191780
+ children: members?.state === "failed" ? tr$1("出现统计暂不可用") : tr$1("出现统计整理中")
191781
+ }) : members.items.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
191868
191782
  style: styles$20.state,
191869
- children: state.result.dayCount > 0 ? tr$1("有发声,但未找到稳定的原始识别身份。") : tr$1("暂无可核实的关联说话人。")
191783
+ children: tr$1("暂无可核实的关联说话人。")
191870
191784
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
191871
191785
  style: styles$20.list,
191872
- children: state.result?.items.map((member) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
191786
+ children: members.items.map((member) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
191873
191787
  style: styles$20.memberRow,
191874
191788
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UnmarkedSpeakerTokenAvatar, {
191875
191789
  token: member.token,
@@ -191890,274 +191804,251 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
191890
191804
  ]
191891
191805
  });
191892
191806
  }
191893
- function ArkmeRecognizedSpeakersSurface({ accountKey, onBack, loadMarked = readSpeakerOptions, loadPresence = readSpeakerPresence, loadMarkedMembers = defaultLoadMarkedMembers, loadUnmarked = readSpeakerPage, directory: providedDirectory }) {
191807
+ function ArkmeRecognizedSpeakersSurface(props) {
191808
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DirectorySurface, { ...props }, props.accountKey);
191809
+ }
191810
+ function DirectorySurface({ accountKey, onBack, directory = recognizedSpeakerDirectory, loadMarkedMembers = defaultLoadMarkedMembers }) {
191894
191811
  useArkmeLocale();
191895
- const directory = (0, react$1.useMemo)(() => providedDirectory ?? (loadMarked === readSpeakerOptions && loadUnmarked === readSpeakerPage && loadPresence === readSpeakerPresence ? recognizedSpeakerDirectory : new RecognizedSpeakerDirectory({
191896
- marked: loadMarked,
191897
- page: loadUnmarked,
191898
- presence: loadPresence
191899
- }, 0)), [
191900
- providedDirectory,
191901
- loadMarked,
191902
- loadUnmarked,
191903
- loadPresence
191904
- ]);
191905
191812
  const [filter, setFilter] = (0, react$1.useState)("all");
191906
- const [query, setQuery] = (0, react$1.useState)("");
191907
- const [orderPreference, setOrderPreference] = (0, react$1.useState)(() => ({
191908
- accountKey,
191909
- value: readRecognizedSpeakerOrder(accountKey)
191910
- }));
191911
- const order = orderPreference.accountKey === accountKey ? orderPreference.value : readRecognizedSpeakerOrder(accountKey);
191912
- const [visibleCount, setVisibleCount] = (0, react$1.useState)(100);
191913
- const [refreshRevision, setRefreshRevision] = (0, react$1.useState)(0);
191914
- const [marked, setMarked] = (0, react$1.useState)({
191915
- loading: true,
191916
- items: [],
191917
- error: ""
191918
- });
191919
- const [presence, setPresence] = (0, react$1.useState)({
191920
- loading: true,
191921
- error: ""
191922
- });
191923
- const [unmarked, setUnmarked] = (0, react$1.useState)({
191924
- loading: true,
191925
- complete: false,
191926
- items: [],
191927
- projectionState: void 0,
191928
- error: ""
191929
- });
191930
- const [selectedCandidate, setSelectedCandidate] = (0, react$1.useState)();
191931
- const [selectedMarked, setSelectedMarked] = (0, react$1.useState)();
191932
- const lastAccountKey = (0, react$1.useRef)(accountKey);
191933
- const readAccount = (0, react$1.useRef)();
191934
- const refresh = (0, react$1.useCallback)(() => {
191935
- directory.invalidate(accountKey);
191936
- setSelectedMarked(void 0);
191937
- setRefreshRevision((value) => value + 1);
191938
- }, [directory, accountKey]);
191813
+ const [input, setInput] = (0, react$1.useState)(""), [queryText, setQueryText] = (0, react$1.useState)("");
191814
+ const [sort, setSort] = (0, react$1.useState)(() => readRecognizedSpeakerOrder(accountKey));
191815
+ const query = (0, react$1.useMemo)(() => ({
191816
+ filter,
191817
+ sort,
191818
+ query: queryText
191819
+ }), [
191820
+ filter,
191821
+ sort,
191822
+ queryText
191823
+ ]);
191824
+ const [list, setList] = (0, react$1.useState)();
191825
+ const listRef = (0, react$1.useRef)(list);
191826
+ listRef.current = list;
191827
+ const [loading, setLoading] = (0, react$1.useState)(true), [moreLoading, setMoreLoading] = (0, react$1.useState)(false);
191828
+ const [error, setError] = (0, react$1.useState)(""), [seenError, setSeenError] = (0, react$1.useState)("");
191829
+ const [selected, setSelected] = (0, react$1.useState)();
191830
+ const [avatars, setAvatars] = (0, react$1.useState)({});
191831
+ const rootRef = (0, react$1.useRef)(null), listScroll = (0, react$1.useRef)(0), hadSelection = (0, react$1.useRef)(false);
191832
+ (0, react$1.useLayoutEffect)(() => {
191833
+ const root = rootRef.current;
191834
+ if (root && root.clientWidth <= 680) {
191835
+ if (selected && !hadSelection.current) {
191836
+ listScroll.current = root.scrollTop;
191837
+ root.scrollTop = 0;
191838
+ } else if (!selected && hadSelection.current) root.scrollTop = listScroll.current;
191839
+ }
191840
+ hadSelection.current = selected !== void 0;
191841
+ }, [selected]);
191842
+ const [revision, setRevision] = (0, react$1.useState)(0);
191843
+ const force = (0, react$1.useRef)(false), recoveryAttempts = (0, react$1.useRef)(0);
191844
+ const controllerRef = (0, react$1.useRef)();
191845
+ const snapshot = (0, react$1.useSyncExternalStore)(directory.subscribe, () => directory.get(accountKey), () => directory.get(accountKey));
191846
+ const summary = snapshot.summary;
191847
+ const queryValid = directoryQueryValid(input);
191848
+ const queryPending = normalizeDirectoryQuery(input) !== queryText;
191849
+ const listMatches = list?.query.filter === filter && list.query.sort === sort && list.query.query === queryText;
191939
191850
  (0, react$1.useEffect)(() => {
191940
- setVisibleCount(100);
191851
+ if (!directory.hasPendingSeen(accountKey)) setSeenError("");
191941
191852
  }, [
191853
+ directory,
191942
191854
  accountKey,
191943
- filter,
191944
- query,
191945
- order
191855
+ snapshot
191946
191856
  ]);
191857
+ (0, react$1.useEffect)(() => {
191858
+ const timer = setTimeout(() => setQueryText(normalizeDirectoryQuery(input)), 300);
191859
+ return () => clearTimeout(timer);
191860
+ }, [input]);
191861
+ const refresh = (0, react$1.useCallback)(() => {
191862
+ force.current = true;
191863
+ directory.invalidate(accountKey);
191864
+ setRevision((value) => value + 1);
191865
+ }, [directory, accountKey]);
191947
191866
  (0, react$1.useEffect)(() => {
191948
191867
  const controller = new AbortController();
191949
- const retryTimers = [];
191950
- const retry = (error, read) => {
191951
- const delay = speakerRetryDelay(error);
191952
- if (delay !== void 0) retryTimers.push(setTimeout(() => {
191953
- if (!controller.signal.aborted) read();
191954
- }, delay));
191955
- };
191956
- readAccount.current = void 0;
191957
- if (lastAccountKey.current !== accountKey) {
191958
- lastAccountKey.current = accountKey;
191959
- setMarked({
191960
- loading: true,
191961
- items: [],
191962
- error: ""
191963
- });
191964
- setPresence({
191965
- loading: true,
191966
- error: ""
191967
- });
191968
- setUnmarked({
191969
- loading: true,
191970
- complete: false,
191971
- items: [],
191972
- projectionState: void 0,
191973
- error: ""
191974
- });
191975
- setOrderPreference({
191976
- accountKey,
191977
- value: readRecognizedSpeakerOrder(accountKey)
191978
- });
191979
- setQuery("");
191980
- setSelectedMarked(void 0);
191981
- setSelectedCandidate(void 0);
191982
- }
191983
- setMarked((previous) => ({
191984
- ...previous,
191985
- loading: true,
191986
- error: ""
191987
- }));
191988
- setPresence((previous) => ({
191989
- ...previous,
191990
- loading: true,
191991
- error: ""
191992
- }));
191993
- setUnmarked((previous) => ({
191994
- ...previous,
191995
- loading: true,
191996
- complete: false,
191997
- error: ""
191998
- }));
191999
- const cachedMarked = directory.peekMarked(accountKey);
192000
- if (cachedMarked !== void 0) setMarked({
192001
- loading: false,
192002
- items: cachedMarked,
192003
- error: ""
192004
- });
192005
- const readMarked = () => {
192006
- directory.readMarked(accountKey, controller.signal).then((items) => {
192007
- if (!controller.signal.aborted) setMarked({
192008
- loading: false,
192009
- items,
192010
- error: ""
192011
- });
192012
- }).catch((error) => {
191868
+ controllerRef.current = controller;
191869
+ let retry;
191870
+ let busy = false;
191871
+ const cached = directory.peek(accountKey, query);
191872
+ setList(cached);
191873
+ setSelected(void 0);
191874
+ setError("");
191875
+ setMoreLoading(false);
191876
+ const load = async () => {
191877
+ if (busy || !directoryVisible() || controller.signal.aborted) return;
191878
+ if (!directoryQueryValid(query.query)) {
191879
+ setLoading(false);
191880
+ return;
191881
+ }
191882
+ busy = true;
191883
+ setLoading(true);
191884
+ try {
191885
+ const result = await directory.first(accountKey, query, controller.signal, force.current);
192013
191886
  if (!controller.signal.aborted) {
192014
- setMarked((previous) => ({
192015
- ...previous,
192016
- loading: false,
192017
- error: failureMessage(error)
192018
- }));
192019
- retry(error, readMarked);
191887
+ force.current = false;
191888
+ setList(result);
191889
+ setError("");
192020
191890
  }
192021
- });
191891
+ } catch (error) {
191892
+ if (controller.signal.aborted) return;
191893
+ setError(failureMessage(error));
191894
+ const delay = speakerRetryDelay(error);
191895
+ if (delay !== void 0) retry = setTimeout(() => {
191896
+ load();
191897
+ }, delay);
191898
+ } finally {
191899
+ busy = false;
191900
+ if (!controller.signal.aborted) setLoading(false);
191901
+ }
192022
191902
  };
192023
- readMarked();
192024
- const cachedPresence = directory.peekPresence(accountKey);
192025
- if (cachedPresence !== void 0) setPresence({
192026
- loading: false,
192027
- result: cachedPresence,
192028
- error: ""
191903
+ load();
191904
+ const unwatch = directory.watch(accountKey, () => {
191905
+ if (controller.signal.aborted) return;
191906
+ const current = listRef.current;
191907
+ if (!current || current.coverage === "unknown" || directory.needsReload(accountKey, current)) load();
192029
191908
  });
192030
- const readPresence = () => {
192031
- directory.readPresence(accountKey, controller.signal).then((result) => {
192032
- if (!controller.signal.aborted) setPresence({
192033
- loading: false,
192034
- result,
192035
- error: ""
192036
- });
192037
- }).catch((error) => {
192038
- if (!controller.signal.aborted) {
192039
- setPresence((previous) => ({
192040
- ...previous,
192041
- loading: false,
192042
- error: failureMessage(error)
192043
- }));
192044
- retry(error, readPresence);
192045
- }
192046
- });
191909
+ return () => {
191910
+ controller.abort();
191911
+ clearTimeout(retry);
191912
+ unwatch();
192047
191913
  };
192048
- readPresence();
192049
- const readCandidates = () => {
192050
- directory.readCandidates(accountKey, controller.signal, (snapshot) => {
192051
- if (!controller.signal.aborted) {
192052
- readAccount.current = accountKey;
192053
- setUnmarked({
192054
- ...snapshot,
192055
- loading: true,
192056
- error: ""
192057
- });
192058
- }
192059
- }).then((snapshot) => {
192060
- if (!controller.signal.aborted) setUnmarked({
192061
- ...snapshot,
192062
- loading: false,
192063
- error: ""
192064
- });
192065
- }).catch((error) => {
192066
- if (!controller.signal.aborted) {
192067
- setUnmarked((previous) => ({
192068
- ...previous,
192069
- loading: false,
192070
- complete: false,
192071
- error: failureMessage(error)
192072
- }));
192073
- retry(error, readCandidates);
191914
+ }, [
191915
+ accountKey,
191916
+ directory,
191917
+ query,
191918
+ revision
191919
+ ]);
191920
+ (0, react$1.useEffect)(() => {
191921
+ if (!list || loading || queryPending || list.query.filter !== filter || list.query.query !== queryText) return;
191922
+ let disposed = false;
191923
+ const confirm = () => {
191924
+ if (directoryVisible()) directory.confirmDisplayed(accountKey, list).then(() => {
191925
+ if (!disposed) {
191926
+ setSeenError("");
191927
+ if (directory.needsReload(accountKey, list)) refresh();
192074
191928
  }
191929
+ }).catch(() => {
191930
+ if (!disposed) setSeenError(tr$1("查看状态暂未同步,联网后自动重试。"));
192075
191931
  });
192076
191932
  };
192077
- readCandidates();
191933
+ confirm();
191934
+ if (typeof document !== "undefined") document.addEventListener("visibilitychange", confirm);
192078
191935
  return () => {
192079
- controller.abort();
192080
- retryTimers.forEach(clearTimeout);
191936
+ disposed = true;
191937
+ if (typeof document !== "undefined") document.removeEventListener("visibilitychange", confirm);
192081
191938
  };
192082
191939
  }, [
192083
191940
  accountKey,
192084
- refreshRevision,
192085
- directory
191941
+ directory,
191942
+ list,
191943
+ loading,
191944
+ queryPending,
191945
+ filter,
191946
+ queryText,
191947
+ refresh
192086
191948
  ]);
191949
+ const recoverDetail = (0, react$1.useCallback)(() => {
191950
+ if (!selected || recoveryAttempts.current >= 1) {
191951
+ setSelected(void 0);
191952
+ refresh();
191953
+ return;
191954
+ }
191955
+ recoveryAttempts.current += 1;
191956
+ const controller = controllerRef.current;
191957
+ if (!controller) return;
191958
+ directory.first(accountKey, query, controller.signal, true).then((next) => {
191959
+ if (controller.signal.aborted) return;
191960
+ setList(next);
191961
+ setSelected(next.items.find((item) => item.personKey === selected.personKey));
191962
+ }).catch((error) => {
191963
+ if (!controller.signal.aborted) {
191964
+ setSelected(void 0);
191965
+ setError(failureMessage(error));
191966
+ }
191967
+ });
191968
+ }, [
191969
+ accountKey,
191970
+ directory,
191971
+ selected,
191972
+ query,
191973
+ refresh
191974
+ ]);
191975
+ const loadMore = async () => {
191976
+ const controller = controllerRef.current;
191977
+ if (!list || !controller || moreLoading || queryPending) return;
191978
+ setMoreLoading(true);
191979
+ setError("");
191980
+ try {
191981
+ const next = await directory.more(accountKey, list, controller.signal);
191982
+ if (!controller.signal.aborted) setList(next);
191983
+ } catch (error) {
191984
+ if (!controller.signal.aborted) setError(failureMessage(error));
191985
+ } finally {
191986
+ if (!controller.signal.aborted) setMoreLoading(false);
191987
+ }
191988
+ };
191989
+ const disabled = summary?.state === "disabled" || list?.state === "disabled";
191990
+ const rows = disabled || queryPending || !listMatches ? [] : list?.items ?? [];
191991
+ const avatarBatch = JSON.stringify(rows.filter((row) => row.type === "marked" && avatars[row.detailRef] === void 0).slice(0, 50).map((row) => row.detailRef));
192087
191992
  (0, react$1.useEffect)(() => {
192088
- const state = presence.result?.state;
192089
- if (presence.loading || presence.error !== "" || state !== "building" && state !== "stale" && state !== "failed") return;
191993
+ const refs = JSON.parse(avatarBatch);
191994
+ if (refs.length === 0) return;
192090
191995
  const controller = new AbortController();
192091
- const timer = setTimeout(() => {
192092
- directory.refreshPresence(accountKey, controller.signal).then((result) => {
192093
- if (!controller.signal.aborted) setPresence({
192094
- loading: false,
192095
- result,
192096
- error: ""
192097
- });
192098
- }).catch((error) => {
192099
- if (!controller.signal.aborted) setPresence((previous) => ({
191996
+ let retry;
191997
+ let busy = false, retryAt = 0;
191998
+ const load = async () => {
191999
+ if (!directoryVisible() || controller.signal.aborted || busy) return;
192000
+ if (retryAt > Date.now()) {
192001
+ clearTimeout(retry);
192002
+ retry = setTimeout(() => {
192003
+ load();
192004
+ }, retryAt - Date.now());
192005
+ return;
192006
+ }
192007
+ busy = true;
192008
+ try {
192009
+ const result = await directory.avatars(accountKey, refs, controller.signal);
192010
+ if (controller.signal.aborted) return;
192011
+ const resolved = new Map(result.map((item) => [item.detailRef, item.avatarRef]));
192012
+ setAvatars((previous) => ({
192100
192013
  ...previous,
192101
- loading: false,
192102
- error: failureMessage(error)
192014
+ ...Object.fromEntries(refs.map((ref) => [ref, resolved.get(ref) || null]))
192103
192015
  }));
192104
- });
192105
- }, Math.max(5e3, presence.result?.retryAfterMs ?? 1e4));
192016
+ } catch (error) {
192017
+ if (controller.signal.aborted) return;
192018
+ const delay = speakerRetryDelay(error);
192019
+ if (delay !== void 0) {
192020
+ retryAt = Date.now() + Math.max(15e3, delay);
192021
+ retry = setTimeout(() => {
192022
+ load();
192023
+ }, retryAt - Date.now());
192024
+ }
192025
+ } finally {
192026
+ busy = false;
192027
+ }
192028
+ };
192029
+ const wake = () => {
192030
+ clearTimeout(retry);
192031
+ load();
192032
+ };
192033
+ load();
192034
+ if (typeof document !== "undefined") document.addEventListener("visibilitychange", wake);
192035
+ if (typeof window !== "undefined") window.addEventListener("online", wake);
192106
192036
  return () => {
192107
- clearTimeout(timer);
192108
192037
  controller.abort();
192038
+ clearTimeout(retry);
192039
+ if (typeof document !== "undefined") document.removeEventListener("visibilitychange", wake);
192040
+ if (typeof window !== "undefined") window.removeEventListener("online", wake);
192109
192041
  };
192110
192042
  }, [
192111
192043
  accountKey,
192112
- refreshRevision,
192113
192044
  directory,
192114
- presence.loading,
192115
- presence.error,
192116
- presence.result
192117
- ]);
192118
- const refreshPresenceVersion = (0, react$1.useCallback)(() => {
192119
- setPresence({
192120
- loading: false,
192121
- result: {
192122
- state: "stale",
192123
- scope: "all-history",
192124
- items: [],
192125
- retryAfterMs: 1e3
192126
- },
192127
- error: ""
192128
- });
192129
- }, []);
192130
- const rows = (0, react$1.useMemo)(() => identifiedSpeakerRows(marked.items, unmarked.items), [marked.items, unmarked.items]);
192131
- const hasSelf = rows.some((row) => row.kind === "marked" && row.isCurrentUser);
192132
- (0, react$1.useEffect)(() => {
192133
- if (readAccount.current !== accountKey || marked.loading || marked.error !== "") return;
192134
- if (rows.length > 0 || !unmarked.loading && unmarked.complete) recognizedSpeakerTracker.acknowledge(accountKey);
192135
- if (!unmarked.loading && unmarked.complete && unmarked.error === "") recognizedSpeakerTracker.observe(accountKey, marked.items, unmarked.items, true);
192136
- }, [
192137
- accountKey,
192138
- marked.loading,
192139
- marked.error,
192140
- marked.items,
192141
- unmarked.loading,
192142
- unmarked.complete,
192143
- unmarked.error,
192144
- unmarked.items,
192145
- rows.length
192045
+ avatarBatch
192146
192046
  ]);
192147
- const presenceByOption = (0, react$1.useMemo)(() => new Map(presence.error === "" && presence.result?.state === "fresh" ? presence.result.items.map((item) => [item.optionKey, item]) : []), [presence.result, presence.error]);
192148
- const normalizedQuery = query.trim().toLocaleLowerCase();
192149
- const orderValue = (row) => {
192150
- const stat = row.kind === "marked" ? presenceByOption.get(row.optionKey) : unmarked.projectionState === void 0 || unmarked.projectionState === "fresh" ? row : void 0;
192151
- return {
192152
- key: `${row.kind}:${row.key}`,
192153
- name: row.name,
192154
- dayCount: stat?.dayCount,
192155
- lastSeenAt: stat?.lastSeenAt
192156
- };
192157
- };
192158
- const visible = rows.filter((row) => (filter === "all" || row.kind === filter) && (normalizedQuery === "" || row.name.toLocaleLowerCase().includes(normalizedQuery) || row.kind === "unmarked" && row.subtitle.toLocaleLowerCase().includes(normalizedQuery))).sort((left, right) => Number(right.kind === "marked" && right.isCurrentUser) - Number(left.kind === "marked" && left.isCurrentUser) || compareRecognizedSpeakers(orderValue(left), orderValue(right), order));
192159
- const loading = marked.loading || unmarked.loading;
192047
+ const state = disabled ? tr$1("说话人目录暂不可用,请稍后再试。") : statusText(summary?.snapshotVersion === list?.snapshotVersion ? summary : list?.coverage === "complete" ? list : summary);
192048
+ const hasNewVersion = list?.coverage === "complete" && summary?.snapshotVersion && summary.snapshotVersion !== list.snapshotVersion;
192049
+ const selfMissing = listMatches && filter === "all" && queryText === "" && !queryPending && list?.coverage === "complete" && list.state === "fresh" && !list.items.some((row) => row.isSelf);
192160
192050
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
192051
+ ref: rootRef,
192161
192052
  style: styles$20.root,
192162
192053
  "data-arkme-owned": "recognized-speakers-surface",
192163
192054
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: `
@@ -192197,6 +192088,7 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
192197
192088
  marginLeft: "auto"
192198
192089
  },
192199
192090
  onClick: refresh,
192091
+ disabled: loading,
192200
192092
  children: tr$1("刷新")
192201
192093
  })
192202
192094
  ]
@@ -192205,41 +192097,44 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
192205
192097
  style: styles$20.filters,
192206
192098
  children: [
192207
192099
  [
192208
- ["all", "全部"],
192209
- ["marked", "已标记"],
192210
- ["unmarked", "未标记"]
192211
- ].map(([kind, label]) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
192100
+ [
192101
+ "all",
192102
+ "全部",
192103
+ summary?.totalCount
192104
+ ],
192105
+ [
192106
+ "marked",
192107
+ "已标记",
192108
+ summary?.markedCount
192109
+ ],
192110
+ [
192111
+ "unmarked",
192112
+ "未标记",
192113
+ summary?.unmarkedCount
192114
+ ]
192115
+ ].map(([kind, label, count]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
192212
192116
  type: "button",
192213
192117
  "aria-pressed": filter === kind,
192214
192118
  style: {
192215
192119
  ...styles$20.filter,
192216
192120
  ...filter === kind ? styles$20.selectedFilter : {}
192217
192121
  },
192218
- onClick: () => {
192219
- setFilter(kind);
192220
- setSelectedCandidate(void 0);
192221
- setSelectedMarked(void 0);
192222
- },
192223
- children: tr$1(label)
192122
+ onClick: () => setFilter(kind),
192123
+ children: [tr$1(label), !disabled && count != null ? ` ${count}` : ""]
192224
192124
  }, kind)),
192225
192125
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
192226
192126
  "aria-label": tr$1("搜索说话人"),
192227
- placeholder: tr$1("搜索说话人"),
192228
- value: query,
192229
- onChange: (event) => {
192230
- setQuery(event.target.value);
192231
- },
192127
+ placeholder: tr$1("搜索名称或编号"),
192128
+ value: input,
192129
+ onChange: (event) => setInput(event.target.value),
192232
192130
  style: styles$20.search
192233
192131
  }),
192234
192132
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
192235
192133
  "aria-label": tr$1("说话人排序"),
192236
- value: order,
192134
+ value: sort,
192237
192135
  onChange: (event) => {
192238
192136
  const next = event.target.value === "recent" ? "recent" : "frequent";
192239
- setOrderPreference({
192240
- accountKey,
192241
- value: next
192242
- });
192137
+ setSort(next);
192243
192138
  writeRecognizedSpeakerOrder(accountKey, next);
192244
192139
  },
192245
192140
  style: styles$20.sort,
@@ -192253,186 +192148,115 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
192253
192148
  })
192254
192149
  ]
192255
192150
  }),
192256
- !marked.loading && marked.error === "" && !hasSelf && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SpeakerSelfGuide, { onOpenRecordings: onBack }, accountKey),
192151
+ !queryValid && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
192152
+ role: "alert",
192153
+ style: styles$20.error,
192154
+ children: tr$1("搜索内容过长,请缩短至 256 字节以内。")
192155
+ }),
192156
+ selfMissing && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SpeakerSelfGuide, { onOpenRecordings: onBack }, accountKey),
192157
+ hasNewVersion && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
192158
+ role: "status",
192159
+ style: styles$20.note,
192160
+ children: tr$1("目录有更新,刷新后查看最新人物。")
192161
+ }),
192162
+ state && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
192163
+ role: "status",
192164
+ style: styles$20.note,
192165
+ children: state
192166
+ }),
192167
+ seenError && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
192168
+ role: "status",
192169
+ style: styles$20.note,
192170
+ children: seenError
192171
+ }),
192172
+ error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
192173
+ role: "alert",
192174
+ style: styles$20.error,
192175
+ children: error
192176
+ }),
192257
192177
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
192258
192178
  className: "arkme-recognized-speakers-grid",
192259
- "data-detail-open": selectedCandidate === void 0 && selectedMarked === void 0 ? "false" : "true",
192260
- children: [
192261
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
192262
- className: "arkme-recognized-speakers-list",
192263
- children: [
192264
- filter !== "marked" && unmarked.loading && !unmarked.complete && rows.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
192265
- role: "status",
192266
- style: styles$20.note,
192267
- children: tr$1("正在补齐说话人列表,排序仍在更新…")
192268
- }),
192269
- filter !== "marked" && !unmarked.loading && !unmarked.complete && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
192270
- role: "status",
192271
- style: styles$20.note,
192272
- children: tr$1("列表尚未完整,当前仅对已加载的说话人排序和搜索。")
192273
- }),
192274
- marked.error !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
192275
- role: "alert",
192276
- style: styles$20.error,
192277
- children: [
192278
- tr$1("已标记说话人读取失败:"),
192279
- " ",
192280
- marked.error
192281
- ]
192282
- }),
192283
- unmarked.error !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
192284
- role: "alert",
192285
- style: styles$20.error,
192286
- children: [
192287
- tr$1("未标记说话人读取失败:"),
192288
- " ",
192289
- unmarked.error
192290
- ]
192291
- }),
192292
- unmarked.projectionState === "building" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
192293
- role: "status",
192294
- style: styles$20.state,
192295
- children: tr$1("未标记说话人正在整理,结果可能不完整。")
192296
- }),
192297
- unmarked.projectionState === "stale" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
192298
- role: "status",
192299
- style: styles$20.state,
192300
- children: tr$1("未标记说话人正在更新,结果可能不完整。")
192301
- }),
192302
- unmarked.projectionState === "failed" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
192303
- role: "alert",
192304
- style: styles$20.error,
192305
- children: tr$1("未标记说话人整理失败,请稍后刷新。")
192306
- }),
192307
- loading && rows.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
192308
- role: "status",
192309
- style: styles$20.state,
192310
- children: tr$1("正在加载说话人…")
192311
- }) : visible.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
192312
- role: "status",
192313
- style: styles$20.state,
192314
- children: loading ? tr$1("正在更新说话人…") : rows.length === 0 && marked.error === "" && unmarked.error === "" && (unmarked.projectionState === void 0 || unmarked.projectionState === "fresh") ? tr$1("暂无已识别说话人") : tr$1("暂无可显示的说话人")
192315
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
192316
- style: styles$20.list,
192317
- children: visible.slice(0, visibleCount).map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: row.kind === "unmarked" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
192318
- type: "button",
192319
- className: "arkme-recognized-speakers-row",
192320
- "aria-current": selectedCandidate === row.candidateRef ? "true" : void 0,
192321
- style: {
192322
- ...styles$20.row,
192323
- cursor: "pointer"
192324
- },
192325
- onClick: () => {
192326
- setSelectedCandidate(row.candidateRef);
192327
- setSelectedMarked(void 0);
192328
- },
192329
- children: [
192330
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UnmarkedSpeakerTokenAvatar, {
192331
- token: row.token,
192332
- size: 38,
192333
- label: row.name
192334
- }),
192335
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
192336
- style: styles$20.copy,
192337
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
192338
- style: styles$20.name,
192339
- children: row.name
192340
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
192341
- style: styles$20.meta,
192342
- children: row.subtitle
192343
- })]
192344
- }),
192345
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
192346
- style: styles$20.badge,
192347
- children: [tr$1("未标记"), " ›"]
192348
- })
192349
- ]
192350
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
192351
- type: "button",
192352
- className: "arkme-recognized-speakers-row",
192353
- "aria-current": selectedMarked?.key === row.key ? "true" : void 0,
192354
- style: {
192355
- ...styles$20.row,
192356
- cursor: "pointer"
192357
- },
192358
- onClick: () => {
192359
- setSelectedMarked(row);
192360
- setSelectedCandidate(void 0);
192361
- },
192362
- children: [
192363
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ArkmeUserAvatar, {
192364
- ...row.avatarRef === void 0 ? {} : { avatarRef: row.avatarRef },
192365
- size: 38,
192366
- label: row.name
192367
- }),
192368
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
192369
- style: styles$20.copy,
192370
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
192371
- style: styles$20.name,
192372
- children: [row.name, row.isCurrentUser ? ` · ${tr$1("我")}` : ""]
192373
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
192374
- style: styles$20.meta,
192375
- children: markedPresenceLabel(presenceByOption.get(row.optionKey), presence.result, presence.loading, presence.error)
192376
- })]
192377
- }),
192378
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
192379
- style: styles$20.badge,
192380
- children: [tr$1("已标记"), " ›"]
192381
- })
192382
- ]
192383
- }) }, `${row.kind}:${row.key}`))
192384
- }),
192385
- visible.length > visibleCount && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
192386
- type: "button",
192387
- style: {
192388
- ...styles$20.button,
192389
- marginTop: 14
192390
- },
192391
- onClick: () => {
192392
- setVisibleCount((count) => count + 100);
192393
- },
192394
- children: tr$1("显示更多说话人")
192395
- })
192396
- ]
192397
- }),
192398
- selectedCandidate !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
192399
- style: styles$20.detail,
192400
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
192179
+ "data-detail-open": selected !== void 0 ? "true" : "false",
192180
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
192181
+ className: "arkme-recognized-speakers-list",
192182
+ "aria-busy": loading || queryPending,
192183
+ children: [!disabled && (loading || queryPending) && rows.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
192184
+ role: "status",
192185
+ style: styles$20.state,
192186
+ children: tr$1("正在加载说话人…")
192187
+ }) : !disabled && !error && rows.length === 0 && list?.coverage === "complete" && !queryPending ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
192188
+ role: "status",
192189
+ style: styles$20.state,
192190
+ children: tr$1(queryText ? "没有匹配的说话人" : filter === "all" ? "暂无已识别说话人" : "该分类暂无说话人")
192191
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
192192
+ style: styles$20.list,
192193
+ children: rows.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
192401
192194
  type: "button",
192402
- className: "arkme-recognized-speakers-mobile-back",
192403
- style: styles$20.button,
192404
- onClick: () => {
192405
- setSelectedCandidate(void 0);
192195
+ className: "arkme-recognized-speakers-row",
192196
+ "aria-current": selected?.personKey === row.personKey ? "true" : void 0,
192197
+ style: {
192198
+ ...styles$20.row,
192199
+ cursor: "pointer"
192406
192200
  },
192407
- children: tr$1("‹ 返回列表")
192408
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UnmarkedSpeakerDetail, {
192409
- accountKey,
192410
- candidateRef: selectedCandidate,
192411
- onDirectoryRefresh: refresh,
192412
- onCandidateCleared: () => {
192413
- setSelectedCandidate(void 0);
192414
- }
192415
- }, `${accountKey}:${selectedCandidate}`)]
192416
- }),
192417
- selectedMarked !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
192418
- style: styles$20.detail,
192419
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
192420
- type: "button",
192421
- className: "arkme-recognized-speakers-mobile-back",
192422
- style: styles$20.button,
192423
192201
  onClick: () => {
192424
- setSelectedMarked(void 0);
192202
+ recoveryAttempts.current = 0;
192203
+ setSelected(row);
192425
192204
  },
192426
- children: tr$1("‹ 返回列表")
192427
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MarkedSpeakerDetail, {
192428
- accountKey,
192429
- speaker: selectedMarked,
192430
- loadMembers: loadMarkedMembers,
192431
- version: presence.result?.version,
192432
- onVersionChanged: refreshPresenceVersion
192433
- }, `${accountKey}:${selectedMarked.speakerRef}`)]
192434
- })
192435
- ]
192205
+ children: [
192206
+ row.type === "unmarked" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UnmarkedSpeakerTokenAvatar, {
192207
+ token: row.displayNumber > 0 ? String(row.displayNumber) : "",
192208
+ size: 38,
192209
+ label: row.displayName
192210
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ArkmeUserAvatar, {
192211
+ ...avatars[row.detailRef] ? { avatarRef: avatars[row.detailRef] } : {},
192212
+ fallbackContent: speakerNameAvatarFallback(row),
192213
+ size: 38,
192214
+ label: row.displayName
192215
+ }),
192216
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
192217
+ style: styles$20.copy,
192218
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
192219
+ style: styles$20.name,
192220
+ children: [row.displayName, row.isSelf ? ` · ${tr$1("我")}` : ""]
192221
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
192222
+ style: styles$20.meta,
192223
+ children: personStats(row)
192224
+ })]
192225
+ }),
192226
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
192227
+ style: styles$20.badge,
192228
+ children: [tr$1(row.type === "marked" ? "已标记" : "未标记"), " ›"]
192229
+ })
192230
+ ]
192231
+ }) }, row.personKey))
192232
+ }), !disabled && !queryPending && list?.hasMore && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
192233
+ type: "button",
192234
+ disabled: moreLoading || loading,
192235
+ style: {
192236
+ ...styles$20.button,
192237
+ marginTop: 14
192238
+ },
192239
+ onClick: () => {
192240
+ loadMore();
192241
+ },
192242
+ children: tr$1(moreLoading ? "正在加载…" : "加载更多说话人")
192243
+ })]
192244
+ }), selected && !disabled && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
192245
+ style: styles$20.detail,
192246
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
192247
+ type: "button",
192248
+ className: "arkme-recognized-speakers-mobile-back",
192249
+ style: styles$20.button,
192250
+ onClick: () => setSelected(void 0),
192251
+ children: tr$1("‹ 返回列表")
192252
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PersonDetail, {
192253
+ row: selected,
192254
+ accountKey,
192255
+ directory,
192256
+ loadMembers: loadMarkedMembers,
192257
+ onChanged: recoverDetail
192258
+ }, selected.detailRef)]
192259
+ })]
192436
192260
  })
192437
192261
  ]
192438
192262
  })]
@@ -212840,7 +212664,7 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
212840
212664
  const c$3 = e$20;
212841
212665
  //#endregion
212842
212666
  //#region package.json
212843
- var version = "0.1.86";
212667
+ var version = "0.1.87";
212844
212668
  //#endregion
212845
212669
  //#region src/client/arkme-membership.ts
212846
212670
  function membershipLabel(state) {