@senguoyun/dsh-arkme 0.1.83 → 0.1.84

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 (56) hide show
  1. package/docs/recognized-speaker-directory-backend-handoff-2026-09-28.md +157 -0
  2. package/docs/recognized-speaker-entry-and-visits.md +47 -0
  3. package/docs/recognized-speaker-presence-delivery.md +51 -0
  4. package/docs/recognized-speaker-sorting.md +30 -0
  5. package/lib/client.js +1194 -253
  6. package/lib/client.js.map +1 -1
  7. package/lib/harness-model-client.js +51 -7
  8. package/lib/harness-native-selection-client.js +35 -0
  9. package/lib/harness-sidebar-client.js +20 -0
  10. package/lib/index.js +154 -77
  11. package/lib/persistent-extension.js +1 -1
  12. package/lib/{persistent-runtime-B6vPAaiG.js → persistent-runtime-CuKplHIv.js} +101 -262
  13. package/lib/sdk.js +20 -0
  14. package/lib/types/arkme-service.d.ts +1 -1
  15. package/lib/types/arkme-service.d.ts.map +1 -1
  16. package/lib/types/client/ArkmeAccountUsage.d.ts.map +1 -1
  17. package/lib/types/client/ArkmeModelPricing.d.ts +9 -0
  18. package/lib/types/client/ArkmeModelPricing.d.ts.map +1 -0
  19. package/lib/types/client/ArkmeModelSelect.d.ts.map +1 -1
  20. package/lib/types/client/ArkmeNoteDetails.d.ts.map +1 -1
  21. package/lib/types/client/ArkmeRecognizedSpeakersSurface.d.ts +6 -2
  22. package/lib/types/client/ArkmeRecognizedSpeakersSurface.d.ts.map +1 -1
  23. package/lib/types/client/ArkmeRecordingSurface.d.ts.map +1 -1
  24. package/lib/types/client/ArkmeSidebar.d.ts.map +1 -1
  25. package/lib/types/client/ArkoModelMenu.d.ts.map +1 -1
  26. package/lib/types/client/locales/en.d.ts.map +1 -1
  27. package/lib/types/client/reaction-preview-store.d.ts +1 -0
  28. package/lib/types/client/reaction-preview-store.d.ts.map +1 -1
  29. package/lib/types/client/recognized-speaker-directory.d.ts +36 -0
  30. package/lib/types/client/recognized-speaker-directory.d.ts.map +1 -0
  31. package/lib/types/client/recognized-speaker-order.d.ts +28 -0
  32. package/lib/types/client/recognized-speaker-order.d.ts.map +1 -0
  33. package/lib/types/client/recognized-speaker-tracker.d.ts +33 -0
  34. package/lib/types/client/recognized-speaker-tracker.d.ts.map +1 -0
  35. package/lib/types/client/recordings/RecognizedSpeakerEntry.d.ts +12 -0
  36. package/lib/types/client/recordings/RecognizedSpeakerEntry.d.ts.map +1 -0
  37. package/lib/types/client/recordings/SpeakerSelfGuide.d.ts +8 -0
  38. package/lib/types/client/recordings/SpeakerSelfGuide.d.ts.map +1 -0
  39. package/lib/types/client/recordings/recording-speaker-options-store.d.ts.map +1 -1
  40. package/lib/types/sdk/index.d.ts +10 -0
  41. package/lib/types/sdk/index.d.ts.map +1 -1
  42. package/lib/types/services/chat-service.d.ts.map +1 -1
  43. package/lib/types/services/recording-service.d.ts +2 -5
  44. package/lib/types/services/recording-service.d.ts.map +1 -1
  45. package/lib/types/services/unmarked-speaker-service.d.ts.map +1 -1
  46. package/lib/types/tools/business/recordings/index.d.ts +1 -1
  47. package/lib/types/tools/business/recordings/index.d.ts.map +1 -1
  48. package/lib/types/tools/business/recordings/speaker-presence.d.ts +2 -0
  49. package/lib/types/tools/business/recordings/speaker-presence.d.ts.map +1 -0
  50. package/lib/types/tools/ports/recordings.d.ts +4 -0
  51. package/lib/types/tools/ports/recordings.d.ts.map +1 -1
  52. package/lib/types/types.d.ts +18 -7
  53. package/lib/types/types.d.ts.map +1 -1
  54. package/package.json +5 -5
  55. package/lib/types/recording-speaker-presence.d.ts +0 -28
  56. package/lib/types/recording-speaker-presence.d.ts.map +0 -1
