@vivix-ai/ivi-frontend-sdk 0.3.6 → 0.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -628,6 +628,12 @@ interface IVIVolumeControlProps {
628
628
 
629
629
  type IviSubtitleRole = IviRuntimeConversationItem["role"];
630
630
  type IviSubtitleSource = "conversation" | "response_audio_transcript";
631
+ type IviSubtitleCompletedReason = "conversation_done" | "response_done";
632
+ type IviCompletedSubtitleDecision = "keep" | "remove" | {
633
+ removeAfterMs: number;
634
+ };
635
+ type IviCompletedSubtitleDecisionResult = IviCompletedSubtitleDecision | void | Promise<IviCompletedSubtitleDecision | void>;
636
+ declare const DEFAULT_HIDE_COMPLETED_SUBTITLE_AFTER_MS = 3000;
631
637
  interface IviSubtitleItem {
632
638
  id: string;
633
639
  role: IviSubtitleRole;
@@ -649,6 +655,15 @@ interface IviSubtitleItem {
649
655
  /** 最近一次字幕内容或状态更新的时间戳。 */
650
656
  updatedAt: number;
651
657
  }
658
+ interface IviSubtitleCompletedContext {
659
+ /** 当前字幕队列快照。 */
660
+ entries: readonly IviSubtitleItem[];
661
+ /** 本次完成态来自 conversation 完成还是 response 完成。 */
662
+ reason: IviSubtitleCompletedReason;
663
+ /** 当字幕被移除、会话结束、配置变化或组件卸载时触发。 */
664
+ signal: AbortSignal;
665
+ }
666
+ type IviSubtitleCompletedHandler = (item: IviSubtitleItem, context: IviSubtitleCompletedContext) => IviCompletedSubtitleDecisionResult;
652
667
  interface IviUseSubtitlesOptions {
653
668
  /** 要收集的发言人角色,默认 ["user"]。传单个字符串或数组均可。 */
654
669
  roles?: IviSubtitleRole | IviSubtitleRole[];
@@ -660,6 +675,16 @@ interface IviUseSubtitlesOptions {
660
675
  * 只有 response.done 才会把这一轮 model 字幕标记为 done。
661
676
  */
662
677
  useModelStreamingTranscript?: boolean;
678
+ /**
679
+ * 字幕进入完成态后多久自动移除,默认 3000ms;传 false 可恢复为一直保留直到被 maxItems 裁剪。
680
+ * 自定义 onSubtitleCompleted 时,此配置仅作为未提供自定义策略时的默认策略。
681
+ */
682
+ hideCompletedAfterMs?: number | false;
683
+ /**
684
+ * 字幕进入完成态时调用一次。返回 "remove" 立即删除,返回 { removeAfterMs } 延迟删除,
685
+ * 返回 "keep" 或 undefined 则保留。异步策略可通过 context.signal 响应取消。
686
+ */
687
+ onSubtitleCompleted?: IviSubtitleCompletedHandler;
663
688
  }
664
689
  /**
665
690
  * 监听 runtime 中的 conversation/response 事件,并维护当前应展示的字幕队列。
@@ -689,6 +714,10 @@ interface IVISubtitleOverlayProps {
689
714
  * 当 roles 包含 "model" 时,是否使用 response.output_audio_transcript.* 事件作为 model 字幕来源。
690
715
  */
691
716
  useModelStreamingTranscript?: boolean;
717
+ /** 字幕进入完成态后多久自动移除,默认 3000ms;传 false 可关闭自动移除。 */
718
+ hideCompletedAfterMs?: IviUseSubtitlesOptions["hideCompletedAfterMs"];
719
+ /** 字幕进入完成态时的自定义保留/删除策略,优先于 hideCompletedAfterMs。 */
720
+ onSubtitleCompleted?: IviUseSubtitlesOptions["onSubtitleCompleted"];
692
721
  /** 样式配置 */
693
722
  subtitleStyle?: IVISubtitleOverlayStyle;
694
723
  /** 自定义类名 */
@@ -773,4 +802,4 @@ declare function useManagedIviRuntime(config: IviManagedRuntimeConfig): IviRunti
773
802
 
774
803
  declare function useIviStageView(): IviStageViewContextValue;
775
804
 
776
- export { EMPTY_RUNTIME_STATE, IVILivekitPlayer, type IVILivekitPlayerProps, IVIStageView, type IVIStageViewProps, IVISubtitleOverlay, type IVISubtitleOverlayProps, type IVISubtitleOverlayStyle, IVITrackSlot, type IVITrackSlotProps, IVITrtcPlayer, type IVITrtcPlayerProps, type IviFrontendClientConfig, IviFrontendSdk, type IviManagedRuntimeConfig, type IviManagedRuntimeLogCallback, type IviManagedRuntimeLogEntry, type IviManagedRuntimeLogLevel, type IviManagedRuntimeLogSource, type IviRuntimeConversationItem, type IviRuntimeConversationLifecycle, type IviRuntimeConversationStatus, IviRuntimeCoordinator, type IviRuntimeCoordinatorConfig, IviRuntimeDispatcher, type IviRuntimeDispatcherConfig, type IviRuntimeEventListener, type IviRuntimeLogCallback, type IviRuntimeLogEntry, type IviRuntimeLogLevel, type IviRuntimeSource, type IviRuntimeSourcePreloadState, type IviRuntimeState, type IviRuntimeStatus, type IviRuntimeStream, type IviRuntimeTrtcAIDenoiserMode, type IviRuntimeTrtcAIDenoiserOptions, type IviRuntimeTrtcEvent, type IviRuntimeTrtcEventListener, type IviRuntimeTrtcEventType, type IviRuntimeUserTextToResponseCallbacks, type IviRuntimeUserTextToResponseOptions, type IviRuntimeUserTextToResponseResult, type IviSourcePlaybackLivekit, type IviSourcePlaybackLivekitDescriptor, type IviStageSlotBinding, type IviStageViewContextValue, type IviSubtitleItem, type IviSubtitleRole, type IviSubtitleSource, type IviUseSubtitlesOptions, LivekitSourceManager, TrtcSourceManager, isLivekitSourcePlayback, isReadyLivekitRuntimeSource, isSameLivekitConfig, useIviStageView, useIviSubtitles, useManagedIviRuntime, useRuntimeState };
805
+ export { DEFAULT_HIDE_COMPLETED_SUBTITLE_AFTER_MS, EMPTY_RUNTIME_STATE, IVILivekitPlayer, type IVILivekitPlayerProps, IVIStageView, type IVIStageViewProps, IVISubtitleOverlay, type IVISubtitleOverlayProps, type IVISubtitleOverlayStyle, IVITrackSlot, type IVITrackSlotProps, IVITrtcPlayer, type IVITrtcPlayerProps, type IviCompletedSubtitleDecision, type IviCompletedSubtitleDecisionResult, type IviFrontendClientConfig, IviFrontendSdk, type IviManagedRuntimeConfig, type IviManagedRuntimeLogCallback, type IviManagedRuntimeLogEntry, type IviManagedRuntimeLogLevel, type IviManagedRuntimeLogSource, type IviRuntimeConversationItem, type IviRuntimeConversationLifecycle, type IviRuntimeConversationStatus, IviRuntimeCoordinator, type IviRuntimeCoordinatorConfig, IviRuntimeDispatcher, type IviRuntimeDispatcherConfig, type IviRuntimeEventListener, type IviRuntimeLogCallback, type IviRuntimeLogEntry, type IviRuntimeLogLevel, type IviRuntimeSource, type IviRuntimeSourcePreloadState, type IviRuntimeState, type IviRuntimeStatus, type IviRuntimeStream, type IviRuntimeTrtcAIDenoiserMode, type IviRuntimeTrtcAIDenoiserOptions, type IviRuntimeTrtcEvent, type IviRuntimeTrtcEventListener, type IviRuntimeTrtcEventType, type IviRuntimeUserTextToResponseCallbacks, type IviRuntimeUserTextToResponseOptions, type IviRuntimeUserTextToResponseResult, type IviSourcePlaybackLivekit, type IviSourcePlaybackLivekitDescriptor, type IviStageSlotBinding, type IviStageViewContextValue, type IviSubtitleCompletedContext, type IviSubtitleCompletedHandler, type IviSubtitleCompletedReason, type IviSubtitleItem, type IviSubtitleRole, type IviSubtitleSource, type IviUseSubtitlesOptions, LivekitSourceManager, TrtcSourceManager, isLivekitSourcePlayback, isReadyLivekitRuntimeSource, isSameLivekitConfig, useIviStageView, useIviSubtitles, useManagedIviRuntime, useRuntimeState };
package/dist/index.d.ts CHANGED
@@ -628,6 +628,12 @@ interface IVIVolumeControlProps {
628
628
 
629
629
  type IviSubtitleRole = IviRuntimeConversationItem["role"];
630
630
  type IviSubtitleSource = "conversation" | "response_audio_transcript";
631
+ type IviSubtitleCompletedReason = "conversation_done" | "response_done";
632
+ type IviCompletedSubtitleDecision = "keep" | "remove" | {
633
+ removeAfterMs: number;
634
+ };
635
+ type IviCompletedSubtitleDecisionResult = IviCompletedSubtitleDecision | void | Promise<IviCompletedSubtitleDecision | void>;
636
+ declare const DEFAULT_HIDE_COMPLETED_SUBTITLE_AFTER_MS = 3000;
631
637
  interface IviSubtitleItem {
632
638
  id: string;
633
639
  role: IviSubtitleRole;
@@ -649,6 +655,15 @@ interface IviSubtitleItem {
649
655
  /** 最近一次字幕内容或状态更新的时间戳。 */
650
656
  updatedAt: number;
651
657
  }
658
+ interface IviSubtitleCompletedContext {
659
+ /** 当前字幕队列快照。 */
660
+ entries: readonly IviSubtitleItem[];
661
+ /** 本次完成态来自 conversation 完成还是 response 完成。 */
662
+ reason: IviSubtitleCompletedReason;
663
+ /** 当字幕被移除、会话结束、配置变化或组件卸载时触发。 */
664
+ signal: AbortSignal;
665
+ }
666
+ type IviSubtitleCompletedHandler = (item: IviSubtitleItem, context: IviSubtitleCompletedContext) => IviCompletedSubtitleDecisionResult;
652
667
  interface IviUseSubtitlesOptions {
653
668
  /** 要收集的发言人角色,默认 ["user"]。传单个字符串或数组均可。 */
654
669
  roles?: IviSubtitleRole | IviSubtitleRole[];
@@ -660,6 +675,16 @@ interface IviUseSubtitlesOptions {
660
675
  * 只有 response.done 才会把这一轮 model 字幕标记为 done。
661
676
  */
662
677
  useModelStreamingTranscript?: boolean;
678
+ /**
679
+ * 字幕进入完成态后多久自动移除,默认 3000ms;传 false 可恢复为一直保留直到被 maxItems 裁剪。
680
+ * 自定义 onSubtitleCompleted 时,此配置仅作为未提供自定义策略时的默认策略。
681
+ */
682
+ hideCompletedAfterMs?: number | false;
683
+ /**
684
+ * 字幕进入完成态时调用一次。返回 "remove" 立即删除,返回 { removeAfterMs } 延迟删除,
685
+ * 返回 "keep" 或 undefined 则保留。异步策略可通过 context.signal 响应取消。
686
+ */
687
+ onSubtitleCompleted?: IviSubtitleCompletedHandler;
663
688
  }
664
689
  /**
665
690
  * 监听 runtime 中的 conversation/response 事件,并维护当前应展示的字幕队列。
@@ -689,6 +714,10 @@ interface IVISubtitleOverlayProps {
689
714
  * 当 roles 包含 "model" 时,是否使用 response.output_audio_transcript.* 事件作为 model 字幕来源。
690
715
  */
691
716
  useModelStreamingTranscript?: boolean;
717
+ /** 字幕进入完成态后多久自动移除,默认 3000ms;传 false 可关闭自动移除。 */
718
+ hideCompletedAfterMs?: IviUseSubtitlesOptions["hideCompletedAfterMs"];
719
+ /** 字幕进入完成态时的自定义保留/删除策略,优先于 hideCompletedAfterMs。 */
720
+ onSubtitleCompleted?: IviUseSubtitlesOptions["onSubtitleCompleted"];
692
721
  /** 样式配置 */
693
722
  subtitleStyle?: IVISubtitleOverlayStyle;
694
723
  /** 自定义类名 */
@@ -773,4 +802,4 @@ declare function useManagedIviRuntime(config: IviManagedRuntimeConfig): IviRunti
773
802
 
774
803
  declare function useIviStageView(): IviStageViewContextValue;
775
804
 
776
- export { EMPTY_RUNTIME_STATE, IVILivekitPlayer, type IVILivekitPlayerProps, IVIStageView, type IVIStageViewProps, IVISubtitleOverlay, type IVISubtitleOverlayProps, type IVISubtitleOverlayStyle, IVITrackSlot, type IVITrackSlotProps, IVITrtcPlayer, type IVITrtcPlayerProps, type IviFrontendClientConfig, IviFrontendSdk, type IviManagedRuntimeConfig, type IviManagedRuntimeLogCallback, type IviManagedRuntimeLogEntry, type IviManagedRuntimeLogLevel, type IviManagedRuntimeLogSource, type IviRuntimeConversationItem, type IviRuntimeConversationLifecycle, type IviRuntimeConversationStatus, IviRuntimeCoordinator, type IviRuntimeCoordinatorConfig, IviRuntimeDispatcher, type IviRuntimeDispatcherConfig, type IviRuntimeEventListener, type IviRuntimeLogCallback, type IviRuntimeLogEntry, type IviRuntimeLogLevel, type IviRuntimeSource, type IviRuntimeSourcePreloadState, type IviRuntimeState, type IviRuntimeStatus, type IviRuntimeStream, type IviRuntimeTrtcAIDenoiserMode, type IviRuntimeTrtcAIDenoiserOptions, type IviRuntimeTrtcEvent, type IviRuntimeTrtcEventListener, type IviRuntimeTrtcEventType, type IviRuntimeUserTextToResponseCallbacks, type IviRuntimeUserTextToResponseOptions, type IviRuntimeUserTextToResponseResult, type IviSourcePlaybackLivekit, type IviSourcePlaybackLivekitDescriptor, type IviStageSlotBinding, type IviStageViewContextValue, type IviSubtitleItem, type IviSubtitleRole, type IviSubtitleSource, type IviUseSubtitlesOptions, LivekitSourceManager, TrtcSourceManager, isLivekitSourcePlayback, isReadyLivekitRuntimeSource, isSameLivekitConfig, useIviStageView, useIviSubtitles, useManagedIviRuntime, useRuntimeState };
805
+ export { DEFAULT_HIDE_COMPLETED_SUBTITLE_AFTER_MS, EMPTY_RUNTIME_STATE, IVILivekitPlayer, type IVILivekitPlayerProps, IVIStageView, type IVIStageViewProps, IVISubtitleOverlay, type IVISubtitleOverlayProps, type IVISubtitleOverlayStyle, IVITrackSlot, type IVITrackSlotProps, IVITrtcPlayer, type IVITrtcPlayerProps, type IviCompletedSubtitleDecision, type IviCompletedSubtitleDecisionResult, type IviFrontendClientConfig, IviFrontendSdk, type IviManagedRuntimeConfig, type IviManagedRuntimeLogCallback, type IviManagedRuntimeLogEntry, type IviManagedRuntimeLogLevel, type IviManagedRuntimeLogSource, type IviRuntimeConversationItem, type IviRuntimeConversationLifecycle, type IviRuntimeConversationStatus, IviRuntimeCoordinator, type IviRuntimeCoordinatorConfig, IviRuntimeDispatcher, type IviRuntimeDispatcherConfig, type IviRuntimeEventListener, type IviRuntimeLogCallback, type IviRuntimeLogEntry, type IviRuntimeLogLevel, type IviRuntimeSource, type IviRuntimeSourcePreloadState, type IviRuntimeState, type IviRuntimeStatus, type IviRuntimeStream, type IviRuntimeTrtcAIDenoiserMode, type IviRuntimeTrtcAIDenoiserOptions, type IviRuntimeTrtcEvent, type IviRuntimeTrtcEventListener, type IviRuntimeTrtcEventType, type IviRuntimeUserTextToResponseCallbacks, type IviRuntimeUserTextToResponseOptions, type IviRuntimeUserTextToResponseResult, type IviSourcePlaybackLivekit, type IviSourcePlaybackLivekitDescriptor, type IviStageSlotBinding, type IviStageViewContextValue, type IviSubtitleCompletedContext, type IviSubtitleCompletedHandler, type IviSubtitleCompletedReason, type IviSubtitleItem, type IviSubtitleRole, type IviSubtitleSource, type IviUseSubtitlesOptions, LivekitSourceManager, TrtcSourceManager, isLivekitSourcePlayback, isReadyLivekitRuntimeSource, isSameLivekitConfig, useIviStageView, useIviSubtitles, useManagedIviRuntime, useRuntimeState };
package/dist/index.js CHANGED
@@ -1105,6 +1105,8 @@ var ConversationManager = class {
1105
1105
 
1106
1106
  // src/runtime/managers/trtc-source-manager.ts
1107
1107
  var TAG = "[IVI-TRTC]";
1108
+ var TRTC_VIEW_ATTR = "data-ivi-trtc-view";
1109
+ var TRTC_MEDIA_STYLE_ID = "ivi-trtc-media-style";
1108
1110
  var DEFAULT_DENOISER_OPTIONS = {
1109
1111
  enabled: true,
1110
1112
  mode: "normal"
@@ -1266,6 +1268,8 @@ var TrtcSourceManager = class {
1266
1268
  if (!session) {
1267
1269
  throw new Error(`TRTC source session not found: ${sourceId}`);
1268
1270
  }
1271
+ ensureTrtcMediaStyleSheet();
1272
+ container.setAttribute(TRTC_VIEW_ATTR, "");
1269
1273
  session.views.set(viewId, {
1270
1274
  sourceId,
1271
1275
  container,
@@ -1288,6 +1292,7 @@ var TrtcSourceManager = class {
1288
1292
  }
1289
1293
  const binding = session.views.get(viewId);
1290
1294
  if (binding?.sourceId === sourceId) {
1295
+ binding.container.removeAttribute(TRTC_VIEW_ATTR);
1291
1296
  session.views.delete(viewId);
1292
1297
  }
1293
1298
  void this.applyAudioPolicy(session);
@@ -1309,6 +1314,7 @@ var TrtcSourceManager = class {
1309
1314
  this.sourceSessionKeys.delete(sourceId);
1310
1315
  session.views.forEach((binding, viewId) => {
1311
1316
  if (binding.sourceId === sourceId) {
1317
+ binding.container.removeAttribute(TRTC_VIEW_ATTR);
1312
1318
  session.views.delete(viewId);
1313
1319
  }
1314
1320
  });
@@ -1628,6 +1634,38 @@ var TrtcSourceManager = class {
1628
1634
  this.onTrtcEvent?.(event);
1629
1635
  }
1630
1636
  };
1637
+ function ensureTrtcMediaStyleSheet() {
1638
+ if (typeof document === "undefined") {
1639
+ return;
1640
+ }
1641
+ if (document.getElementById(TRTC_MEDIA_STYLE_ID)) {
1642
+ return;
1643
+ }
1644
+ const style = document.createElement("style");
1645
+ style.id = TRTC_MEDIA_STYLE_ID;
1646
+ style.textContent = `
1647
+ [${TRTC_VIEW_ATTR}] [id^="player_"],
1648
+ [${TRTC_VIEW_ATTR}] [id^="video_"],
1649
+ [${TRTC_VIEW_ATTR}] [id^="audio_"],
1650
+ [${TRTC_VIEW_ATTR}] video,
1651
+ [${TRTC_VIEW_ATTR}] canvas {
1652
+ width: 100% !important;
1653
+ height: 100% !important;
1654
+ max-width: 100% !important;
1655
+ max-height: 100% !important;
1656
+ background: transparent !important;
1657
+ background-color: transparent !important;
1658
+ background-image: none !important;
1659
+ }
1660
+
1661
+ [${TRTC_VIEW_ATTR}] video,
1662
+ [${TRTC_VIEW_ATTR}] canvas {
1663
+ object-fit: contain !important;
1664
+ display: block !important;
1665
+ }
1666
+ `;
1667
+ document.head.appendChild(style);
1668
+ }
1631
1669
  function isRuntimeTrtcSource(source) {
1632
1670
  return source.status === "ready" && source.playback?.type === "trtc" && typeof source.playback.trtc === "object" && source.playback.trtc !== null;
1633
1671
  }
@@ -1719,6 +1757,9 @@ function enforceContainMedia(container) {
1719
1757
  element.style.setProperty("max-height", "100%", "important");
1720
1758
  element.style.setProperty("object-fit", "contain", "important");
1721
1759
  element.style.setProperty("display", "block", "important");
1760
+ element.style.setProperty("background", "transparent", "important");
1761
+ element.style.setProperty("background-color", "transparent", "important");
1762
+ element.style.setProperty("background-image", "none", "important");
1722
1763
  });
1723
1764
  }
1724
1765
 
@@ -3078,9 +3119,12 @@ function useApplyVolumeToSlot(containerRef, volume, enabled, activeSourceId) {
3078
3119
  return () => observer.disconnect();
3079
3120
  }, [containerRef, volume, enabled, activeSourceId]);
3080
3121
  }
3122
+ var DEFAULT_HIDE_COMPLETED_SUBTITLE_AFTER_MS = 3e3;
3081
3123
  function useIviSubtitles(runtime, options = {}) {
3082
3124
  const roles = options.roles ?? "user";
3083
3125
  const maxItems = normalizeMaxItems(options.maxItems);
3126
+ const hideCompletedAfterMs = normalizeHideCompletedAfterMs(options.hideCompletedAfterMs);
3127
+ const onSubtitleCompleted = options.onSubtitleCompleted;
3084
3128
  const roleKey = Array.isArray(roles) ? roles.join("\0") : roles;
3085
3129
  const roleSet = useMemo(
3086
3130
  () => new Set(roleKey.split("\0")),
@@ -3090,9 +3134,14 @@ function useIviSubtitles(runtime, options = {}) {
3090
3134
  const [subtitles, setSubtitles] = useState([]);
3091
3135
  const seenIdsRef = useRef(/* @__PURE__ */ new Set());
3092
3136
  const initializedRef = useRef(false);
3137
+ const completedSubtitleIdsRef = useRef(/* @__PURE__ */ new Set());
3138
+ const completionControllersRef = useRef(/* @__PURE__ */ new Map());
3139
+ const completionPolicyRef = useRef(null);
3093
3140
  useEffect(() => {
3094
3141
  seenIdsRef.current = /* @__PURE__ */ new Set();
3095
3142
  initializedRef.current = false;
3143
+ completedSubtitleIdsRef.current = /* @__PURE__ */ new Set();
3144
+ abortCompletedSubtitleTasks(completionControllersRef.current);
3096
3145
  setSubtitles([]);
3097
3146
  if (!runtime) {
3098
3147
  return;
@@ -3148,6 +3197,8 @@ function useIviSubtitles(runtime, options = {}) {
3148
3197
  if (event.type === "session.ended") {
3149
3198
  seenIdsRef.current = /* @__PURE__ */ new Set();
3150
3199
  initializedRef.current = false;
3200
+ completedSubtitleIdsRef.current = /* @__PURE__ */ new Set();
3201
+ abortCompletedSubtitleTasks(completionControllersRef.current);
3151
3202
  setSubtitles([]);
3152
3203
  return;
3153
3204
  }
@@ -3165,6 +3216,78 @@ function useIviSubtitles(runtime, options = {}) {
3165
3216
  syncConversations(state.conversations);
3166
3217
  });
3167
3218
  }, [runtime, roleSet, maxItems, useModelStreamingTranscript]);
3219
+ useEffect(() => {
3220
+ return () => {
3221
+ completedSubtitleIdsRef.current = /* @__PURE__ */ new Set();
3222
+ abortCompletedSubtitleTasks(completionControllersRef.current);
3223
+ };
3224
+ }, []);
3225
+ useEffect(() => {
3226
+ const previousPolicy = completionPolicyRef.current;
3227
+ const policyChanged = Boolean(
3228
+ previousPolicy && (previousPolicy.hideCompletedAfterMs !== hideCompletedAfterMs || previousPolicy.onSubtitleCompleted !== onSubtitleCompleted)
3229
+ );
3230
+ completionPolicyRef.current = { hideCompletedAfterMs, onSubtitleCompleted };
3231
+ if (policyChanged) {
3232
+ completedSubtitleIdsRef.current = /* @__PURE__ */ new Set();
3233
+ abortCompletedSubtitleTasks(completionControllersRef.current);
3234
+ }
3235
+ const subtitleById = new Map(subtitles.map((item) => [item.id, item]));
3236
+ const completedIds = completedSubtitleIdsRef.current;
3237
+ const controllers = completionControllersRef.current;
3238
+ for (const [id, controller] of Array.from(controllers)) {
3239
+ const item = subtitleById.get(id);
3240
+ if (!item || !isCompletedSubtitle(item)) {
3241
+ controller.abort();
3242
+ controllers.delete(id);
3243
+ completedIds.delete(id);
3244
+ }
3245
+ }
3246
+ for (const id of Array.from(completedIds)) {
3247
+ const item = subtitleById.get(id);
3248
+ if (!item || !isCompletedSubtitle(item)) {
3249
+ completedIds.delete(id);
3250
+ }
3251
+ }
3252
+ for (const item of subtitles) {
3253
+ if (!isCompletedSubtitle(item) || completedIds.has(item.id)) {
3254
+ continue;
3255
+ }
3256
+ completedIds.add(item.id);
3257
+ const controller = new AbortController();
3258
+ controllers.set(item.id, controller);
3259
+ const context = {
3260
+ entries: subtitles,
3261
+ reason: getSubtitleCompletedReason(item),
3262
+ signal: controller.signal
3263
+ };
3264
+ let decisionResult;
3265
+ try {
3266
+ decisionResult = onSubtitleCompleted ? onSubtitleCompleted(item, context) : getDefaultCompletedSubtitleDecision(hideCompletedAfterMs);
3267
+ } catch {
3268
+ decisionResult = "keep";
3269
+ }
3270
+ void Promise.resolve(decisionResult).then((decision) => {
3271
+ applyCompletedSubtitleDecision(
3272
+ item.id,
3273
+ decision,
3274
+ controller,
3275
+ controllers,
3276
+ completedIds,
3277
+ (id) => setSubtitles((previous) => removeSubtitleById(previous, id))
3278
+ );
3279
+ }).catch(() => {
3280
+ applyCompletedSubtitleDecision(
3281
+ item.id,
3282
+ "keep",
3283
+ controller,
3284
+ controllers,
3285
+ completedIds,
3286
+ (id) => setSubtitles((previous) => removeSubtitleById(previous, id))
3287
+ );
3288
+ });
3289
+ }
3290
+ }, [subtitles, hideCompletedAfterMs, onSubtitleCompleted]);
3168
3291
  return subtitles;
3169
3292
  }
3170
3293
  function normalizeMaxItems(maxItems) {
@@ -3176,6 +3299,35 @@ function normalizeMaxItems(maxItems) {
3176
3299
  }
3177
3300
  return Math.max(0, Math.floor(maxItems));
3178
3301
  }
3302
+ function normalizeHideCompletedAfterMs(hideCompletedAfterMs) {
3303
+ if (hideCompletedAfterMs === false) {
3304
+ return false;
3305
+ }
3306
+ if (hideCompletedAfterMs === void 0 || !Number.isFinite(hideCompletedAfterMs)) {
3307
+ return DEFAULT_HIDE_COMPLETED_SUBTITLE_AFTER_MS;
3308
+ }
3309
+ return Math.max(0, Math.floor(hideCompletedAfterMs));
3310
+ }
3311
+ function getDefaultCompletedSubtitleDecision(hideCompletedAfterMs) {
3312
+ if (hideCompletedAfterMs === false) {
3313
+ return "keep";
3314
+ }
3315
+ return hideCompletedAfterMs === 0 ? "remove" : { removeAfterMs: hideCompletedAfterMs };
3316
+ }
3317
+ function normalizeCompletedSubtitleDecision(decision) {
3318
+ if (decision === "keep" || decision === "remove") {
3319
+ return decision;
3320
+ }
3321
+ if (decision && typeof decision === "object" && "removeAfterMs" in decision) {
3322
+ const removeAfterMs = Number(decision.removeAfterMs);
3323
+ if (!Number.isFinite(removeAfterMs)) {
3324
+ return "keep";
3325
+ }
3326
+ const normalized = Math.max(0, Math.floor(removeAfterMs));
3327
+ return normalized === 0 ? "remove" : { removeAfterMs: normalized };
3328
+ }
3329
+ return "keep";
3330
+ }
3179
3331
  function shouldUseConversationRole(role, roleSet, useModelStreamingTranscript) {
3180
3332
  if (!roleSet.has(role)) {
3181
3333
  return false;
@@ -3189,6 +3341,54 @@ function trimSubtitleItems(items, maxItems) {
3189
3341
  const sorted = [...items].sort((a, b) => a.timestamp - b.timestamp);
3190
3342
  return sorted.length > maxItems ? sorted.slice(sorted.length - maxItems) : sorted;
3191
3343
  }
3344
+ function isCompletedSubtitle(item) {
3345
+ return item.lifecycle === "done" || item.status === "completed" || item.status === "incomplete";
3346
+ }
3347
+ function getSubtitleCompletedReason(item) {
3348
+ return item.source === "response_audio_transcript" || item.role === "model" ? "response_done" : "conversation_done";
3349
+ }
3350
+ function abortCompletedSubtitleTasks(controllers) {
3351
+ for (const controller of controllers.values()) {
3352
+ controller.abort();
3353
+ }
3354
+ controllers.clear();
3355
+ }
3356
+ function applyCompletedSubtitleDecision(itemId, decision, controller, controllers, completedIds, removeSubtitle) {
3357
+ if (controller.signal.aborted) {
3358
+ return;
3359
+ }
3360
+ const normalizedDecision = normalizeCompletedSubtitleDecision(decision);
3361
+ if (normalizedDecision === "keep") {
3362
+ controllers.delete(itemId);
3363
+ return;
3364
+ }
3365
+ const remove = () => {
3366
+ if (controller.signal.aborted) {
3367
+ return;
3368
+ }
3369
+ controllers.delete(itemId);
3370
+ completedIds.delete(itemId);
3371
+ removeSubtitle(itemId);
3372
+ };
3373
+ if (normalizedDecision === "remove") {
3374
+ remove();
3375
+ return;
3376
+ }
3377
+ const timeout = setTimeout(remove, normalizedDecision.removeAfterMs);
3378
+ controller.signal.addEventListener(
3379
+ "abort",
3380
+ () => {
3381
+ clearTimeout(timeout);
3382
+ },
3383
+ { once: true }
3384
+ );
3385
+ }
3386
+ function removeSubtitleById(items, itemId) {
3387
+ if (!items.some((item) => item.id === itemId)) {
3388
+ return items;
3389
+ }
3390
+ return items.filter((item) => item.id !== itemId);
3391
+ }
3192
3392
  function getDisplayText(item) {
3193
3393
  return item.text || item.transcript;
3194
3394
  }
@@ -3314,6 +3514,8 @@ function IVISubtitleOverlay(props) {
3314
3514
  maxItems,
3315
3515
  maxVisible,
3316
3516
  useModelStreamingTranscript,
3517
+ hideCompletedAfterMs,
3518
+ onSubtitleCompleted,
3317
3519
  subtitleStyle,
3318
3520
  className,
3319
3521
  style
@@ -3321,7 +3523,9 @@ function IVISubtitleOverlay(props) {
3321
3523
  const entries = useIviSubtitles(runtime, {
3322
3524
  roles,
3323
3525
  maxItems: maxItems ?? maxVisible,
3324
- useModelStreamingTranscript
3526
+ useModelStreamingTranscript,
3527
+ hideCompletedAfterMs,
3528
+ onSubtitleCompleted
3325
3529
  });
3326
3530
  if (entries.length === 0) return null;
3327
3531
  const fontFamily = subtitleStyle?.fontFamily ?? "system-ui, -apple-system, sans-serif";
@@ -3453,7 +3657,6 @@ function IVITrtcPlayer(props) {
3453
3657
  height: "100%",
3454
3658
  minWidth: 0,
3455
3659
  minHeight: 0,
3456
- backgroundColor: "#000",
3457
3660
  position: "relative",
3458
3661
  ...style
3459
3662
  },
@@ -4412,6 +4615,6 @@ function getClientLogTag(category) {
4412
4615
  return "[IVI-CLIENT]";
4413
4616
  }
4414
4617
 
4415
- export { EMPTY_RUNTIME_STATE, IVILivekitPlayer, IVIStageView, IVISubtitleOverlay, IVITrackSlot, IVITrtcPlayer, IviFrontendSdk, IviRuntimeCoordinator, IviRuntimeDispatcher, LivekitSourceManager, TrtcSourceManager, isLivekitSourcePlayback, isReadyLivekitRuntimeSource, isSameLivekitConfig, useIviStageView, useIviSubtitles, useManagedIviRuntime, useRuntimeState };
4618
+ export { DEFAULT_HIDE_COMPLETED_SUBTITLE_AFTER_MS, EMPTY_RUNTIME_STATE, IVILivekitPlayer, IVIStageView, IVISubtitleOverlay, IVITrackSlot, IVITrtcPlayer, IviFrontendSdk, IviRuntimeCoordinator, IviRuntimeDispatcher, LivekitSourceManager, TrtcSourceManager, isLivekitSourcePlayback, isReadyLivekitRuntimeSource, isSameLivekitConfig, useIviStageView, useIviSubtitles, useManagedIviRuntime, useRuntimeState };
4416
4619
  //# sourceMappingURL=index.js.map
4417
4620
  //# sourceMappingURL=index.js.map