package/lib/client.js CHANGED
@@ -249,6 +249,26 @@ window.__ModuleLoader__.load({
249
249
  this.route = route;
250
250
  this.fetchImpl = options.fetchImpl ?? globalThis.fetch.bind(globalThis);
251
251
  }
252
+ /** Account-scoped marked speaker candidates; refs can be used only with this provider/account. */
253
+ async recordingSpeakerCandidates(signal) {
254
+ await this.requireSpeakerPresence(signal);
255
+ return await this.call("recordings.speaker.options", {}, signal);
256
+ }
257
+ async recordingSpeakerPresence(signal) {
258
+ await this.requireSpeakerPresence(signal);
259
+ return await this.call("recordings.speaker.presence", {}, signal);
260
+ }
261
+ async recordingSpeakerMembers(speakerRef, options = {}) {
262
+ if (speakerRef.trim() === "") throw new TypeError("speakerRef is required");
263
+ await this.requireSpeakerPresence(options.signal);
264
+ return await this.call("recordings.speaker.members", {
265
+ speakerRef,
266
+ ...options.expectedVersion === void 0 ? {} : { expectedVersion: options.expectedVersion }
267
+ }, options.signal);
268
+ }
269
+ async requireSpeakerPresence(signal) {
270
+ if ((await this.capabilities(signal)).features.speakerPresence !== true) throw new Error("当前 Provider 不支持全历史说话人统计");
271
+ }
252
272
  async capabilities(signal) {
253
273
  const capabilities = await this.call("provider.capabilities", void 0, signal);
254
274
  if (capabilities.contractVersion !== 1) throw new Error(`Unsupported Arkme provider contract version ${String(capabilities.contractVersion)}`);
@@ -33147,6 +33167,12 @@ window.__ModuleLoader__.load({
33147
33167
  /** Application copy only. User-authored text is never looked up in this table. */
33148
33168
  const arkmeEnglish = Object.fromEntries(`
33149
33169
  个人世界|Personal world
33170
+ 说话人排序|Sort speakers
33171
+ 经常出现|Most frequent
33172
+ 最近出现|Most recent
33173
+ 正在补齐说话人列表,排序仍在更新…|Loading the remaining speakers. Order is still updating…
33174
+ 列表尚未完整,当前仅对已加载的说话人排序和搜索。|The list is incomplete. Sorting and search cover loaded speakers only.
33175
+ 显示更多说话人|Show more speakers
33150
33176
  语音通话邀请|Voice call invitation
33151
33177
  视频通话邀请|Video call invitation
33152
33178
  公开快记|Public note
@@ -33726,6 +33752,7 @@ AI智能体|AI agent
33726
33752
  群聊名称|Group name
33727
33753
  最新|Latest
33728
33754
  部分历史附件暂不可用|Some earlier attachments are unavailable
33755
+ 附件暂不可用|Attachment unavailable
33729
33756
  正在加载编辑记录…|Loading edit history…
33730
33757
  暂无编辑记录|No edit history
33731
33758
  重新加载历史附件|Reload earlier attachments
@@ -34769,6 +34796,14 @@ Bot 已移出群聊|Bot removed from the group
34769
34796
  {v0}人|{v0} people
34770
34797
  正在读取群 Bot…|Loading group Bots…
34771
34798
  Bot 列表加载失败,点击重试|Could not load Bots. Click to retry
34799
+ 新识别 {v0} 个|{v0} newly identified
34800
+ 识别我的声音|Identify my voice
34801
+ 你的声纹正在处理中|Your voiceprint is being processed
34802
+ 已录入声纹,暂未在录音中匹配到你|Voiceprint enrolled. No match found in your recordings yet.
34803
+ 还没有标记你的声音|Your voice has not been labeled yet
34804
+ 打开你的录音,点击说话人名称并选择自己;请先试听确认。|Open your recording, listen to confirm, then click the speaker name and select yourself.
34805
+ 从录音中标记我|Label myself in a recording
34806
+ 管理声纹|Manage voiceprint
34772
34807
  `.trim().split("\n").map((line) => {
34773
34808
  const index = line.indexOf("|");
34774
34809
  return [line.slice(0, index), line.slice(index + 1)];
@@ -47700,7 +47735,7 @@ function(test) {
47700
47735
  * Test type.
47701
47736
  */
47702
47737
  /** @type {Readonly<ActionTuple>} */
47703
- const empty$4 = [];
47738
+ const empty$5 = [];
47704
47739
  /**
47705
47740
  * Visit nodes, with ancestral information.
47706
47741
  *
@@ -47772,7 +47807,7 @@ function(test) {
47772
47807
  return visit;
47773
47808
  function visit() {
47774
47809
  /** @type {Readonly<ActionTuple>} */
47775
- let result = empty$4;
47810
+ let result = empty$5;
47776
47811
  /** @type {Readonly<ActionTuple>} */
47777
47812
  let subresult;
47778
47813
  /** @type {number} */
@@ -47811,7 +47846,7 @@ function(test) {
47811
47846
  function toResult(value) {
47812
47847
  if (Array.isArray(value)) return value;
47813
47848
  if (typeof value === "number") return [true, value];
47814
- return value === null || value === void 0 ? empty$4 : [value];
47849
+ return value === null || value === void 0 ? empty$5 : [value];
47815
47850
  }
47816
47851
  //#endregion
47817
47852
  //#region node_modules/mdast-util-find-and-replace/lib/index.js
@@ -55726,9 +55761,12 @@ body[data-ds-dark-theme] .arkme-bot-identity {
55726
55761
  }
55727
55762
  this.inactive.delete(target.id);
55728
55763
  this.knownTargets.set(target.id, target);
55729
- this.targets.set(target.id, {
55764
+ if (existing) {
55765
+ existing.target = target;
55766
+ existing.count++;
55767
+ } else this.targets.set(target.id, {
55730
55768
  target,
55731
- count: (existing?.count ?? 0) + 1
55769
+ count: 1
55732
55770
  });
55733
55771
  if (this.refreshing) this.refreshAgain = true;
55734
55772
  this.schedule(40);
@@ -55740,18 +55778,23 @@ body[data-ds-dark-theme] .arkme-bot-identity {
55740
55778
  else {
55741
55779
  this.targets.delete(target.id);
55742
55780
  this.failures.delete(target.id);
55743
- if (this.states.has(target.id)) this.inactive.add(target.id);
55744
- else this.knownTargets.delete(target.id);
55745
- while (this.inactive.size > 400) {
55746
- const oldest = this.inactive.values().next().value;
55747
- this.inactive.delete(oldest);
55748
- this.states.delete(oldest);
55749
- this.knownTargets.delete(oldest);
55750
- }
55781
+ this.retainInactive(target.id);
55751
55782
  }
55752
55783
  if (!this.targets.size) this.stop();
55753
55784
  };
55754
55785
  }
55786
+ retainInactive(id) {
55787
+ if (this.targets.has(id)) return;
55788
+ if (this.states.has(id) || this.failures.has(id)) this.inactive.add(id);
55789
+ else this.knownTargets.delete(id);
55790
+ while (this.inactive.size > 400) {
55791
+ const oldest = this.inactive.values().next().value;
55792
+ this.inactive.delete(oldest);
55793
+ this.states.delete(oldest);
55794
+ this.knownTargets.delete(oldest);
55795
+ this.failures.delete(oldest);
55796
+ }
55797
+ }
55755
55798
  async prepare(scope, targets, signal, force = false) {
55756
55799
  if (scope !== this.scope || signal.aborted) return;
55757
55800
  const missing = targets.filter((target) => force || !this.states.has(target.id)).slice(0, 200);
@@ -55907,7 +55950,19 @@ body[data-ds-dark-theme] .arkme-bot-identity {
55907
55950
  if (recovered.outcome === "revision_conflict") throw new Error("上次表态已在其他设备更新,请重新选择");
55908
55951
  await this.refresh();
55909
55952
  }
55910
- if (!this.states.has(target.id)) await this.refresh();
55953
+ if (!this.states.has(target.id)) {
55954
+ const page = await this.transport({
55955
+ action: "query",
55956
+ accountKey: scope,
55957
+ targets: [target]
55958
+ }, signal);
55959
+ if (signal.aborted) return false;
55960
+ const loaded = page.items.find((item) => item.target_id === target.id);
55961
+ if (!loaded) throw new Error("消息已不可访问");
55962
+ const previous = this.states.get(target.id);
55963
+ if (!previous || previous.mine.revision <= loaded.mine.revision) this.states.set(target.id, loaded);
55964
+ this.knownTargets.set(target.id, target);
55965
+ }
55911
55966
  const state = this.states.get(target.id);
55912
55967
  if (signal.aborted || !state) throw new Error("请先成功加载消息表态");
55913
55968
  const selected = state.mine.selections.find((item) => expressionIdentity(item.expression) === expressionIdentity(chosen));
@@ -55940,6 +55995,7 @@ body[data-ds-dark-theme] .arkme-bot-identity {
55940
55995
  return false;
55941
55996
  } finally {
55942
55997
  if (!signal.aborted) {
55998
+ this.retainInactive(target.id);
55943
55999
  this.writing = false;
55944
56000
  this.publish();
55945
56001
  }
@@ -56813,6 +56869,474 @@ body[data-ds-dark-theme] .arkme-bot-identity {
56813
56869
  }
56814
56870
  };
56815
56871
  //#endregion
56872
+ //#region src/client/recognized-speaker-tracker.ts
56873
+ const empty$4 = { checkedAt: 0 };
56874
+ const storageKey$4 = (account) => `dsh-arkme:speaker-visits:v1:${account}`;
56875
+ const storage$1 = () => {
56876
+ try {
56877
+ return window.localStorage;
56878
+ } catch {
56879
+ return;
56880
+ }
56881
+ };
56882
+ function markedSpeakerKeys(options) {
56883
+ return [...new Set(options.filter((option) => option.kind === "speaker").map((option) => `m:${option.personKey || option.optionKey}`))];
56884
+ }
56885
+ /** Only complete identity snapshots can establish a baseline or produce a precise delta.
56886
+ * A removed identity may be a merge/recluster; without lineage we suppress ambiguous
56887
+ * additions for that transition rather than presenting a false "new person".
56888
+ */
56889
+ var RecognizedSpeakerTracker = class {
56890
+ getStorage;
56891
+ records = /* @__PURE__ */ new Map();
56892
+ snapshots = /* @__PURE__ */ new Map();
56893
+ listeners = /* @__PURE__ */ new Set();
56894
+ constructor(getStorage = storage$1) {
56895
+ this.getStorage = getStorage;
56896
+ }
56897
+ subscribe = (listener) => {
56898
+ this.listeners.add(listener);
56899
+ return () => {
56900
+ this.listeners.delete(listener);
56901
+ };
56902
+ };
56903
+ get = (account) => account === void 0 ? empty$4 : this.snapshots.get(account) ?? empty$4;
56904
+ emit(account, snapshot) {
56905
+ this.snapshots.set(account, snapshot);
56906
+ for (const listener of this.listeners) listener();
56907
+ }
56908
+ read(account) {
56909
+ if (this.records.has(account)) return this.records.get(account);
56910
+ try {
56911
+ const value = JSON.parse(this.getStorage()?.getItem(storageKey$4(account)) ?? "null");
56912
+ if (value !== null && [
56913
+ "known",
56914
+ "current",
56915
+ "pending"
56916
+ ].every((key) => {
56917
+ const list = value[key];
56918
+ return Array.isArray(list) && list.length <= 2e4 && list.every((item) => typeof item === "string" && item.length <= 256);
56919
+ })) {
56920
+ this.records.set(account, value);
56921
+ return value;
56922
+ }
56923
+ } catch {}
56924
+ }
56925
+ save(account, value) {
56926
+ this.records.set(account, value);
56927
+ try {
56928
+ this.getStorage()?.setItem(storageKey$4(account), JSON.stringify(value));
56929
+ } catch {}
56930
+ }
56931
+ setTotal(account, total) {
56932
+ if (!Number.isSafeInteger(total) || total < 0) return;
56933
+ const previous = this.get(account);
56934
+ this.emit(account, previous.total === total ? {
56935
+ ...previous,
56936
+ total
56937
+ } : {
56938
+ total,
56939
+ checkedAt: 0
56940
+ });
56941
+ }
56942
+ uncertain(account) {
56943
+ const previous = this.get(account);
56944
+ this.emit(account, {
56945
+ ...previous.total === void 0 ? {} : { total: previous.total },
56946
+ checkedAt: 0
56947
+ });
56948
+ }
56949
+ acknowledge(account) {
56950
+ const previous = this.read(account);
56951
+ if (previous === void 0) return;
56952
+ this.save(account, {
56953
+ ...previous,
56954
+ pending: []
56955
+ });
56956
+ this.emit(account, {
56957
+ ...this.get(account),
56958
+ newCount: 0
56959
+ });
56960
+ }
56961
+ observe(account, options, candidates, viewed = false) {
56962
+ const marked = markedSpeakerKeys(options);
56963
+ if (candidates.some((candidate) => !candidate.identityKey)) {
56964
+ this.uncertain(account);
56965
+ return;
56966
+ }
56967
+ const current = [.../* @__PURE__ */ new Set([...marked, ...candidates.map((candidate) => `u:${candidate.identityKey}`)])];
56968
+ const previous = this.read(account);
56969
+ const known = new Set(previous?.known ?? []);
56970
+ const currentSet = new Set(current);
56971
+ const removed = previous?.current.some((key) => !currentSet.has(key)) ?? false;
56972
+ const additions = previous === void 0 || removed ? [] : current.filter((key) => !known.has(key));
56973
+ const pending = viewed ? [] : [.../* @__PURE__ */ new Set([...(previous?.pending ?? []).filter((key) => currentSet.has(key)), ...additions])];
56974
+ current.forEach((key) => known.add(key));
56975
+ const record = known.size > 2e4 ? {
56976
+ known: current,
56977
+ current,
56978
+ pending: []
56979
+ } : {
56980
+ known: [...known],
56981
+ current,
56982
+ pending
56983
+ };
56984
+ this.save(account, record);
56985
+ this.emit(account, {
56986
+ total: current.length,
56987
+ newCount: record.pending.length,
56988
+ checkedAt: Date.now()
56989
+ });
56990
+ }
56991
+ clearSession() {
56992
+ this.snapshots.clear();
56993
+ this.records.clear();
56994
+ for (const listener of this.listeners) listener();
56995
+ }
56996
+ };
56997
+ const recognizedSpeakerTracker = new RecognizedSpeakerTracker();
56998
+ let activeAccount$1 = "";
56999
+ arkmeAuthStore.subscribe(() => {
57000
+ const auth = arkmeAuthStore.getSnapshot().auth;
57001
+ const next = auth?.status === "authenticated" ? `${auth.environment}:${auth.userId}` : "";
57002
+ if (next !== activeAccount$1) {
57003
+ activeAccount$1 = next;
57004
+ recognizedSpeakerTracker.clearSession();
57005
+ }
57006
+ });
57007
+ //#endregion
57008
+ //#region src/client/recognized-speaker-order.ts
57009
+ const preferenceKey$3 = (accountKey) => `dsh-arkme:recognized-speaker-order:v1:${accountKey}`;
57010
+ function browserStorage$1() {
57011
+ try {
57012
+ return typeof window === "undefined" ? void 0 : window.localStorage;
57013
+ } catch {
57014
+ return;
57015
+ }
57016
+ }
57017
+ function readRecognizedSpeakerOrder(accountKey, storage = browserStorage$1()) {
57018
+ try {
57019
+ return storage?.getItem(preferenceKey$3(accountKey)) === "recent" ? "recent" : "frequent";
57020
+ } catch {
57021
+ return "frequent";
57022
+ }
57023
+ }
57024
+ function writeRecognizedSpeakerOrder(accountKey, order, storage = browserStorage$1()) {
57025
+ try {
57026
+ storage?.setItem(preferenceKey$3(accountKey), order);
57027
+ } catch {}
57028
+ }
57029
+ function positiveNumber(value) {
57030
+ return value !== void 0 && Number.isFinite(value) && value > 0 ? value : 0;
57031
+ }
57032
+ /** Missing statistics sort last. Ties remain stable across refreshes and filters. */
57033
+ function compareRecognizedSpeakers(left, right, order) {
57034
+ const leftDays = positiveNumber(left.dayCount), rightDays = positiveNumber(right.dayCount);
57035
+ const leftTime = positiveNumber(left.lastSeenAt), rightTime = positiveNumber(right.lastSeenAt);
57036
+ 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);
57037
+ }
57038
+ /** The upstream cursor has a fixed order. Read metadata pages before claiming global order.
57039
+ * Bound reads to the host's 2,000-candidate reference budget; never silently call a partial
57040
+ * traversal complete. No audio or per-speaker detail is loaded here.
57041
+ */
57042
+ async function loadSpeakerCandidateSnapshot(loadPage, signal, onProgress) {
57043
+ for (let attempt = 0; attempt < 2; attempt += 1) {
57044
+ const rows = /* @__PURE__ */ new Map();
57045
+ const cursors = /* @__PURE__ */ new Set();
57046
+ let cursor = "";
57047
+ let snapshot = {
57048
+ items: [],
57049
+ complete: false,
57050
+ projectionState: void 0
57051
+ };
57052
+ for (let pageIndex = 0; pageIndex < 40; pageIndex += 1) {
57053
+ signal.throwIfAborted();
57054
+ const page = await loadPage(cursor, signal);
57055
+ signal.throwIfAborted();
57056
+ if (page.section !== "unmarked-speakers") throw new Error("未标记说话人列表响应无效");
57057
+ if (page.cursorStale) {
57058
+ snapshot = {
57059
+ items: [],
57060
+ complete: false,
57061
+ projectionState: "stale"
57062
+ };
57063
+ onProgress(snapshot);
57064
+ if (attempt === 0) break;
57065
+ return snapshot;
57066
+ }
57067
+ for (const item of page.items) if (item.kind === "unmarked-speaker" && rows.size < 2e3) rows.set(item.candidateRef, item);
57068
+ snapshot = {
57069
+ items: [...rows.values()],
57070
+ complete: !page.hasMore && page.coverage !== "partial" && (page.projectionState === void 0 || page.projectionState === "fresh"),
57071
+ projectionState: page.projectionState
57072
+ };
57073
+ onProgress(snapshot);
57074
+ if (!page.hasMore || rows.size >= 2e3 || page.projectionState !== void 0 && page.projectionState !== "fresh") return snapshot;
57075
+ const next = page.nextCursor ?? "";
57076
+ if (next === "" || cursors.has(next)) return snapshot;
57077
+ cursors.add(next);
57078
+ cursor = next;
57079
+ if (pageIndex === 39) return snapshot;
57080
+ }
57081
+ }
57082
+ return {
57083
+ items: [],
57084
+ complete: false,
57085
+ projectionState: "stale"
57086
+ };
57087
+ }
57088
+ //#endregion
57089
+ //#region src/client/recognized-speaker-directory.ts
57090
+ const readSpeakerOptions = (signal) => callArkme("recordings.speaker.options", {}, signal);
57091
+ const readSpeakerPresence = (signal) => callArkme("recordings.speaker.presence", {}, signal);
57092
+ const readSpeakerPage = (cursor, signal) => callArkme("directory.list", {
57093
+ section: "unmarked-speakers",
57094
+ limit: 50,
57095
+ ...cursor === "" ? {} : { cursor }
57096
+ }, signal);
57097
+ var SpeakerReadDeferred = class extends Error {
57098
+ retryAt;
57099
+ constructor(retryAt, cause) {
57100
+ super(cause instanceof Error ? cause.message : "说话人列表暂时无法加载", { cause });
57101
+ this.retryAt = retryAt;
57102
+ }
57103
+ };
57104
+ function speakerRetryDelay(error) {
57105
+ return error instanceof SpeakerReadDeferred ? Math.max(1e3, error.retryAt - Date.now()) : void 0;
57106
+ }
57107
+ /** Per-account shared read. A short release grace lets entry -> list hand off the same
57108
+ * request; leaving both cancels it. A late/aborted response can never update the cache.
57109
+ */
57110
+ var SharedRead = class {
57111
+ ttl;
57112
+ load;
57113
+ now;
57114
+ value;
57115
+ freshUntil = 0;
57116
+ retryAt = 0;
57117
+ failures = 0;
57118
+ error;
57119
+ pending;
57120
+ listeners = /* @__PURE__ */ new Set();
57121
+ releaseTimer;
57122
+ constructor(ttl, load, now) {
57123
+ this.ttl = ttl;
57124
+ this.load = load;
57125
+ this.now = now;
57126
+ }
57127
+ read(signal, progress = () => {}) {
57128
+ signal.throwIfAborted();
57129
+ if (this.value !== void 0) progress(this.value);
57130
+ if (this.now() < this.retryAt) return Promise.reject(new SpeakerReadDeferred(this.retryAt, this.error));
57131
+ if (this.value !== void 0 && this.now() < this.freshUntil) return Promise.resolve(this.value);
57132
+ clearTimeout(this.releaseTimer);
57133
+ if (this.pending?.controller.signal.aborted) this.pending = void 0;
57134
+ if (this.pending === void 0) {
57135
+ const controller = new AbortController();
57136
+ const emit = (value) => {
57137
+ controller.signal.throwIfAborted();
57138
+ this.value = value;
57139
+ for (const listener of this.listeners) listener(value);
57140
+ };
57141
+ const promise = Promise.resolve().then(async () => {
57142
+ controller.signal.throwIfAborted();
57143
+ const value = await this.load(controller.signal, emit);
57144
+ controller.signal.throwIfAborted();
57145
+ emit(value);
57146
+ this.freshUntil = this.now() + this.ttl;
57147
+ this.failures = 0;
57148
+ this.retryAt = 0;
57149
+ this.error = void 0;
57150
+ return value;
57151
+ }).catch((error) => {
57152
+ if (controller.signal.aborted) throw error;
57153
+ this.failures += 1;
57154
+ const limited = /(?:\b429\b|rate.?limit|too many requests)/i.test(error instanceof Error ? error.message : String(error));
57155
+ this.retryAt = this.now() + Math.min(5 * 6e4, (limited ? 6e4 : 15e3) * 2 ** Math.min(4, this.failures - 1));
57156
+ this.error = error;
57157
+ throw new SpeakerReadDeferred(this.retryAt, error);
57158
+ }).finally(() => {
57159
+ if (this.pending?.controller === controller) this.pending = void 0;
57160
+ });
57161
+ this.pending = {
57162
+ controller,
57163
+ promise
57164
+ };
57165
+ }
57166
+ const pending = this.pending;
57167
+ return new Promise((resolve, reject) => {
57168
+ const listener = (value) => {
57169
+ if (!signal.aborted) progress(value);
57170
+ };
57171
+ this.listeners.add(listener);
57172
+ const cleanup = () => {
57173
+ signal.removeEventListener("abort", abort);
57174
+ this.listeners.delete(listener);
57175
+ if (this.listeners.size === 0 && this.pending === pending) this.releaseTimer = setTimeout(() => {
57176
+ if (this.listeners.size === 0 && this.pending === pending) pending.controller.abort();
57177
+ }, 250);
57178
+ };
57179
+ const abort = () => {
57180
+ cleanup();
57181
+ reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
57182
+ };
57183
+ signal.addEventListener("abort", abort, { once: true });
57184
+ pending.promise.then((value) => {
57185
+ cleanup();
57186
+ if (!signal.aborted) resolve(value);
57187
+ }, (error) => {
57188
+ cleanup();
57189
+ if (!signal.aborted) reject(error);
57190
+ });
57191
+ });
57192
+ }
57193
+ invalidate() {
57194
+ clearTimeout(this.releaseTimer);
57195
+ this.pending?.controller.abort();
57196
+ this.pending = void 0;
57197
+ this.freshUntil = 0;
57198
+ }
57199
+ dispose() {
57200
+ this.invalidate();
57201
+ this.listeners.clear();
57202
+ this.value = void 0;
57203
+ }
57204
+ };
57205
+ function pause(milliseconds, signal) {
57206
+ if (milliseconds <= 0) return Promise.resolve();
57207
+ return new Promise((resolve, reject) => {
57208
+ const abort = () => {
57209
+ clearTimeout(timer);
57210
+ reject(signal.reason);
57211
+ };
57212
+ const timer = setTimeout(() => {
57213
+ signal.removeEventListener("abort", abort);
57214
+ resolve();
57215
+ }, milliseconds);
57216
+ signal.addEventListener("abort", abort, { once: true });
57217
+ if (signal.aborted) abort();
57218
+ });
57219
+ }
57220
+ var RecognizedSpeakerDirectory = class {
57221
+ loaders;
57222
+ pageGapMs;
57223
+ now;
57224
+ accounts = /* @__PURE__ */ new Map();
57225
+ constructor(loaders = {
57226
+ marked: readSpeakerOptions,
57227
+ page: readSpeakerPage,
57228
+ presence: readSpeakerPresence
57229
+ }, pageGapMs = 750, now = Date.now) {
57230
+ this.loaders = loaders;
57231
+ this.pageGapMs = pageGapMs;
57232
+ this.now = now;
57233
+ }
57234
+ account(account) {
57235
+ const found = this.accounts.get(account);
57236
+ if (found !== void 0) return found;
57237
+ const pages = /* @__PURE__ */ new Map();
57238
+ let startedAt = 0, lastRequestAt = 0, completed = false;
57239
+ let state;
57240
+ state = {
57241
+ pages,
57242
+ marked: new SharedRead(6e4, (signal) => this.loaders.marked(signal), this.now),
57243
+ presence: new SharedRead(6e4, (signal) => this.loaders.presence(signal), this.now),
57244
+ candidates: new SharedRead(5 * 6e4, async (signal, emit) => {
57245
+ if (completed || this.now() - startedAt >= 5 * 6e4) pages.clear();
57246
+ if (pages.size === 0) startedAt = this.now();
57247
+ completed = false;
57248
+ let total;
57249
+ const result = await loadSpeakerCandidateSnapshot(async (cursor) => {
57250
+ const cached = pages.get(cursor);
57251
+ if (cached !== void 0) return cached;
57252
+ await pause(Math.max(0, lastRequestAt + this.pageGapMs - this.now()), signal);
57253
+ signal.throwIfAborted();
57254
+ lastRequestAt = this.now();
57255
+ const page = await this.loaders.page(cursor, signal);
57256
+ signal.throwIfAborted();
57257
+ if (page.cursorStale) {
57258
+ pages.clear();
57259
+ startedAt = this.now();
57260
+ } else if (page.projectionState === void 0 || page.projectionState === "fresh") pages.set(cursor, page);
57261
+ return page;
57262
+ }, signal, (snapshot) => {
57263
+ const first = pages.get("");
57264
+ total = first?.coverage !== "partial" ? first?.total : void 0;
57265
+ if (!state.candidates.value?.complete || snapshot.complete) emit({
57266
+ ...snapshot,
57267
+ total
57268
+ });
57269
+ });
57270
+ completed = result.complete;
57271
+ if (!result.complete) {
57272
+ if (!state.candidates.value?.complete) emit({
57273
+ ...result,
57274
+ total
57275
+ });
57276
+ throw new Error("说话人列表尚未完整,请稍后重试");
57277
+ }
57278
+ return {
57279
+ ...result,
57280
+ total
57281
+ };
57282
+ }, this.now)
57283
+ };
57284
+ this.accounts.set(account, state);
57285
+ return state;
57286
+ }
57287
+ peekMarked(account) {
57288
+ return this.accounts.get(account)?.marked.value;
57289
+ }
57290
+ peekPresence(account) {
57291
+ return this.accounts.get(account)?.presence.value;
57292
+ }
57293
+ peekCandidates(account) {
57294
+ return this.accounts.get(account)?.candidates.value;
57295
+ }
57296
+ readMarked(account, signal) {
57297
+ return this.account(account).marked.read(signal);
57298
+ }
57299
+ readPresence(account, signal) {
57300
+ return this.account(account).presence.read(signal);
57301
+ }
57302
+ refreshPresence(account, signal) {
57303
+ this.account(account).presence.invalidate();
57304
+ return this.readPresence(account, signal);
57305
+ }
57306
+ readCandidates(account, signal, progress) {
57307
+ return this.account(account).candidates.read(signal, progress);
57308
+ }
57309
+ invalidate(account) {
57310
+ const state = this.accounts.get(account);
57311
+ if (state === void 0) return;
57312
+ state.pages.clear();
57313
+ state.marked.invalidate();
57314
+ state.presence.invalidate();
57315
+ state.candidates.invalidate();
57316
+ }
57317
+ clear() {
57318
+ for (const state of this.accounts.values()) {
57319
+ state.marked.dispose();
57320
+ state.presence.dispose();
57321
+ state.candidates.dispose();
57322
+ }
57323
+ this.accounts.clear();
57324
+ }
57325
+ };
57326
+ const recognizedSpeakerDirectory = new RecognizedSpeakerDirectory();
57327
+ const accountKey = () => {
57328
+ const auth = arkmeAuthStore.getSnapshot().auth;
57329
+ return auth?.status === "authenticated" ? `${auth.environment}:${auth.userId}` : "";
57330
+ };
57331
+ let activeAccount = accountKey();
57332
+ arkmeAuthStore.subscribe(() => {
57333
+ const next = accountKey();
57334
+ if (next !== activeAccount) {
57335
+ activeAccount = next;
57336
+ recognizedSpeakerDirectory.clear();
57337
+ }
57338
+ });
57339
+ //#endregion
56816
57340
  //#region src/client/recordings/recording-speaker-options-store.ts
56817
57341
  function recordingSpeakerAccount(auth) {
56818
57342
  return auth?.status === "authenticated" && auth.userId !== void 0 ? `${auth.environment}:${auth.userId}` : void 0;
@@ -56868,6 +57392,10 @@ body[data-ds-dark-theme] .arkme-bot-identity {
56868
57392
  throw error;
56869
57393
  } finally {
56870
57394
  if (context.current()) {
57395
+ if (binding.account !== void 0) {
57396
+ recognizedSpeakerTracker.uncertain(binding.account);
57397
+ recognizedSpeakerDirectory.invalidate(binding.account);
57398
+ }
56871
57399
  recordingSpeakerOptions.reset();
56872
57400
  recordingSpeakerOptions.invalidate();
56873
57401
  recordingSpeakerItemContexts.reset((candidateKey) => candidateKey === key || !recordingSpeakerItemContexts.get(candidateKey).mutating);
@@ -123941,7 +124469,7 @@ body[data-ds-dark-theme] .arkme-mention-all-avatar [data-arkme-theme-image="dark
123941
124469
  function detailExtensionSenderName(item) {
123942
124470
  return item.senderDisplayName.trim() || (item.sourceKind === "agent_message" ? "Agent" : "未知用户");
123943
124471
  }
123944
- function detailExtensionAuthor(item, personalSource, profile) {
124472
+ function detailExtensionAuthor(item, personalSource, profile, sourceKind, conversationMembers = []) {
123945
124473
  const role = personalSource && item.sourceKind === "record_extension" ? item.selfRole : void 0;
123946
124474
  if (role !== void 0) return {
123947
124475
  name: role.name,
@@ -123949,16 +124477,21 @@ body[data-ds-dark-theme] .arkme-mention-all-avatar [data-arkme-theme-image="dark
123949
124477
  role
123950
124478
  };
123951
124479
  const ownRecord = personalSource && item.sourceKind === "record_extension" && profile !== void 0 && (item.recordOwnerUserId === void 0 || item.recordOwnerUserId === profile.userId);
124480
+ const currentNameFallback = ownRecord && item.senderNameSnapshot !== true && item.senderDisplayName.trim() === "我";
124481
+ const privateOwn = sourceKind === "private_chat" && item.senderIsMe === true && profile !== void 0;
124482
+ const member = item.senderMemberRef === void 0 ? void 0 : conversationMembers.find((candidate) => candidate.memberRef === item.senderMemberRef);
124483
+ const memberName = member?.displayName.trim();
123952
124484
  return {
123953
- name: ownRecord && item.senderNameSnapshot !== true && item.senderDisplayName.trim() === "我" ? profile?.nickname.trim() || profile?.displayName.trim() || detailExtensionSenderName(item) : detailExtensionSenderName(item),
123954
- avatar: arkmePersonalAvatarRef({
124485
+ name: privateOwn || currentNameFallback ? profile?.nickname.trim() || profile?.displayName.trim() || detailExtensionSenderName(item) : memberName && memberName !== "群成员" ? memberName : detailExtensionSenderName(item),
124486
+ avatar: privateOwn ? profile?.avatarRef.trim() || member?.avatarRef?.trim() || item.senderAvatarUrl || "" : member?.avatarRef?.trim() || arkmePersonalAvatarRef({
123955
124487
  isMe: ownRecord,
123956
124488
  ...item.senderAvatarUrl === void 0 ? {} : { avatarRef: item.senderAvatarUrl }
123957
- }, profile) ?? ""
124489
+ }, profile) || ""
123958
124490
  };
123959
124491
  }
123960
124492
  function detailExtensionTimelineItem$1(item, author) {
123961
124493
  const avatar = author.avatar;
124494
+ const mediaOnlyUnavailable = item.mediaUnavailable === true && item.mediaItems.length > 0 && item.title.trim() === "" && item.textContent.trim() === "" && (item.contentBlocks?.length ?? 0) === 0;
123962
124495
  return {
123963
124496
  itemUid: item.recordUid,
123964
124497
  senderName: author.name,
@@ -123972,7 +124505,7 @@ body[data-ds-dark-theme] .arkme-mention-all-avatar [data-arkme-theme-image="dark
123972
124505
  templateKind: item.templateKind,
123973
124506
  displayKind: item.displayKind,
123974
124507
  ...item.contentBlocks === void 0 ? {} : { contentBlocks: item.contentBlocks },
123975
- ...item.mediaUnavailable === true ? { mediaUnavailable: true } : {},
124508
+ ...mediaOnlyUnavailable ? { mediaUnavailable: true } : {},
123976
124509
  ...author.role === void 0 ? {} : { selfRole: author.role },
123977
124510
  ...avatar !== "" && !/^(https?:|data:|blob:)/iu.test(avatar) ? { avatarRef: avatar } : {}
123978
124511
  };
@@ -124076,7 +124609,7 @@ body[data-ds-dark-theme] .arkme-mention-all-avatar [data-arkme-theme-image="dark
124076
124609
  })]
124077
124610
  });
124078
124611
  }
124079
- function DetailExtensionContext({ state, optimistic, selectedRecordUid, sourceRef, sourceIdentityKey, shareWebsite, onMessageCopyLinkOpen, onMentionClick, isMentionClickable, onRetry, onSelect, personalSource, currentSelfProfile }) {
124612
+ function DetailExtensionContext({ state, optimistic, selectedRecordUid, sourceRef, sourceIdentityKey, shareWebsite, onMessageCopyLinkOpen, onMentionClick, isMentionClickable, onRetry, onSelect, personalSource, currentSelfProfile, sourceKind, conversationMembers }) {
124080
124613
  const context = state.kind === "success" ? state.context : void 0;
124081
124614
  const byUid = /* @__PURE__ */ new Map();
124082
124615
  for (const item of optimistic) byUid.set(item.recordUid, item);
@@ -124119,8 +124652,9 @@ body[data-ds-dark-theme] .arkme-mention-all-avatar [data-arkme-theme-image="dark
124119
124652
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
124120
124653
  style: styles$56.extensionContextList,
124121
124654
  children: orderedDetailExtensions(extensions, context?.parentRecordUid ?? "").map(({ item: extension, nested }) => {
124122
- const author = detailExtensionAuthor(extension, personalSource, currentSelfProfile);
124655
+ const author = detailExtensionAuthor(extension, personalSource, currentSelfProfile, sourceKind, conversationMembers);
124123
124656
  const timelineItem = detailExtensionTimelineItem$1(extension, author);
124657
+ const missingMedia = extension.mediaUnavailable === true && extension.mediaItems.length > 0 && (extension.title.trim() !== "" || extension.textContent.trim() !== "" || (extension.contentBlocks?.length ?? 0) > 0);
124124
124658
  const selected = extension.recordUid === selectedRecordUid;
124125
124659
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
124126
124660
  style: {
@@ -124146,26 +124680,44 @@ body[data-ds-dark-theme] .arkme-mention-all-avatar [data-arkme-theme-image="dark
124146
124680
  size: nested ? 28 : 32
124147
124681
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
124148
124682
  style: styles$56.extensionContextBody,
124149
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
124150
- style: styles$56.extensionContextHead,
124151
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
124152
- style: styles$56.extensionContextName,
124153
- children: author.name
124154
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
124155
- style: styles$56.extensionContextTime,
124156
- children: [dateLabel$1(extension.sendAtMillis), timeLabel$5(extension.sendAtMillis)].filter(Boolean).join(" ")
124157
- })]
124158
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ArkmeMessageContent, {
124159
- presentation: "detail",
124160
- item: timelineItem,
124161
- highlightMentions: true,
124162
- ...sourceRef === void 0 ? {} : { sourceRef },
124163
- ...sourceIdentityKey === void 0 ? {} : { sourceIdentityKey },
124164
- ...shareWebsite === void 0 ? {} : { shareWebsite },
124165
- ...onMessageCopyLinkOpen === void 0 ? {} : { onMessageCopyLinkOpen },
124166
- ...onMentionClick === void 0 ? {} : { onMentionClick },
124167
- ...isMentionClickable === void 0 ? {} : { isMentionClickable }
124168
- })]
124683
+ children: [
124684
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
124685
+ style: styles$56.extensionContextHead,
124686
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
124687
+ style: styles$56.extensionContextName,
124688
+ children: author.name
124689
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
124690
+ style: styles$56.extensionContextTime,
124691
+ children: [dateLabel$1(extension.sendAtMillis), timeLabel$5(extension.sendAtMillis)].filter(Boolean).join(" ")
124692
+ })]
124693
+ }),
124694
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ArkmeMessageContent, {
124695
+ presentation: "detail",
124696
+ item: timelineItem,
124697
+ highlightMentions: true,
124698
+ ...sourceRef === void 0 ? {} : { sourceRef },
124699
+ ...sourceIdentityKey === void 0 ? {} : { sourceIdentityKey },
124700
+ ...shareWebsite === void 0 ? {} : { shareWebsite },
124701
+ ...onMessageCopyLinkOpen === void 0 ? {} : { onMessageCopyLinkOpen },
124702
+ ...onMentionClick === void 0 ? {} : { onMentionClick },
124703
+ ...isMentionClickable === void 0 ? {} : { isMentionClickable }
124704
+ }),
124705
+ missingMedia && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
124706
+ "data-arkme-missing-extension-media": true,
124707
+ style: {
124708
+ display: "inline-flex",
124709
+ alignItems: "center",
124710
+ gap: 4,
124711
+ marginTop: 5,
124712
+ color: arkmeTheme.tertiary,
124713
+ fontSize: 12
124714
+ },
124715
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(o$22, {
124716
+ size: 13,
124717
+ "aria-hidden": true
124718
+ }), tr$1("附件暂不可用")]
124719
+ })
124720
+ ]
124169
124721
  })]
124170
124722
  }, extension.recordUid);
124171
124723
  })
@@ -124212,10 +124764,18 @@ body[data-ds-dark-theme] .arkme-mention-all-avatar [data-arkme-theme-image="dark
124212
124764
  const controller = new AbortController();
124213
124765
  listAbortRef.current = controller;
124214
124766
  setRelatedState((current) => current.kind === "success" ? current : { kind: "loading" });
124215
- callArkme("source.related-quick-notes.from-message", {
124767
+ const readRelated = () => callArkme("source.related-quick-notes.from-message", {
124216
124768
  sourceRef: normalizedSourceRef,
124217
124769
  messageActionRef
124218
- }, controller.signal).then((list) => {
124770
+ }, controller.signal);
124771
+ readRelated().then(async (first) => {
124772
+ if (first.items.length > 0 || controller.signal.aborted) return first;
124773
+ try {
124774
+ return await readRelated();
124775
+ } catch {
124776
+ return first;
124777
+ }
124778
+ }).then((list) => {
124219
124779
  if (controller.signal.aborted || listAbortRef.current !== controller) return;
124220
124780
  setRelatedState(list.items.length === 0 ? { kind: "empty" } : {
124221
124781
  kind: "success",
@@ -124362,9 +124922,10 @@ body[data-ds-dark-theme] .arkme-mention-all-avatar [data-arkme-theme-image="dark
124362
124922
  const textContent = showOriginal && item.aiPolish?.originalText !== void 0 ? item.aiPolish.originalText : item.aiPolish?.state === "polished" && item.aiPolish.polishedText !== void 0 ? item.aiPolish.polishedText : item.textContent;
124363
124923
  const canToggle = item.aiPolish?.state === "polished" && item.aiPolish.originalText !== void 0 && item.aiPolish.polishedText !== void 0;
124364
124924
  const personalSource = sourceKind === "send_to_self" || sourceKind === "topic" || sourceKind === "default_category";
124925
+ const privateOwnCurrentName = sourceKind === "private_chat" && item.isMe && item.senderKind !== "bot" && currentSelfProfile !== void 0;
124365
124926
  const currentNameFallback = personalSource && item.isMe && item.avatarSnapshot === true && item.senderNameSnapshot !== true && currentSelfProfile !== void 0;
124366
124927
  const selfRole = sourceKind === void 0 ? void 0 : arkmeSelfRoleForPresentation(item, sourceKind);
124367
- const authorName = selfRole?.name ?? (currentNameFallback ? currentSelfProfile?.nickname.trim() || currentSelfProfile?.displayName.trim() || item.senderName : arkmeTimelineDetailSenderText(item, conversationMembers));
124928
+ const authorName = selfRole?.name ?? (privateOwnCurrentName || currentNameFallback ? currentSelfProfile?.nickname.trim() || currentSelfProfile?.displayName.trim() || item.senderName : arkmeTimelineDetailSenderText(item, conversationMembers));
124368
124929
  const authorAvatarRef = personalSource ? arkmePersonalAvatarRef(item, currentSelfProfile) : item.avatarRef;
124369
124930
  const roleAvatarFallback = selfRole === void 0 ? void 0 : arkmeSelfRoleAvatarFallback(selfRole);
124370
124931
  const topicTitle = selfTopicPath?.trim() || item.selfTopic?.title?.trim() || tr$1("未指定主题");
@@ -124579,6 +125140,8 @@ body[data-ds-dark-theme] .arkme-mention-all-avatar [data-arkme-theme-image="dark
124579
125140
  state: extensionState,
124580
125141
  optimistic: optimisticExtensions,
124581
125142
  personalSource,
125143
+ sourceKind,
125144
+ conversationMembers,
124582
125145
  ...currentSelfProfile === void 0 ? {} : { currentSelfProfile },
124583
125146
  ...selectedExtensionRecordUid === void 0 ? {} : { selectedRecordUid: selectedExtensionRecordUid },
124584
125147
  ...sourceRef === void 0 ? {} : { sourceRef },
@@ -154041,7 +154604,7 @@ body:has([data-arkme-self-topic-menu]) > [role="menu"] { z-index: 10030; }
154041
154604
  };
154042
154605
  //#endregion
154043
154606
  //#region node_modules/@rc-component/util/es/hooks/useMemo.js
154044
- function useMemo$36(getValue, condition, shouldUpdate) {
154607
+ function useMemo$37(getValue, condition, shouldUpdate) {
154045
154608
  const cacheRef = react.useRef({});
154046
154609
  if (!("value" in cacheRef.current) || shouldUpdate(cacheRef.current.condition, condition)) {
154047
154610
  cacheRef.current.value = getValue();
@@ -154154,7 +154717,7 @@ body:has([data-arkme-self-topic-menu]) > [role="menu"] { z-index: 10030; }
154154
154717
  };
154155
154718
  };
154156
154719
  const useComposeRef = (...refs) => {
154157
- return useMemo$36(() => composeRef(...refs), refs, (prev, next) => prev.length !== next.length || prev.every((ref, i) => ref !== next[i]));
154720
+ return useMemo$37(() => composeRef(...refs), refs, (prev, next) => prev.length !== next.length || prev.every((ref, i) => ref !== next[i]));
154158
154721
  };
154159
154722
  const supportRef = (nodeOrComponent) => {
154160
154723
  if (!nodeOrComponent) return false;
@@ -171867,8 +172430,19 @@ html body {
171867
172430
  o$11.displayName = "CheckIcon";
171868
172431
  const n$6 = o$11;
171869
172432
  //#endregion
172433
+ //#region src/client/ArkmeModelPricing.tsx
172434
+ /** One disclosure per menu keeps pricing available without repeating it in every row. */
172435
+ function ArkmeModelPricing({ models }) {
172436
+ const pricedModels = models.filter((model) => model.description);
172437
+ if (pricedModels.length === 0) return null;
172438
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
172439
+ className: "arkme-model-price",
172440
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", { children: tr$1("按用量扣积分 · 计费说明") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dl", { children: pricedModels.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: model.name }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: model.description })] }, model.id)) })]
172441
+ });
172442
+ }
172443
+ //#endregion
171870
172444
  //#region src/client/arkme-model-select.css?inline
171871
- var arkme_model_select_default = ".arkme-model-select {\n min-width: 0;\n font-family: inherit;\n position: relative;\n}\n\n.arkme-model-select button {\n font: inherit;\n cursor: pointer;\n}\n\n.arkme-model-select button:disabled {\n opacity: .5;\n cursor: default;\n}\n\n.arkme-model-select button:focus-visible {\n outline-offset: -2px;\n outline: 2px solid #3578e5;\n}\n\n.arkme-model-trigger {\n max-width: min(360px, 45vw);\n height: 28px;\n color: var(--dsw-alias-label-secondary, #626772);\n background: none;\n border: 0;\n border-radius: 14px;\n align-items: center;\n gap: 5px;\n padding: 0 8px;\n display: flex;\n font-size: 13px !important;\n}\n\n.arkme-model-trigger > span:first-child {\n text-overflow: ellipsis;\n white-space: nowrap;\n overflow: hidden;\n}\n\n.arkme-model-trigger small {\n flex-shrink: 0;\n}\n\n.arkme-model-menu {\n z-index: 30;\n box-sizing: border-box;\n border: 1px solid var(--dsw-alias-border-inverted, #e8e8eb);\n background: var(--dsw-specific-menu, #fff);\n width: max-content;\n min-width: min(280px, 100vw - 32px);\n max-width: min(420px, 100vw - 32px);\n max-height: min(400px, 100vh - 96px);\n color: var(--dsw-alias-label-primary, #20232c);\n border-radius: 12px;\n padding: 5px;\n position: absolute;\n bottom: calc(100% + 8px);\n right: 0;\n overflow-y: auto;\n box-shadow: 0 8px 30px #0002;\n}\n\n.arkme-model-group-heading {\n color: var(--dsw-alias-label-tertiary, #8a8e96);\n justify-content: space-between;\n align-items: center;\n gap: 18px;\n padding: 7px 9px 4px;\n font-size: 12px;\n display: flex;\n}\n\n.arkme-model-balance-actions {\n flex-shrink: 0;\n gap: 8px;\n display: flex;\n}\n\n.arkme-model-balance-actions button {\n color: #3478e5;\n white-space: nowrap;\n background: none;\n border: 0;\n padding: 2px 0;\n}\n\n.arkme-model-balance-actions button:hover {\n text-decoration: underline;\n}\n\n.arkme-model-option {\n width: 100%;\n min-height: 38px;\n color: inherit;\n text-align: left;\n background: none;\n border: 0;\n border-radius: 8px;\n justify-content: space-between;\n align-items: center;\n gap: 18px;\n padding: 8px 10px;\n display: flex;\n font-size: 14px !important;\n}\n\n.arkme-model-option:hover, .arkme-model-trigger:hover {\n background: var(--dsw-alias-interactive-bg-hover, #f3f4f6);\n}\n\n.arkme-model-option > span:first-child {\n overflow-wrap: anywhere;\n min-width: 0;\n}\n\n.arkme-model-option small {\n color: var(--dsw-alias-label-tertiary, #8a8e96);\n font-size: 12px;\n display: block;\n}\n\n.arkme-model-status, .arkme-model-error {\n padding: 8px 10px;\n font-size: 12px;\n}\n\n.arkme-model-error {\n color: var(--dsw-alias-state-error-primary, #b54841);\n}\n\n.arkme-model-error button {\n color: inherit;\n background: none;\n border: 0;\n text-decoration: underline;\n}\n\n.arkme-model-price {\n color: var(--dsw-alias-label-tertiary, #8a8e96);\n padding: 0 10px 8px;\n font-size: 12px;\n}\n\n.arkme-model-price summary {\n cursor: pointer;\n}\n\n.arkme-model-price small {\n overflow-wrap: anywhere;\n padding-top: 6px;\n line-height: 1.6;\n display: block;\n}\n";
172445
+ var arkme_model_select_default = ".arkme-model-select {\n min-width: 0;\n font-family: inherit;\n position: relative;\n}\n\n.arkme-model-select button {\n font: inherit;\n cursor: pointer;\n}\n\n.arkme-model-select button:disabled {\n opacity: .5;\n cursor: default;\n}\n\n.arkme-model-select button:focus-visible {\n outline-offset: -2px;\n outline: 2px solid #3578e5;\n}\n\n.arkme-model-trigger {\n max-width: min(360px, 45vw);\n height: 28px;\n color: var(--dsw-alias-label-secondary, #626772);\n background: none;\n border: 0;\n border-radius: 14px;\n align-items: center;\n gap: 5px;\n padding: 0 8px;\n display: flex;\n font-size: 13px !important;\n}\n\n.arkme-model-trigger > span:first-child {\n text-overflow: ellipsis;\n white-space: nowrap;\n overflow: hidden;\n}\n\n.arkme-model-trigger small {\n flex-shrink: 0;\n}\n\n.arkme-model-menu {\n z-index: 30;\n box-sizing: border-box;\n border: 1px solid var(--dsw-alias-border-inverted, #e8e8eb);\n background: var(--dsw-specific-menu, #fff);\n width: max-content;\n min-width: min(280px, 100vw - 32px);\n max-width: min(420px, 100vw - 32px);\n max-height: min(400px, 100vh - 96px);\n color: var(--dsw-alias-label-primary, #20232c);\n border-radius: 12px;\n padding: 5px;\n position: absolute;\n bottom: calc(100% + 8px);\n right: 0;\n overflow-y: auto;\n box-shadow: 0 8px 30px #0002;\n}\n\n.arkme-model-group-heading {\n color: var(--dsw-alias-label-tertiary, #8a8e96);\n justify-content: space-between;\n align-items: center;\n gap: 18px;\n padding: 7px 9px 4px;\n font-size: 12px;\n display: flex;\n}\n\n.arkme-model-balance-actions {\n flex-shrink: 0;\n gap: 8px;\n display: flex;\n}\n\n.arkme-model-balance-actions button {\n color: #3478e5;\n white-space: nowrap;\n background: none;\n border: 0;\n padding: 2px 0;\n}\n\n.arkme-model-balance-actions button:hover {\n text-decoration: underline;\n}\n\n.arkme-model-option {\n width: 100%;\n min-height: 38px;\n color: inherit;\n text-align: left;\n background: none;\n border: 0;\n border-radius: 8px;\n justify-content: space-between;\n align-items: center;\n gap: 18px;\n padding: 8px 10px;\n display: flex;\n font-size: 14px !important;\n}\n\n.arkme-model-option:hover, .arkme-model-trigger:hover {\n background: var(--dsw-alias-interactive-bg-hover, #f3f4f6);\n}\n\n.arkme-model-option > span:first-child {\n overflow-wrap: anywhere;\n min-width: 0;\n}\n\n.arkme-model-option small {\n color: var(--dsw-alias-label-tertiary, #8a8e96);\n font-size: 12px;\n display: block;\n}\n\n.arkme-model-status, .arkme-model-error {\n padding: 8px 10px;\n font-size: 12px;\n}\n\n.arkme-model-error {\n color: var(--dsw-alias-state-error-primary, #b54841);\n}\n\n.arkme-model-error button {\n color: inherit;\n background: none;\n border: 0;\n text-decoration: underline;\n}\n\n.arkme-model-price {\n border-top: 1px solid var(--dsw-alias-border-inverted, #e8e8eb);\n color: var(--dsw-alias-label-tertiary, #8a8e96);\n margin-top: 4px;\n padding: 10px;\n font-size: 12px;\n}\n\n.arkme-model-price summary {\n cursor: pointer;\n}\n\n.arkme-model-price summary:focus-visible {\n outline-offset: 2px;\n outline: 2px solid #3578e5;\n}\n\n.arkme-model-price dl {\n overflow-wrap: anywhere;\n margin: 0;\n line-height: 1.6;\n}\n\n.arkme-model-price dl > div {\n margin-top: 10px;\n}\n\n.arkme-model-price dt {\n color: var(--dsw-alias-label-primary, #20232c);\n}\n\n.arkme-model-price dd {\n margin: 2px 0 0;\n}\n";
171872
172446
  //#endregion
171873
172447
  //#region src/client/ArkoModelMenu.tsx
171874
172448
  function ArkoModelMenu({ anchor, catalog, busy, error, onSelect, onClose }) {
@@ -171938,7 +172512,7 @@ html body {
171938
172512
  "Home",
171939
172513
  "End"
171940
172514
  ].includes(event.key)) return;
171941
- const items = Array.from(menu.current?.querySelectorAll("button:not(:disabled)") ?? []);
172515
+ const items = Array.from(menu.current?.querySelectorAll("button:not(:disabled), summary") ?? []);
171942
172516
  if (!items.length) return;
171943
172517
  event.preventDefault();
171944
172518
  event.stopPropagation();
@@ -171957,7 +172531,7 @@ html body {
171957
172531
  children: [
171958
172532
  catalog.options.map((option) => {
171959
172533
  const selected = option.routeKey === catalog.effectiveRouteKey;
171960
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
172534
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
171961
172535
  type: "button",
171962
172536
  className: "arkme-model-option",
171963
172537
  role: "menuitemradio",
@@ -171977,11 +172551,13 @@ html body {
171977
172551
  style: { flex: "none" },
171978
172552
  "aria-hidden": true
171979
172553
  })]
171980
- }), option.costDescription && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
171981
- className: "arkme-model-price",
171982
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", { children: tr$1("按用量扣积分 · 计费说明") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: option.costDescription })]
171983
- })] }, option.routeKey);
172554
+ }) }, option.routeKey);
171984
172555
  }),
172556
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ArkmeModelPricing, { models: catalog.options.map((option) => ({
172557
+ id: option.routeKey,
172558
+ name: option.displayName,
172559
+ description: option.costDescription
172560
+ })) }),
171985
172561
  busy && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
171986
172562
  className: "arkme-model-status",
171987
172563
  role: "status",
@@ -177478,6 +178054,132 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
177478
178054
  e$36.displayName = "FingerprintIcon";
177479
178055
  const p$2 = e$36;
177480
178056
  //#endregion
178057
+ //#region src/client/recordings/RecognizedSpeakerEntry.tsx
178058
+ function RecognizedSpeakerEntry({ accountKey, active, onOpen, style, readMarked = readSpeakerOptions, readPage = readSpeakerPage, directory: providedDirectory }) {
178059
+ useArkmeLocale();
178060
+ const directory = (0, react$1.useMemo)(() => providedDirectory ?? (readMarked === readSpeakerOptions && readPage === readSpeakerPage ? recognizedSpeakerDirectory : new RecognizedSpeakerDirectory({
178061
+ marked: readMarked,
178062
+ page: readPage,
178063
+ presence: readSpeakerPresence
178064
+ }, 0)), [
178065
+ providedDirectory,
178066
+ readMarked,
178067
+ readPage
178068
+ ]);
178069
+ const snapshot = (0, react$1.useSyncExternalStore)(recognizedSpeakerTracker.subscribe, () => recognizedSpeakerTracker.get(accountKey), () => recognizedSpeakerTracker.get(accountKey));
178070
+ (0, react$1.useEffect)(() => {
178071
+ if (!active || accountKey === void 0) return;
178072
+ const controller = new AbortController();
178073
+ let busy = false;
178074
+ let retryTimer;
178075
+ const refresh = async () => {
178076
+ if (busy || controller.signal.aborted || typeof document !== "undefined" && document.visibilityState === "hidden") return;
178077
+ busy = true;
178078
+ try {
178079
+ let marked;
178080
+ const updateTotal = () => {
178081
+ const value = directory.peekCandidates(accountKey);
178082
+ if (marked !== void 0 && value?.total !== void 0 && !controller.signal.aborted) recognizedSpeakerTracker.setTotal(accountKey, markedSpeakerKeys(marked).length + value.total);
178083
+ };
178084
+ const [, result] = await Promise.all([directory.readMarked(accountKey, controller.signal).then((value) => {
178085
+ marked = value;
178086
+ updateTotal();
178087
+ }), directory.readCandidates(accountKey, controller.signal, updateTotal)]);
178088
+ if (controller.signal.aborted) return;
178089
+ if (result.complete && marked !== void 0) recognizedSpeakerTracker.observe(accountKey, marked, result.items);
178090
+ else recognizedSpeakerTracker.uncertain(accountKey);
178091
+ } catch (error) {
178092
+ if (!controller.signal.aborted) {
178093
+ recognizedSpeakerTracker.uncertain(accountKey);
178094
+ const delay = speakerRetryDelay(error);
178095
+ if (delay !== void 0) {
178096
+ clearTimeout(retryTimer);
178097
+ retryTimer = setTimeout(() => {
178098
+ refresh();
178099
+ }, delay);
178100
+ }
178101
+ }
178102
+ } finally {
178103
+ busy = false;
178104
+ }
178105
+ };
178106
+ refresh();
178107
+ const timer = setInterval(() => {
178108
+ refresh();
178109
+ }, 6e4);
178110
+ const onVisible = () => {
178111
+ refresh();
178112
+ };
178113
+ if (typeof document !== "undefined") document.addEventListener("visibilitychange", onVisible);
178114
+ return () => {
178115
+ controller.abort();
178116
+ clearInterval(timer);
178117
+ clearTimeout(retryTimer);
178118
+ if (typeof document !== "undefined") document.removeEventListener("visibilitychange", onVisible);
178119
+ };
178120
+ }, [
178121
+ accountKey,
178122
+ active,
178123
+ directory
178124
+ ]);
178125
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
178126
+ "data-arkme-feedback": "recording-action",
178127
+ type: "button",
178128
+ style: {
178129
+ ...style,
178130
+ flexDirection: "column",
178131
+ minWidth: 0,
178132
+ fontSize: 13
178133
+ },
178134
+ onClick: onOpen,
178135
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
178136
+ style: {
178137
+ display: "flex",
178138
+ maxWidth: "100%",
178139
+ alignItems: "center",
178140
+ justifyContent: "center",
178141
+ gap: 4
178142
+ },
178143
+ children: [
178144
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(n$7, {
178145
+ size: 16,
178146
+ style: { flexShrink: 0 },
178147
+ "aria-hidden": true
178148
+ }),
178149
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
178150
+ title: tr$1("已识别说话人"),
178151
+ style: {
178152
+ minWidth: 0,
178153
+ overflow: "hidden",
178154
+ textOverflow: "ellipsis"
178155
+ },
178156
+ children: tr$1("已识别说话人")
178157
+ }),
178158
+ snapshot.total !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
178159
+ style: {
178160
+ flexShrink: 0,
178161
+ fontVariantNumeric: "tabular-nums"
178162
+ },
178163
+ children: snapshot.total
178164
+ }),
178165
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(s$21, {
178166
+ size: 12,
178167
+ style: { flexShrink: 0 },
178168
+ "aria-hidden": true
178169
+ })
178170
+ ]
178171
+ }), (snapshot.newCount ?? 0) > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
178172
+ style: {
178173
+ fontSize: 11,
178174
+ lineHeight: "15px",
178175
+ fontWeight: 400
178176
+ },
178177
+ "aria-live": "polite",
178178
+ children: tr$1("新识别 {v0} 个", { v0: snapshot.newCount })
178179
+ })]
178180
+ });
178181
+ }
178182
+ //#endregion
177481
178183
  //#region src/recording-time.ts
177482
178184
  const RECORDING_MINIMUM_YEAR = 1970;
177483
178185
  function recordingMinimumLocalDateMillis() {
@@ -185865,7 +186567,7 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
185865
186567
  toolbar: {
185866
186568
  marginTop: 12,
185867
186569
  display: "grid",
185868
- gridTemplateColumns: "repeat(3, minmax(0, 1fr))",
186570
+ gridTemplateColumns: "repeat(2, minmax(0, 1fr))",
185869
186571
  alignItems: "stretch",
185870
186572
  gap: 8
185871
186573
  },
@@ -188095,25 +188797,14 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
188095
188797
  "aria-hidden": true
188096
188798
  }), tr$1("全天候录音")]
188097
188799
  }),
188098
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
188099
- "data-arkme-feedback": "recording-action",
188100
- type: "button",
188800
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RecognizedSpeakerEntry, {
188101
188801
  style: styles$21.mobileGuideButton,
188102
- onClick: () => {
188802
+ accountKey: auth.auth?.status === "authenticated" ? `${auth.auth.environment}:${auth.auth.userId}` : void 0,
188803
+ active: active && ui.mode === "recordings",
188804
+ onOpen: () => {
188103
188805
  tour.finish(false);
188104
188806
  arkmeUi.showRecognizedSpeakers(selectedDate.getTime());
188105
- },
188106
- children: [
188107
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(n$7, {
188108
- size: 16,
188109
- "aria-hidden": true
188110
- }),
188111
- tr$1("已识别说话人"),
188112
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(s$21, {
188113
- size: 12,
188114
- "aria-hidden": true
188115
- })
188116
- ]
188807
+ }
188117
188808
  }),
188118
188809
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
188119
188810
  "data-arkme-feedback": "recording-action",
@@ -189487,6 +190178,82 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
189487
190178
  });
189488
190179
  }
189489
190180
  //#endregion
190181
+ //#region src/client/recordings/SpeakerSelfGuide.tsx
190182
+ const loadVoiceprint = (signal) => callArkme("voiceprint.status", {}, signal);
190183
+ function SpeakerSelfGuide({ onOpenRecordings, readVoiceprint = loadVoiceprint }) {
190184
+ useArkmeLocale();
190185
+ const [state, setState] = (0, react$1.useState)({ loading: true });
190186
+ (0, react$1.useEffect)(() => {
190187
+ const controller = new AbortController();
190188
+ readVoiceprint(controller.signal).then((value) => {
190189
+ if (!controller.signal.aborted) setState({
190190
+ loading: false,
190191
+ value
190192
+ });
190193
+ }).catch(() => {
190194
+ if (!controller.signal.aborted) setState({ loading: false });
190195
+ });
190196
+ return () => {
190197
+ controller.abort();
190198
+ };
190199
+ }, [readVoiceprint]);
190200
+ if (state.loading) return null;
190201
+ const pending = state.value?.enrollmentPending === true;
190202
+ const hasVoiceprint = state.value?.hasVoiceprint === true;
190203
+ const title = pending ? "你的声纹正在处理中" : hasVoiceprint ? "已录入声纹,暂未在录音中匹配到你" : "还没有标记你的声音";
190204
+ const actionStyle = {
190205
+ border: 0,
190206
+ padding: 0,
190207
+ background: "transparent",
190208
+ color: arkmeTheme.accent,
190209
+ cursor: "pointer",
190210
+ font: "inherit",
190211
+ fontSize: 12
190212
+ };
190213
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("aside", {
190214
+ "aria-label": tr$1("识别我的声音"),
190215
+ style: {
190216
+ padding: "12px 14px",
190217
+ marginBottom: 16,
190218
+ borderRadius: 10,
190219
+ background: arkmeTheme.layer1,
190220
+ fontSize: 13
190221
+ },
190222
+ children: [
190223
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: tr$1(title) }),
190224
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
190225
+ style: {
190226
+ marginTop: 5,
190227
+ color: arkmeTheme.secondary,
190228
+ fontSize: 12
190229
+ },
190230
+ children: tr$1("打开你的录音,点击说话人名称并选择自己;请先试听确认。")
190231
+ }),
190232
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
190233
+ style: {
190234
+ display: "flex",
190235
+ gap: 18,
190236
+ flexWrap: "wrap",
190237
+ marginTop: 8
190238
+ },
190239
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
190240
+ type: "button",
190241
+ style: actionStyle,
190242
+ onClick: onOpenRecordings,
190243
+ children: tr$1("从录音中标记我")
190244
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
190245
+ type: "button",
190246
+ style: actionStyle,
190247
+ onClick: () => {
190248
+ arkmeUi.showVoiceprint();
190249
+ },
190250
+ children: tr$1(state.value !== void 0 && !hasVoiceprint && !pending ? "录入我的声纹" : "管理声纹")
190251
+ })]
190252
+ })
190253
+ ]
190254
+ });
190255
+ }
190256
+ //#endregion
189490
190257
  //#region src/client/ArkmeRecognizedSpeakersSurface.tsx
189491
190258
  /** Group only by a stable owner identity, never by a shared name or speaker number. */
189492
190259
  function identifiedSpeakerRows(options, candidates) {
@@ -189514,7 +190281,9 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
189514
190281
  name: tr$1("说话人 {v0}", { v0: candidate.speakerToken?.trim() || candidate.displayName }),
189515
190282
  token: candidate.speakerToken?.trim() || "",
189516
190283
  subtitle: candidate.subtitle,
189517
- candidateRef: candidate.candidateRef
190284
+ candidateRef: candidate.candidateRef,
190285
+ dayCount: candidate.appearanceDays,
190286
+ lastSeenAt: candidate.latestAtMillis
189518
190287
  });
189519
190288
  }
189520
190289
  return [...[...marked.values()].sort((left, right) => left.name.localeCompare(right.name)), ...unmarked.values()];
@@ -189600,6 +190369,19 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
189600
190369
  font: "inherit",
189601
190370
  fontSize: 13
189602
190371
  },
190372
+ sort: {
190373
+ flex: "none",
190374
+ height: 36,
190375
+ maxWidth: "100%",
190376
+ padding: "0 9px",
190377
+ border: `1px solid ${arkmeTheme.border}`,
190378
+ borderRadius: 9,
190379
+ background: arkmeTheme.base,
190380
+ color: arkmeTheme.text,
190381
+ font: "inherit",
190382
+ fontSize: 13,
190383
+ cursor: "pointer"
190384
+ },
189603
190385
  list: {
189604
190386
  minWidth: 0,
189605
190387
  margin: 0,
@@ -189693,39 +190475,31 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
189693
190475
  borderBottom: `1px solid ${arkmeTheme.borderSoft}`
189694
190476
  }
189695
190477
  };
189696
- const defaultLoadMarked = async (signal) => await callArkme("recordings.speaker.options", {}, signal);
189697
- const defaultLoadPresence = async (signal) => await callArkme("recordings.speaker.presence", {}, signal);
189698
- const defaultLoadMarkedMembers = async (speakerRef, signal) => await callArkme("recordings.speaker.members", { speakerRef }, signal);
189699
- const defaultLoadUnmarked = async (cursor, signal) => await callArkme("directory.list", {
189700
- section: "unmarked-speakers",
189701
- limit: 50,
189702
- ...cursor === "" ? { refresh: true } : { cursor }
190478
+ const defaultLoadMarkedMembers = async (speakerRef, signal, expectedVersion) => await callArkme("recordings.speaker.members", {
190479
+ speakerRef,
190480
+ ...expectedVersion === void 0 ? {} : { expectedVersion }
189703
190481
  }, signal);
189704
190482
  function failureMessage(error) {
189705
190483
  return error instanceof Error && error.message.trim() !== "" ? error.message : tr$1("说话人列表暂时无法加载");
189706
190484
  }
189707
190485
  function markedPresenceLabel(stat, result, loading, error) {
190486
+ if (loading || result?.state === "building") return tr$1("出现统计整理中");
190487
+ if (error !== "" || result?.state === "failed" || result === void 0) return tr$1("出现统计暂不可用");
190488
+ if (result.state === "stale") return tr$1("出现统计更新中");
189708
190489
  if (stat !== void 0) {
189709
190490
  const date = new Date(stat.lastSeenAt);
189710
190491
  if (!Number.isNaN(date.getTime())) {
189711
190492
  const two = (value) => String(value).padStart(2, "0");
189712
190493
  const recent = `${date.getFullYear()}-${two(date.getMonth() + 1)}-${two(date.getDate())} ${two(date.getHours())}:${two(date.getMinutes())}`;
189713
- return result?.scope === "recent-seven-days" ? tr$1("近 7 天出现 {v0} 天 · 最近 {v1}", {
189714
- v0: stat.dayCount,
189715
- v1: recent
189716
- }) : tr$1("出现 {v0} 天 · 最近 {v1}", {
190494
+ return tr$1("出现 {v0} 天 · 最近 {v1}", {
189717
190495
  v0: stat.dayCount,
189718
190496
  v1: recent
189719
190497
  });
189720
190498
  }
189721
190499
  }
189722
- if (loading || result?.state === "building") return tr$1("出现统计整理中");
189723
- if (error !== "" || result?.state === "failed" || result === void 0) return tr$1("出现统计暂不可用");
189724
- if (result.state === "stale") return tr$1("出现统计更新中");
189725
- if (result.scope === "recent-seven-days") return tr$1("近 7 天未见已转写发声 · 全历史待接口");
189726
190500
  return tr$1("暂无可统计的录音片段");
189727
190501
  }
189728
- function MarkedSpeakerDetail({ accountKey, speaker, loadMembers }) {
190502
+ function MarkedSpeakerDetail({ accountKey, speaker, loadMembers, version, onVersionChanged }) {
189729
190503
  const [state, setState] = (0, react$1.useState)({
189730
190504
  loading: true,
189731
190505
  error: ""
@@ -189736,25 +190510,38 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
189736
190510
  loading: true,
189737
190511
  error: ""
189738
190512
  });
189739
- loadMembers(speaker.speakerRef, controller.signal).then((result) => {
189740
- if (!controller.signal.aborted) setState({
189741
- loading: false,
189742
- result,
189743
- error: ""
189744
- });
189745
- }).catch((error) => {
189746
- if (!controller.signal.aborted) setState({
189747
- loading: false,
189748
- error: failureMessage(error)
189749
- });
189750
- });
190513
+ let timer;
190514
+ const load = async () => {
190515
+ try {
190516
+ const result = await loadMembers(speaker.speakerRef, controller.signal, version);
190517
+ if (controller.signal.aborted) return;
190518
+ setState({
190519
+ loading: false,
190520
+ result,
190521
+ error: ""
190522
+ });
190523
+ if (result.state === "stale" && result.version !== void 0 && result.version !== version) onVersionChanged();
190524
+ if (result.state !== "fresh") timer = setTimeout(() => {
190525
+ load();
190526
+ }, Math.max(5e3, result.retryAfterMs ?? 1e4));
190527
+ } catch (error) {
190528
+ if (!controller.signal.aborted) setState({
190529
+ loading: false,
190530
+ error: failureMessage(error)
190531
+ });
190532
+ }
190533
+ };
190534
+ load();
189751
190535
  return () => {
190536
+ clearTimeout(timer);
189752
190537
  controller.abort();
189753
190538
  };
189754
190539
  }, [
189755
190540
  accountKey,
189756
190541
  speaker.speakerRef,
189757
- loadMembers
190542
+ loadMembers,
190543
+ version,
190544
+ onVersionChanged
189758
190545
  ]);
189759
190546
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
189760
190547
  "aria-label": tr$1("已标记说话人详情"),
@@ -189780,23 +190567,27 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
189780
190567
  margin: "0 0 8px",
189781
190568
  whiteSpace: "normal"
189782
190569
  },
189783
- children: tr$1("仅展示近 7 天已转写片段中可核实的关联;完整历史待接口。")
190570
+ children: tr$1("展示全部历史已转写片段中可核实的关联。")
189784
190571
  }),
189785
- state.loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
190572
+ state.loading || state.result?.state === "building" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
189786
190573
  role: "status",
189787
190574
  style: styles$20.state,
189788
190575
  children: tr$1("正在查找关联说话人…")
189789
- }) : state.error !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
190576
+ }) : state.error !== "" || state.result?.state === "failed" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
189790
190577
  role: "alert",
189791
190578
  style: styles$20.error,
189792
- children: state.error
190579
+ children: state.error || tr$1("出现统计暂不可用")
190580
+ }) : state.result?.state === "stale" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
190581
+ role: "status",
190582
+ style: styles$20.state,
190583
+ children: tr$1("出现统计更新中")
189793
190584
  }) : state.result?.items.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
189794
190585
  role: "status",
189795
190586
  style: styles$20.state,
189796
- children: state.result.dayCount > 0 ? tr$1("近 7 天有发声,但未找到稳定的原始识别身份。") : tr$1("近 7 天未找到关联,不代表完整历史中没有。")
190587
+ children: state.result.dayCount > 0 ? tr$1("有发声,但未找到稳定的原始识别身份。") : tr$1("暂无可核实的关联说话人。")
189797
190588
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
189798
190589
  style: styles$20.list,
189799
- children: state.result?.items.map((member, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
190590
+ children: state.result?.items.map((member) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
189800
190591
  style: styles$20.memberRow,
189801
190592
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UnmarkedSpeakerTokenAvatar, {
189802
190593
  token: member.token,
@@ -189809,18 +190600,34 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
189809
190600
  children: tr$1("说话人 {v0}", { v0: member.token })
189810
190601
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
189811
190602
  style: styles$20.meta,
189812
- children: tr$1("近 7 天出现 {v0} 天", { v0: member.dayCount })
190603
+ children: tr$1("出现 {v0} 天", { v0: member.dayCount })
189813
190604
  })]
189814
190605
  })]
189815
- }, `${member.token}:${index}`))
190606
+ }, member.identityKey))
189816
190607
  })
189817
190608
  ]
189818
190609
  });
189819
190610
  }
189820
- function ArkmeRecognizedSpeakersSurface({ accountKey, onBack, loadMarked = defaultLoadMarked, loadPresence = defaultLoadPresence, loadMarkedMembers = defaultLoadMarkedMembers, loadUnmarked = defaultLoadUnmarked }) {
190611
+ function ArkmeRecognizedSpeakersSurface({ accountKey, onBack, loadMarked = readSpeakerOptions, loadPresence = readSpeakerPresence, loadMarkedMembers = defaultLoadMarkedMembers, loadUnmarked = readSpeakerPage, directory: providedDirectory }) {
189821
190612
  useArkmeLocale();
190613
+ const directory = (0, react$1.useMemo)(() => providedDirectory ?? (loadMarked === readSpeakerOptions && loadUnmarked === readSpeakerPage && loadPresence === readSpeakerPresence ? recognizedSpeakerDirectory : new RecognizedSpeakerDirectory({
190614
+ marked: loadMarked,
190615
+ page: loadUnmarked,
190616
+ presence: loadPresence
190617
+ }, 0)), [
190618
+ providedDirectory,
190619
+ loadMarked,
190620
+ loadUnmarked,
190621
+ loadPresence
190622
+ ]);
189822
190623
  const [filter, setFilter] = (0, react$1.useState)("all");
189823
190624
  const [query, setQuery] = (0, react$1.useState)("");
190625
+ const [orderPreference, setOrderPreference] = (0, react$1.useState)(() => ({
190626
+ accountKey,
190627
+ value: readRecognizedSpeakerOrder(accountKey)
190628
+ }));
190629
+ const order = orderPreference.accountKey === accountKey ? orderPreference.value : readRecognizedSpeakerOrder(accountKey);
190630
+ const [visibleCount, setVisibleCount] = (0, react$1.useState)(100);
189824
190631
  const [refreshRevision, setRefreshRevision] = (0, react$1.useState)(0);
189825
190632
  const [marked, setMarked] = (0, react$1.useState)({
189826
190633
  loading: true,
@@ -189833,24 +190640,64 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
189833
190640
  });
189834
190641
  const [unmarked, setUnmarked] = (0, react$1.useState)({
189835
190642
  loading: true,
189836
- loadingMore: false,
190643
+ complete: false,
189837
190644
  items: [],
189838
- nextCursor: "",
189839
- hasMore: false,
190645
+ projectionState: void 0,
189840
190646
  error: ""
189841
190647
  });
189842
190648
  const [selectedCandidate, setSelectedCandidate] = (0, react$1.useState)();
189843
190649
  const [selectedMarked, setSelectedMarked] = (0, react$1.useState)();
189844
- const moreController = (0, react$1.useRef)();
189845
- const moreBusy = (0, react$1.useRef)(false);
190650
+ const lastAccountKey = (0, react$1.useRef)(accountKey);
190651
+ const readAccount = (0, react$1.useRef)();
189846
190652
  const refresh = (0, react$1.useCallback)(() => {
189847
- moreController.current?.abort();
189848
- moreBusy.current = false;
190653
+ directory.invalidate(accountKey);
189849
190654
  setSelectedMarked(void 0);
189850
190655
  setRefreshRevision((value) => value + 1);
189851
- }, []);
190656
+ }, [directory, accountKey]);
190657
+ (0, react$1.useEffect)(() => {
190658
+ setVisibleCount(100);
190659
+ }, [
190660
+ accountKey,
190661
+ filter,
190662
+ query,
190663
+ order
190664
+ ]);
189852
190665
  (0, react$1.useEffect)(() => {
189853
190666
  const controller = new AbortController();
190667
+ const retryTimers = [];
190668
+ const retry = (error, read) => {
190669
+ const delay = speakerRetryDelay(error);
190670
+ if (delay !== void 0) retryTimers.push(setTimeout(() => {
190671
+ if (!controller.signal.aborted) read();
190672
+ }, delay));
190673
+ };
190674
+ readAccount.current = void 0;
190675
+ if (lastAccountKey.current !== accountKey) {
190676
+ lastAccountKey.current = accountKey;
190677
+ setMarked({
190678
+ loading: true,
190679
+ items: [],
190680
+ error: ""
190681
+ });
190682
+ setPresence({
190683
+ loading: true,
190684
+ error: ""
190685
+ });
190686
+ setUnmarked({
190687
+ loading: true,
190688
+ complete: false,
190689
+ items: [],
190690
+ projectionState: void 0,
190691
+ error: ""
190692
+ });
190693
+ setOrderPreference({
190694
+ accountKey,
190695
+ value: readRecognizedSpeakerOrder(accountKey)
190696
+ });
190697
+ setQuery("");
190698
+ setSelectedMarked(void 0);
190699
+ setSelectedCandidate(void 0);
190700
+ }
189854
190701
  setMarked((previous) => ({
189855
190702
  ...previous,
189856
190703
  loading: true,
@@ -189864,73 +190711,103 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
189864
190711
  setUnmarked((previous) => ({
189865
190712
  ...previous,
189866
190713
  loading: true,
189867
- loadingMore: false,
190714
+ complete: false,
189868
190715
  error: ""
189869
190716
  }));
189870
- loadMarked(controller.signal).then((items) => {
189871
- if (!controller.signal.aborted) setMarked({
189872
- loading: false,
189873
- items,
189874
- error: ""
189875
- });
189876
- }).catch((error) => {
189877
- if (!controller.signal.aborted) setMarked((previous) => ({
189878
- ...previous,
189879
- loading: false,
189880
- error: failureMessage(error)
189881
- }));
190717
+ const cachedMarked = directory.peekMarked(accountKey);
190718
+ if (cachedMarked !== void 0) setMarked({
190719
+ loading: false,
190720
+ items: cachedMarked,
190721
+ error: ""
189882
190722
  });
189883
- loadPresence(controller.signal).then((result) => {
189884
- if (!controller.signal.aborted) setPresence({
189885
- loading: false,
189886
- result,
189887
- error: ""
190723
+ const readMarked = () => {
190724
+ directory.readMarked(accountKey, controller.signal).then((items) => {
190725
+ if (!controller.signal.aborted) setMarked({
190726
+ loading: false,
190727
+ items,
190728
+ error: ""
190729
+ });
190730
+ }).catch((error) => {
190731
+ if (!controller.signal.aborted) {
190732
+ setMarked((previous) => ({
190733
+ ...previous,
190734
+ loading: false,
190735
+ error: failureMessage(error)
190736
+ }));
190737
+ retry(error, readMarked);
190738
+ }
189888
190739
  });
189889
- }).catch((error) => {
189890
- if (!controller.signal.aborted) setPresence((previous) => ({
189891
- ...previous,
189892
- loading: false,
189893
- error: failureMessage(error)
189894
- }));
190740
+ };
190741
+ readMarked();
190742
+ const cachedPresence = directory.peekPresence(accountKey);
190743
+ if (cachedPresence !== void 0) setPresence({
190744
+ loading: false,
190745
+ result: cachedPresence,
190746
+ error: ""
189895
190747
  });
189896
- loadUnmarked("", controller.signal).then((page) => {
189897
- if (controller.signal.aborted) return;
189898
- if (page.section !== "unmarked-speakers") throw new Error("未标记说话人列表响应无效");
189899
- setUnmarked({
189900
- loading: false,
189901
- loadingMore: false,
189902
- items: page.items.filter((item) => item.kind === "unmarked-speaker"),
189903
- nextCursor: page.nextCursor ?? "",
189904
- hasMore: page.hasMore,
189905
- projectionState: page.projectionState,
189906
- error: ""
190748
+ const readPresence = () => {
190749
+ directory.readPresence(accountKey, controller.signal).then((result) => {
190750
+ if (!controller.signal.aborted) setPresence({
190751
+ loading: false,
190752
+ result,
190753
+ error: ""
190754
+ });
190755
+ }).catch((error) => {
190756
+ if (!controller.signal.aborted) {
190757
+ setPresence((previous) => ({
190758
+ ...previous,
190759
+ loading: false,
190760
+ error: failureMessage(error)
190761
+ }));
190762
+ retry(error, readPresence);
190763
+ }
189907
190764
  });
189908
- }).catch((error) => {
189909
- if (!controller.signal.aborted) setUnmarked((previous) => ({
189910
- ...previous,
189911
- loading: false,
189912
- loadingMore: false,
189913
- error: failureMessage(error)
189914
- }));
189915
- });
190765
+ };
190766
+ readPresence();
190767
+ const readCandidates = () => {
190768
+ directory.readCandidates(accountKey, controller.signal, (snapshot) => {
190769
+ if (!controller.signal.aborted) {
190770
+ readAccount.current = accountKey;
190771
+ setUnmarked({
190772
+ ...snapshot,
190773
+ loading: true,
190774
+ error: ""
190775
+ });
190776
+ }
190777
+ }).then((snapshot) => {
190778
+ if (!controller.signal.aborted) setUnmarked({
190779
+ ...snapshot,
190780
+ loading: false,
190781
+ error: ""
190782
+ });
190783
+ }).catch((error) => {
190784
+ if (!controller.signal.aborted) {
190785
+ setUnmarked((previous) => ({
190786
+ ...previous,
190787
+ loading: false,
190788
+ complete: false,
190789
+ error: failureMessage(error)
190790
+ }));
190791
+ retry(error, readCandidates);
190792
+ }
190793
+ });
190794
+ };
190795
+ readCandidates();
189916
190796
  return () => {
189917
190797
  controller.abort();
189918
- moreController.current?.abort();
189919
- moreBusy.current = false;
190798
+ retryTimers.forEach(clearTimeout);
189920
190799
  };
189921
190800
  }, [
189922
190801
  accountKey,
189923
190802
  refreshRevision,
189924
- loadMarked,
189925
- loadPresence,
189926
- loadUnmarked
190803
+ directory
189927
190804
  ]);
189928
190805
  (0, react$1.useEffect)(() => {
189929
190806
  const state = presence.result?.state;
189930
- if (presence.error !== "" || state !== "building" && state !== "stale") return;
190807
+ if (presence.loading || presence.error !== "" || state !== "building" && state !== "stale" && state !== "failed") return;
189931
190808
  const controller = new AbortController();
189932
190809
  const timer = setTimeout(() => {
189933
- loadPresence(controller.signal).then((result) => {
190810
+ directory.refreshPresence(accountKey, controller.signal).then((result) => {
189934
190811
  if (!controller.signal.aborted) setPresence({
189935
190812
  loading: false,
189936
190813
  result,
@@ -189950,56 +190827,53 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
189950
190827
  };
189951
190828
  }, [
189952
190829
  accountKey,
189953
- loadPresence,
190830
+ refreshRevision,
190831
+ directory,
190832
+ presence.loading,
189954
190833
  presence.error,
189955
190834
  presence.result
189956
190835
  ]);
189957
- const loadMore = (0, react$1.useCallback)(() => {
189958
- if (moreBusy.current || unmarked.loading || !unmarked.hasMore || unmarked.nextCursor === "") return;
189959
- moreBusy.current = true;
189960
- const controller = new AbortController();
189961
- moreController.current = controller;
189962
- setUnmarked((previous) => ({
189963
- ...previous,
189964
- loadingMore: true,
190836
+ const refreshPresenceVersion = (0, react$1.useCallback)(() => {
190837
+ setPresence({
190838
+ loading: false,
190839
+ result: {
190840
+ state: "stale",
190841
+ scope: "all-history",
190842
+ items: [],
190843
+ retryAfterMs: 1e3
190844
+ },
189965
190845
  error: ""
189966
- }));
189967
- loadUnmarked(unmarked.nextCursor, controller.signal).then((page) => {
189968
- if (controller.signal.aborted) return;
189969
- if (page.section !== "unmarked-speakers") throw new Error("未标记说话人列表响应无效");
189970
- if (page.cursorStale === true) {
189971
- refresh();
189972
- return;
189973
- }
189974
- setUnmarked((previous) => ({
189975
- ...previous,
189976
- loadingMore: false,
189977
- items: [...new Map([...previous.items, ...page.items.filter((item) => item.kind === "unmarked-speaker")].map((item) => [item.candidateRef, item])).values()],
189978
- nextCursor: page.nextCursor ?? "",
189979
- hasMore: page.hasMore,
189980
- projectionState: page.projectionState ?? previous.projectionState,
189981
- error: ""
189982
- }));
189983
- }).catch((error) => {
189984
- if (!controller.signal.aborted) setUnmarked((previous) => ({
189985
- ...previous,
189986
- loadingMore: false,
189987
- error: failureMessage(error)
189988
- }));
189989
- }).finally(() => {
189990
- moreBusy.current = false;
189991
190846
  });
190847
+ }, []);
190848
+ const rows = (0, react$1.useMemo)(() => identifiedSpeakerRows(marked.items, unmarked.items), [marked.items, unmarked.items]);
190849
+ const hasSelf = rows.some((row) => row.kind === "marked" && row.isCurrentUser);
190850
+ (0, react$1.useEffect)(() => {
190851
+ if (readAccount.current !== accountKey || marked.loading || marked.error !== "") return;
190852
+ if (rows.length > 0 || !unmarked.loading && unmarked.complete) recognizedSpeakerTracker.acknowledge(accountKey);
190853
+ if (!unmarked.loading && unmarked.complete && unmarked.error === "") recognizedSpeakerTracker.observe(accountKey, marked.items, unmarked.items, true);
189992
190854
  }, [
189993
- loadUnmarked,
189994
- refresh,
189995
- unmarked.hasMore,
190855
+ accountKey,
190856
+ marked.loading,
190857
+ marked.error,
190858
+ marked.items,
189996
190859
  unmarked.loading,
189997
- unmarked.nextCursor
190860
+ unmarked.complete,
190861
+ unmarked.error,
190862
+ unmarked.items,
190863
+ rows.length
189998
190864
  ]);
189999
- const rows = (0, react$1.useMemo)(() => identifiedSpeakerRows(marked.items, unmarked.items), [marked.items, unmarked.items]);
190000
- const presenceByOption = (0, react$1.useMemo)(() => new Map(presence.result?.items.map((item) => [item.optionKey, item]) ?? []), [presence.result]);
190865
+ 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]);
190001
190866
  const normalizedQuery = query.trim().toLocaleLowerCase();
190002
- const visible = rows.filter((row) => (filter === "all" || row.kind === filter) && (normalizedQuery === "" || row.name.toLocaleLowerCase().includes(normalizedQuery) || row.kind === "unmarked" && row.subtitle.toLocaleLowerCase().includes(normalizedQuery)));
190867
+ const orderValue = (row) => {
190868
+ const stat = row.kind === "marked" ? presenceByOption.get(row.optionKey) : unmarked.projectionState === void 0 || unmarked.projectionState === "fresh" ? row : void 0;
190869
+ return {
190870
+ key: `${row.kind}:${row.key}`,
190871
+ name: row.name,
190872
+ dayCount: stat?.dayCount,
190873
+ lastSeenAt: stat?.lastSeenAt
190874
+ };
190875
+ };
190876
+ 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));
190003
190877
  const loading = marked.loading || unmarked.loading;
190004
190878
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
190005
190879
  style: styles$20.root,
@@ -190047,33 +190921,57 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
190047
190921
  }),
190048
190922
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
190049
190923
  style: styles$20.filters,
190050
- children: [[
190051
- ["all", "全部"],
190052
- ["marked", "已标记"],
190053
- ["unmarked", "未标记"]
190054
- ].map(([kind, label]) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
190055
- type: "button",
190056
- "aria-pressed": filter === kind,
190057
- style: {
190058
- ...styles$20.filter,
190059
- ...filter === kind ? styles$20.selectedFilter : {}
190060
- },
190061
- onClick: () => {
190062
- setFilter(kind);
190063
- setSelectedCandidate(void 0);
190064
- setSelectedMarked(void 0);
190065
- },
190066
- children: tr$1(label)
190067
- }, kind)), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
190068
- "aria-label": tr$1("搜索说话人"),
190069
- placeholder: tr$1("搜索已加载的说话人"),
190070
- value: query,
190071
- onChange: (event) => {
190072
- setQuery(event.target.value);
190073
- },
190074
- style: styles$20.search
190075
- })]
190924
+ children: [
190925
+ [
190926
+ ["all", "全部"],
190927
+ ["marked", "已标记"],
190928
+ ["unmarked", "未标记"]
190929
+ ].map(([kind, label]) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
190930
+ type: "button",
190931
+ "aria-pressed": filter === kind,
190932
+ style: {
190933
+ ...styles$20.filter,
190934
+ ...filter === kind ? styles$20.selectedFilter : {}
190935
+ },
190936
+ onClick: () => {
190937
+ setFilter(kind);
190938
+ setSelectedCandidate(void 0);
190939
+ setSelectedMarked(void 0);
190940
+ },
190941
+ children: tr$1(label)
190942
+ }, kind)),
190943
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
190944
+ "aria-label": tr$1("搜索说话人"),
190945
+ placeholder: tr$1("搜索说话人"),
190946
+ value: query,
190947
+ onChange: (event) => {
190948
+ setQuery(event.target.value);
190949
+ },
190950
+ style: styles$20.search
190951
+ }),
190952
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
190953
+ "aria-label": tr$1("说话人排序"),
190954
+ value: order,
190955
+ onChange: (event) => {
190956
+ const next = event.target.value === "recent" ? "recent" : "frequent";
190957
+ setOrderPreference({
190958
+ accountKey,
190959
+ value: next
190960
+ });
190961
+ writeRecognizedSpeakerOrder(accountKey, next);
190962
+ },
190963
+ style: styles$20.sort,
190964
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
190965
+ value: "frequent",
190966
+ children: tr$1("经常出现")
190967
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
190968
+ value: "recent",
190969
+ children: tr$1("最近出现")
190970
+ })]
190971
+ })
190972
+ ]
190076
190973
  }),
190974
+ !marked.loading && marked.error === "" && !hasSelf && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SpeakerSelfGuide, { onOpenRecordings: onBack }, accountKey),
190077
190975
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
190078
190976
  className: "arkme-recognized-speakers-grid",
190079
190977
  "data-detail-open": selectedCandidate === void 0 && selectedMarked === void 0 ? "false" : "true",
@@ -190081,9 +190979,15 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
190081
190979
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
190082
190980
  className: "arkme-recognized-speakers-list",
190083
190981
  children: [
190084
- presence.result?.scope === "recent-seven-days" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
190982
+ filter !== "marked" && unmarked.loading && !unmarked.complete && rows.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
190983
+ role: "status",
190984
+ style: styles$20.note,
190985
+ children: tr$1("正在补齐说话人列表,排序仍在更新…")
190986
+ }),
190987
+ filter !== "marked" && !unmarked.loading && !unmarked.complete && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
190988
+ role: "status",
190085
190989
  style: styles$20.note,
190086
- children: tr$1("已标记项仅统计近 7 天已转写发声;全历史统计待接口。")
190990
+ children: tr$1("列表尚未完整,当前仅对已加载的说话人排序和搜索。")
190087
190991
  }),
190088
190992
  marked.error !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
190089
190993
  role: "alert",
@@ -190128,7 +191032,7 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
190128
191032
  children: loading ? tr$1("正在更新说话人…") : rows.length === 0 && marked.error === "" && unmarked.error === "" && (unmarked.projectionState === void 0 || unmarked.projectionState === "fresh") ? tr$1("暂无已识别说话人") : tr$1("暂无可显示的说话人")
190129
191033
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
190130
191034
  style: styles$20.list,
190131
- children: visible.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: row.kind === "unmarked" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
191035
+ 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", {
190132
191036
  type: "button",
190133
191037
  className: "arkme-recognized-speakers-row",
190134
191038
  "aria-current": selectedCandidate === row.candidateRef ? "true" : void 0,
@@ -190196,22 +191100,16 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
190196
191100
  ]
190197
191101
  }) }, `${row.kind}:${row.key}`))
190198
191102
  }),
190199
- unmarked.hasMore && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
191103
+ visible.length > visibleCount && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
190200
191104
  type: "button",
190201
191105
  style: {
190202
191106
  ...styles$20.button,
190203
191107
  marginTop: 14
190204
191108
  },
190205
- disabled: unmarked.loading || unmarked.loadingMore || unmarked.nextCursor === "",
190206
- onClick: loadMore,
190207
- children: unmarked.loadingMore ? tr$1("正在加载…") : tr$1("加载更多未标记说话人")
190208
- }),
190209
- unmarked.hasMore && query.trim() !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
190210
- style: {
190211
- ...styles$20.meta,
190212
- marginTop: 10
191109
+ onClick: () => {
191110
+ setVisibleCount((count) => count + 100);
190213
191111
  },
190214
- children: tr$1("搜索仅覆盖已加载的说话人,可继续加载更多。")
191112
+ children: tr$1("显示更多说话人")
190215
191113
  })
190216
191114
  ]
190217
191115
  }),
@@ -190247,7 +191145,9 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
190247
191145
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MarkedSpeakerDetail, {
190248
191146
  accountKey,
190249
191147
  speaker: selectedMarked,
190250
- loadMembers: loadMarkedMembers
191148
+ loadMembers: loadMarkedMembers,
191149
+ version: presence.result?.version,
191150
+ onVersionChanged: refreshPresenceVersion
190251
191151
  }, `${accountKey}:${selectedMarked.speakerRef}`)]
190252
191152
  })
190253
191153
  ]
@@ -210658,7 +211558,7 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
210658
211558
  const c$3 = e$20;
210659
211559
  //#endregion
210660
211560
  //#region package.json
210661
- var version = "0.1.83";
211561
+ var version = "0.1.84";
210662
211562
  //#endregion
210663
211563
  //#region src/client/arkme-membership.ts
210664
211564
  function membershipLabel(state) {
@@ -212051,6 +212951,9 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
212051
212951
  }
212052
212952
  //#endregion
212053
212953
  //#region src/client/ArkmeAccountUsage.tsx
212954
+ function formatWholePoints(value) {
212955
+ return (pointsUnits(value) / 10000000n).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
212956
+ }
212054
212957
  function useUsage(operation, scope, revision) {
212055
212958
  const [result, setResult] = (0, react$1.useState)();
212056
212959
  (0, react$1.useEffect)(() => {
@@ -212200,7 +213103,7 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
212200
213103
  children: tr$1("AI 额度")
212201
213104
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
212202
213105
  className: "arkme-usage-summary-total",
212203
- children: points.status === "ready" ? `${formatAiPoints(points.value.availablePoints)} ${tr$1("积分")}` : status(points)
213106
+ children: points.status === "ready" ? `${formatWholePoints(points.value.availablePoints)} ${tr$1("积分")}` : status(points)
212204
213107
  })]
212205
213108
  }),
212206
213109
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageSummaryRow, {
@@ -212295,14 +213198,14 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
212295
213198
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
212296
213199
  tr$1("可用"),
212297
213200
  " ",
212298
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: formatAiPoints(points.value.availablePoints) }),
213201
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: formatWholePoints(points.value.availablePoints) }),
212299
213202
  " ",
212300
213203
  tr$1("积分")
212301
213204
  ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
212302
213205
  className: "arkme-usage-points-sources",
212303
213206
  children: tr$1("赠送 {v0} · 充值 {v1}", {
212304
- v0: formatAiPoints(points.value.grantedPoints),
212305
- v1: formatAiPoints(points.value.purchasedPoints)
213207
+ v0: formatWholePoints(points.value.grantedPoints),
213208
+ v1: formatWholePoints(points.value.purchasedPoints)
212306
213209
  })
212307
213210
  })]
212308
213211
  }), points.value.grants.some((grant) => grant.expiresAt > 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("small", { children: [
@@ -234358,6 +235261,42 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
234358
235261
  } : detailRow;
234359
235262
  const detailTopicPresentation = detailItem === void 0 ? void 0 : arkmeTimelineSelfTopicPresentation(detailItem, source, selfSources);
234360
235263
  const detailTopicPath = detailItem === void 0 ? void 0 : detailTopicPresentation?.displayLabel ?? detailItem.selfTopic?.title ?? (source?.kind === "topic" ? source.displayName : void 0);
235264
+ const detailConversationBadge = source !== void 0 && (source.kind === "private_chat" || source.kind === "group_chat") ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
235265
+ "data-arkme-feedback": "neutral",
235266
+ "data-arkme-detail-conversation-source": true,
235267
+ type: "button",
235268
+ style: {
235269
+ ...arkmeDetailSourceBadgeStyle,
235270
+ cursor: "pointer"
235271
+ },
235272
+ "aria-label": tr$1("来源:{v0}", { v0: source.displayName }),
235273
+ onClick: (event) => {
235274
+ event.stopPropagation();
235275
+ setDrawer(void 0);
235276
+ setDetailBackStack([]);
235277
+ pendingExtensionParentDetailRef.current = void 0;
235278
+ },
235279
+ children: [
235280
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ArkmeDirectorySourceAvatar, {
235281
+ source,
235282
+ size: 16
235283
+ }),
235284
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
235285
+ style: {
235286
+ minWidth: 0,
235287
+ overflow: "hidden",
235288
+ textOverflow: "ellipsis",
235289
+ whiteSpace: "nowrap"
235290
+ },
235291
+ children: source.displayName
235292
+ }),
235293
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(s$21, {
235294
+ size: 12,
235295
+ "aria-hidden": true,
235296
+ style: { flex: "none" }
235297
+ })
235298
+ ]
235299
+ }) : void 0;
234361
235300
  const openExtensionParentDetail = (parent) => {
234362
235301
  if (source === void 0 || detailItem === void 0 || parent.itemUid === detailItem.itemUid) return;
234363
235302
  const from = {
@@ -239291,6 +240230,7 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
239291
240230
  }),
239292
240231
  detailItem.forwardRecords !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ForwardRecordsDetail, {
239293
240232
  item: detailItem,
240233
+ sourceBadge: detailConversationBadge,
239294
240234
  onPrivateChatOpened: activateSource,
239295
240235
  onClose: () => {
239296
240236
  setDrawer(void 0);
@@ -239314,6 +240254,7 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
239314
240254
  }, `${authenticatedAccountKey}:${conversationOverlayKey}:${detailItem.itemUid}`),
239315
240255
  activeConversation && drawer === "detail" && detailItem !== void 0 && detailItem.callRecord === void 0 && detailItem.forwardRecords === void 0 && detailSharedRecording === void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ArkmeTimelineDetailDrawer, {
239316
240256
  item: detailItem,
240257
+ sourceBadge: detailConversationBadge,
239317
240258
  sourceRef: source?.sourceRef,
239318
240259
  sourceIdentityKey: conversationKey,
239319
240260
  canExtend: !archiveReadOnly,