@vivix-ai/ivi-frontend-sdk 0.2.2 → 0.2.4

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
@@ -1,12 +1,21 @@
1
- import { IviRealtimeSession, IviStage, IviTrack, IviSource, IviSourcePlayback, IviSessionSourceFailedPayload, IviStream, IviStreamStatus, IviConversationItemRole, IviConversationItem, IviConversationItemContent, ReceiveSessionEndedEvent, ParsedIviEvent, IviRealtimeResponseCreateParams, ReceiveConversationItemAddedEvent, ReceiveConversationItemDoneEvent, ReceiveResponseCreatedEvent, IviSourcePlaybackTrtc, IviClient, IviClientConfig } from '@vivix/ivi-sdk-ts';
1
+ import { IviRealtimeSessionConfig, IviStageComposition, IviTrack, IviSourceAsset, IviReceiveSessionSourceReadyPayload, IviReceiveSessionSourceFailedPayload, IviStream, IviConversationItem, IviConversationItemContent, ReceiveSessionEndedEvent, ParsedIviEvent, IviRealtimeResponseCreateParams, ReceiveConversationItemAddedEvent, ReceiveConversationItemDoneEvent, ReceiveResponseCreatedEvent, IviClient, IviClientConfig } from '@vivix/ivi-sdk-ts';
2
2
  import { ReactNode, CSSProperties, ReactElement, VideoHTMLAttributes, ImgHTMLAttributes } from 'react';
3
3
  import { IviClientLogEntry } from '@vivix/ivi-sdk-ts/client';
4
4
 
5
5
  type IviRuntimeStatus = "idle" | "connecting" | "syncing" | "running" | "stopped";
6
+ type IviRuntimeConversationRole = NonNullable<IviConversationItem["role"]>;
7
+ type IviRuntimeSourcePlayback = IviReceiveSessionSourceReadyPayload["playback"];
8
+ type IviRuntimeTrtcPlayback = NonNullable<NonNullable<IviRuntimeSourcePlayback>["trtc"]> & {
9
+ app_id: string;
10
+ user_id: string;
11
+ user_sig: string;
12
+ room_id: string;
13
+ };
14
+ type IviRuntimeStreamStatus = IviStream["status"];
6
15
  interface IviRuntimeState {
7
16
  status: IviRuntimeStatus;
8
- session: IviRealtimeSession | null;
9
- stage: IviStage | null;
17
+ session: IviRealtimeSessionConfig | null;
18
+ stage: IviStageComposition | null;
10
19
  tracks: Map<string, IviTrack>;
11
20
  sources: Map<string, IviRuntimeSource>;
12
21
  streams: Map<string, IviRuntimeStream>;
@@ -18,7 +27,7 @@ interface IviRuntimeState {
18
27
  */
19
28
  interface IviRuntimeStream {
20
29
  stream: IviStream;
21
- status: IviStreamStatus;
30
+ status: IviRuntimeStreamStatus;
22
31
  /** stream.started 事件携带的关联 track ID。 */
23
32
  trackId?: string;
24
33
  error?: unknown;
@@ -32,14 +41,14 @@ interface IviRuntimeSourcePreloadState {
32
41
  autoclearAfterPlay: boolean;
33
42
  }
34
43
  interface IviRuntimeSource {
35
- source: IviSource;
44
+ source: IviSourceAsset;
36
45
  status: "created" | "ready" | "failed";
37
- playback?: IviSourcePlayback;
46
+ playback?: IviRuntimeSourcePlayback;
38
47
  width?: number;
39
48
  height?: number;
40
49
  durationMs?: number;
41
50
  hasAudio?: boolean;
42
- error?: IviSessionSourceFailedPayload["error"];
51
+ error?: IviReceiveSessionSourceFailedPayload["error"];
43
52
  /**
44
53
  * 预加载状态:
45
54
  * - 存在:渲染层应对该 source 进行预加载;
@@ -60,7 +69,7 @@ type IviRuntimeConversationLifecycle = "added" | "done";
60
69
  type IviRuntimeConversationStatus = "in_progress" | "completed" | "incomplete";
61
70
  interface IviRuntimeConversationItem {
62
71
  id: string;
63
- role: IviConversationItemRole;
72
+ role: IviRuntimeConversationRole;
64
73
  lifecycle: IviRuntimeConversationLifecycle;
65
74
  status: IviRuntimeConversationStatus;
66
75
  item: IviConversationItem;
@@ -162,7 +171,7 @@ declare class TrtcSourceManager {
162
171
  * 按 sourceId 注册/更新 TRTC 配置。
163
172
  * 若配置发生变化,会先销毁旧会话再按新参数重建连接。
164
173
  */
165
- upsertSource(sourceId: string, trtc: IviSourcePlaybackTrtc): void;
174
+ upsertSource(sourceId: string, trtc: IviRuntimeTrtcPlayback): void;
166
175
  /**
167
176
  * 删除指定 source 的 TRTC 会话并释放连接资源。
168
177
  * 该操作通常由 source.deleted 或会话重置触发。
@@ -402,7 +411,7 @@ declare const EMPTY_RUNTIME_STATE: IviRuntimeState;
402
411
  declare function useRuntimeState(runtime: IviRuntimeCoordinator | null): IviRuntimeState;
403
412
 
404
413
  interface IVITrtcPlayerProps {
405
- trtc: IviSourcePlaybackTrtc;
414
+ trtc: IviRuntimeTrtcPlayback;
406
415
  sourceId?: string;
407
416
  runtime?: IviRuntimeCoordinator | null;
408
417
  className?: string;
@@ -451,11 +460,18 @@ interface IVISubtitleOverlayProps {
451
460
  /** 会话条目列表(由 runtime state 提供) */
452
461
  conversations: IviRuntimeConversationItem[];
453
462
  /** 要显示的发言人角色,默认 ["user"]。传单个字符串或数组均可。 */
454
- roles?: IviConversationItemRole | IviConversationItemRole[];
463
+ roles?: IviRuntimeConversationRole | IviRuntimeConversationRole[];
455
464
  /** 同时可见的最大条目数,默认 2 */
456
465
  maxVisible?: number;
457
466
  /** lifecycle 变为 done 后自动消失的毫秒数,默认 5000 */
458
467
  dismissAfterMs?: number;
468
+ /**
469
+ * 按 source 生命周期展示字幕:
470
+ * - sourceKey 变化时建立一个新窗口;
471
+ * - 只展示窗口建立后新增到 conversations 的条目;
472
+ * - 不按 dismissAfterMs 自动消失,直到下一次 sourceKey 变化。
473
+ */
474
+ stickySourceKey?: string | null;
459
475
  /** 样式配置 */
460
476
  subtitleStyle?: IVISubtitleOverlayStyle;
461
477
  /** 自定义类名 */
@@ -505,6 +521,12 @@ interface IVITrackSlotProps {
505
521
  showSubtitle?: boolean;
506
522
  /** 字幕组件的自定义配置(字体、颜色、消失延时等) */
507
523
  subtitleProps?: Omit<IVISubtitleOverlayProps, "conversations">;
524
+ /**
525
+ * 对连续 generated_clip m3u8 启用按切片生命周期保留字幕。
526
+ * 启用后会展示 active source 切换后新增到 conversations 的全部条目,
527
+ * 并保留到下一次 source 切换(即当前切片播放完成)。
528
+ */
529
+ keepGeneratedClipSubtitlesUntilEnded?: boolean;
508
530
  /** 媒体空白区域(letterbox/pillarbox)的背景模式,默认 "black" */
509
531
  background?: IviTrackSlotBackground;
510
532
  }
package/dist/index.d.ts CHANGED
@@ -1,12 +1,21 @@
1
- import { IviRealtimeSession, IviStage, IviTrack, IviSource, IviSourcePlayback, IviSessionSourceFailedPayload, IviStream, IviStreamStatus, IviConversationItemRole, IviConversationItem, IviConversationItemContent, ReceiveSessionEndedEvent, ParsedIviEvent, IviRealtimeResponseCreateParams, ReceiveConversationItemAddedEvent, ReceiveConversationItemDoneEvent, ReceiveResponseCreatedEvent, IviSourcePlaybackTrtc, IviClient, IviClientConfig } from '@vivix/ivi-sdk-ts';
1
+ import { IviRealtimeSessionConfig, IviStageComposition, IviTrack, IviSourceAsset, IviReceiveSessionSourceReadyPayload, IviReceiveSessionSourceFailedPayload, IviStream, IviConversationItem, IviConversationItemContent, ReceiveSessionEndedEvent, ParsedIviEvent, IviRealtimeResponseCreateParams, ReceiveConversationItemAddedEvent, ReceiveConversationItemDoneEvent, ReceiveResponseCreatedEvent, IviClient, IviClientConfig } from '@vivix/ivi-sdk-ts';
2
2
  import { ReactNode, CSSProperties, ReactElement, VideoHTMLAttributes, ImgHTMLAttributes } from 'react';
3
3
  import { IviClientLogEntry } from '@vivix/ivi-sdk-ts/client';
4
4
 
5
5
  type IviRuntimeStatus = "idle" | "connecting" | "syncing" | "running" | "stopped";
6
+ type IviRuntimeConversationRole = NonNullable<IviConversationItem["role"]>;
7
+ type IviRuntimeSourcePlayback = IviReceiveSessionSourceReadyPayload["playback"];
8
+ type IviRuntimeTrtcPlayback = NonNullable<NonNullable<IviRuntimeSourcePlayback>["trtc"]> & {
9
+ app_id: string;
10
+ user_id: string;
11
+ user_sig: string;
12
+ room_id: string;
13
+ };
14
+ type IviRuntimeStreamStatus = IviStream["status"];
6
15
  interface IviRuntimeState {
7
16
  status: IviRuntimeStatus;
8
- session: IviRealtimeSession | null;
9
- stage: IviStage | null;
17
+ session: IviRealtimeSessionConfig | null;
18
+ stage: IviStageComposition | null;
10
19
  tracks: Map<string, IviTrack>;
11
20
  sources: Map<string, IviRuntimeSource>;
12
21
  streams: Map<string, IviRuntimeStream>;
@@ -18,7 +27,7 @@ interface IviRuntimeState {
18
27
  */
19
28
  interface IviRuntimeStream {
20
29
  stream: IviStream;
21
- status: IviStreamStatus;
30
+ status: IviRuntimeStreamStatus;
22
31
  /** stream.started 事件携带的关联 track ID。 */
23
32
  trackId?: string;
24
33
  error?: unknown;
@@ -32,14 +41,14 @@ interface IviRuntimeSourcePreloadState {
32
41
  autoclearAfterPlay: boolean;
33
42
  }
34
43
  interface IviRuntimeSource {
35
- source: IviSource;
44
+ source: IviSourceAsset;
36
45
  status: "created" | "ready" | "failed";
37
- playback?: IviSourcePlayback;
46
+ playback?: IviRuntimeSourcePlayback;
38
47
  width?: number;
39
48
  height?: number;
40
49
  durationMs?: number;
41
50
  hasAudio?: boolean;
42
- error?: IviSessionSourceFailedPayload["error"];
51
+ error?: IviReceiveSessionSourceFailedPayload["error"];
43
52
  /**
44
53
  * 预加载状态:
45
54
  * - 存在:渲染层应对该 source 进行预加载;
@@ -60,7 +69,7 @@ type IviRuntimeConversationLifecycle = "added" | "done";
60
69
  type IviRuntimeConversationStatus = "in_progress" | "completed" | "incomplete";
61
70
  interface IviRuntimeConversationItem {
62
71
  id: string;
63
- role: IviConversationItemRole;
72
+ role: IviRuntimeConversationRole;
64
73
  lifecycle: IviRuntimeConversationLifecycle;
65
74
  status: IviRuntimeConversationStatus;
66
75
  item: IviConversationItem;
@@ -162,7 +171,7 @@ declare class TrtcSourceManager {
162
171
  * 按 sourceId 注册/更新 TRTC 配置。
163
172
  * 若配置发生变化,会先销毁旧会话再按新参数重建连接。
164
173
  */
165
- upsertSource(sourceId: string, trtc: IviSourcePlaybackTrtc): void;
174
+ upsertSource(sourceId: string, trtc: IviRuntimeTrtcPlayback): void;
166
175
  /**
167
176
  * 删除指定 source 的 TRTC 会话并释放连接资源。
168
177
  * 该操作通常由 source.deleted 或会话重置触发。
@@ -402,7 +411,7 @@ declare const EMPTY_RUNTIME_STATE: IviRuntimeState;
402
411
  declare function useRuntimeState(runtime: IviRuntimeCoordinator | null): IviRuntimeState;
403
412
 
404
413
  interface IVITrtcPlayerProps {
405
- trtc: IviSourcePlaybackTrtc;
414
+ trtc: IviRuntimeTrtcPlayback;
406
415
  sourceId?: string;
407
416
  runtime?: IviRuntimeCoordinator | null;
408
417
  className?: string;
@@ -451,11 +460,18 @@ interface IVISubtitleOverlayProps {
451
460
  /** 会话条目列表(由 runtime state 提供) */
452
461
  conversations: IviRuntimeConversationItem[];
453
462
  /** 要显示的发言人角色,默认 ["user"]。传单个字符串或数组均可。 */
454
- roles?: IviConversationItemRole | IviConversationItemRole[];
463
+ roles?: IviRuntimeConversationRole | IviRuntimeConversationRole[];
455
464
  /** 同时可见的最大条目数,默认 2 */
456
465
  maxVisible?: number;
457
466
  /** lifecycle 变为 done 后自动消失的毫秒数,默认 5000 */
458
467
  dismissAfterMs?: number;
468
+ /**
469
+ * 按 source 生命周期展示字幕:
470
+ * - sourceKey 变化时建立一个新窗口;
471
+ * - 只展示窗口建立后新增到 conversations 的条目;
472
+ * - 不按 dismissAfterMs 自动消失,直到下一次 sourceKey 变化。
473
+ */
474
+ stickySourceKey?: string | null;
459
475
  /** 样式配置 */
460
476
  subtitleStyle?: IVISubtitleOverlayStyle;
461
477
  /** 自定义类名 */
@@ -505,6 +521,12 @@ interface IVITrackSlotProps {
505
521
  showSubtitle?: boolean;
506
522
  /** 字幕组件的自定义配置(字体、颜色、消失延时等) */
507
523
  subtitleProps?: Omit<IVISubtitleOverlayProps, "conversations">;
524
+ /**
525
+ * 对连续 generated_clip m3u8 启用按切片生命周期保留字幕。
526
+ * 启用后会展示 active source 切换后新增到 conversations 的全部条目,
527
+ * 并保留到下一次 source 切换(即当前切片播放完成)。
528
+ */
529
+ keepGeneratedClipSubtitlesUntilEnded?: boolean;
508
530
  /** 媒体空白区域(letterbox/pillarbox)的背景模式,默认 "black" */
509
531
  background?: IviTrackSlotBackground;
510
532
  }
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { ReceiveConversationItemAddedEvent, ReceiveConversationItemDoneEvent, ReceiveResponseCreatedEvent, IviClient, SessionCreatedEvent, ReceiveSessionEndedEvent, ReceiveSessionStageGetResponseEvent, ReceiveSessionStageUpdatedEvent, ReceiveSessionTrackCreatedEvent, ReceiveSessionTrackDeletedEvent, ReceiveSessionTrackTookEvent, ReceiveSessionTrackCuedEvent, ReceiveSessionTrackNextSetEvent, ReceiveSessionTracksListResponseEvent, ReceiveSessionSourceCreatedEvent, ReceiveSessionSourceReadyEvent, ReceiveSessionSourceFailedEvent, ReceiveSessionSourceDeletedEvent, ReceiveSessionSourcePreloadEvent, ReceiveSessionSourceClearPreloadEvent, ReceiveSessionSourcesListResponseEvent, ReceiveSessionSourcePlaybackCompletedEvent, SessionStreamCreatedEvent, ReceiveSessionStreamStartedEvent, ReceiveSessionStreamEndedEvent, ReceiveSessionStreamFailedEvent, ReceiveSessionStreamDeletedEvent, ReceiveSessionStreamsListResponseEvent, ReceiveConversationListResponseEvent, ReceiveResponseOutputTextDeltaEvent, ReceiveResponseOutputTextDoneEvent, ReceiveResponseOutputAudioTranscriptDeltaEvent, ReceiveResponseOutputAudioTranscriptDoneEvent, ReceiveResponseDoneEvent } from '@vivix/ivi-sdk-ts';
1
+ import { ReceiveConversationItemAddedEvent, ReceiveConversationItemDoneEvent, ReceiveResponseCreatedEvent, IviClient, ReceiveSessionCreatedEvent, ReceiveSessionEndedEvent, ReceiveSessionStageGetResponseEvent, ReceiveSessionStageUpdatedEvent, ReceiveSessionTrackCreatedEvent, ReceiveSessionTrackDeletedEvent, ReceiveSessionTrackTookEvent, ReceiveSessionTrackCuedEvent, ReceiveSessionTrackNextSetEvent, ReceiveSessionTracksListResponseEvent, ReceiveSessionSourceCreatedEvent, ReceiveSessionSourceReadyEvent, ReceiveSessionSourceFailedEvent, ReceiveSessionSourceDeletedEvent, ReceiveSessionSourcePreloadEvent, ReceiveSessionSourceClearPreloadEvent, ReceiveSessionSourcesListResponseEvent, ReceiveSessionSourcePlaybackCompletedEvent, ReceiveSessionStreamCreatedEvent, ReceiveSessionStreamStartedEvent, ReceiveSessionStreamEndedEvent, ReceiveSessionStreamFailedEvent, ReceiveSessionStreamDeletedEvent, ReceiveSessionStreamsListResponseEvent, ReceiveConversationListResponseEvent, ReceiveResponseOutputTextDeltaEvent, ReceiveResponseOutputTextDoneEvent, ReceiveResponseOutputAudioTranscriptDeltaEvent, ReceiveResponseOutputAudioTranscriptDoneEvent, ReceiveResponseDoneEvent } from '@vivix/ivi-sdk-ts';
2
2
  import { createContext, useState, useEffect, useMemo, useContext, useRef, useCallback } from 'react';
3
3
  import { jsx, jsxs } from 'react/jsx-runtime';
4
4
 
@@ -108,7 +108,7 @@ var SessionEventHandler = class {
108
108
  this.callbacks = callbacks;
109
109
  }
110
110
  handle(event) {
111
- if (event instanceof SessionCreatedEvent) {
111
+ if (event instanceof ReceiveSessionCreatedEvent) {
112
112
  const before = this.sessionManager.getSession();
113
113
  this.sessionManager.setSession(event.session);
114
114
  logIviStateChange("session", null, event.type, before, this.sessionManager.getSession());
@@ -257,7 +257,7 @@ var SourceEventHandler = class {
257
257
  const before = this.sourceManager.get(sourceId);
258
258
  this.sourceManager.upsertCreated(source);
259
259
  this.sourceManager.applyPreload(sourceId, {
260
- autoclearAfterPlay: event.autoclearAfterPlay
260
+ autoclearAfterPlay: event.autoclearAfterPlay ?? true
261
261
  });
262
262
  logIviStateChange("source", sourceId, event.type, before, this.sourceManager.get(sourceId));
263
263
  }
@@ -299,7 +299,7 @@ var StreamEventHandler = class {
299
299
  this.callbacks = callbacks;
300
300
  }
301
301
  handle(event) {
302
- if (event instanceof SessionStreamCreatedEvent) {
302
+ if (event instanceof ReceiveSessionStreamCreatedEvent) {
303
303
  const streamId = event.stream.stream_id;
304
304
  const before = this.streamManager.getAll().get(streamId);
305
305
  this.streamManager.upsertCreated(event.stream);
@@ -372,32 +372,37 @@ var ConversationEventHandler = class {
372
372
  return { handled: true };
373
373
  }
374
374
  if (event instanceof ReceiveConversationItemAddedEvent) {
375
- const before = this.conversationManager.getAllMap().get(event.item.id);
376
- this.conversationManager.upsertAdded(event.item);
375
+ if (!event.item.id) return { handled: true };
376
+ const item = { ...event.item, id: event.item.id };
377
+ const before = this.conversationManager.getAllMap().get(item.id);
378
+ this.conversationManager.upsertAdded(item);
377
379
  logIviStateChange(
378
380
  "conversationItem",
379
- event.item.id,
381
+ item.id,
380
382
  event.type,
381
383
  before,
382
- this.conversationManager.getAllMap().get(event.item.id)
384
+ this.conversationManager.getAllMap().get(item.id)
383
385
  );
384
386
  this.callbacks.onConversationsChanged();
385
387
  return { handled: true };
386
388
  }
387
389
  if (event instanceof ReceiveConversationItemDoneEvent) {
388
- const before = this.conversationManager.getAllMap().get(event.item.id);
389
- this.conversationManager.markDone(event.item);
390
+ if (!event.item.id) return { handled: true };
391
+ const item = { ...event.item, id: event.item.id };
392
+ const before = this.conversationManager.getAllMap().get(item.id);
393
+ this.conversationManager.markDone(item);
390
394
  logIviStateChange(
391
395
  "conversationItem",
392
- event.item.id,
396
+ item.id,
393
397
  event.type,
394
398
  before,
395
- this.conversationManager.getAllMap().get(event.item.id)
399
+ this.conversationManager.getAllMap().get(item.id)
396
400
  );
397
401
  this.callbacks.onConversationsChanged();
398
402
  return { handled: true };
399
403
  }
400
404
  if (event instanceof ReceiveResponseOutputTextDeltaEvent) {
405
+ if (!event.itemId) return { handled: true };
401
406
  const before = this.conversationManager.getAllMap().get(event.itemId);
402
407
  this.conversationManager.applyTextDelta(event.itemId, event.delta);
403
408
  logIviStateChange(
@@ -411,6 +416,7 @@ var ConversationEventHandler = class {
411
416
  return { handled: true };
412
417
  }
413
418
  if (event instanceof ReceiveResponseOutputTextDoneEvent) {
419
+ if (!event.itemId) return { handled: true };
414
420
  const before = this.conversationManager.getAllMap().get(event.itemId);
415
421
  this.conversationManager.applyTextDone(event.itemId, event.text);
416
422
  logIviStateChange(
@@ -424,6 +430,7 @@ var ConversationEventHandler = class {
424
430
  return { handled: true };
425
431
  }
426
432
  if (event instanceof ReceiveResponseOutputAudioTranscriptDeltaEvent) {
433
+ if (!event.itemId) return { handled: true };
427
434
  const before = this.conversationManager.getAllMap().get(event.itemId);
428
435
  this.conversationManager.applyTranscriptDelta(event.itemId, event.delta);
429
436
  logIviStateChange(
@@ -437,6 +444,7 @@ var ConversationEventHandler = class {
437
444
  return { handled: true };
438
445
  }
439
446
  if (event instanceof ReceiveResponseOutputAudioTranscriptDoneEvent) {
447
+ if (!event.itemId) return { handled: true };
440
448
  const before = this.conversationManager.getAllMap().get(event.itemId);
441
449
  this.conversationManager.applyTranscriptDone(event.itemId, event.transcript);
442
450
  logIviStateChange(
@@ -548,13 +556,13 @@ var TrackManager = class {
548
556
  }
549
557
  this.patchTrack(trackId, {
550
558
  active_source_id: resolvedSourceId,
551
- next_source_id: null
559
+ next_source_id: void 0
552
560
  });
553
561
  }
554
562
  applyTrackCued(trackId, sourceId) {
555
563
  this.patchTrack(trackId, {
556
564
  active_source_id: sourceId,
557
- next_source_id: null
565
+ next_source_id: void 0
558
566
  });
559
567
  }
560
568
  applyTrackNextSet(trackId, sourceId) {
@@ -851,6 +859,7 @@ var ConversationManager = class {
851
859
  const nextItems = /* @__PURE__ */ new Map();
852
860
  const nextOrder = [];
853
861
  items.forEach((item) => {
862
+ if (!hasItemId(item)) return;
854
863
  const runtimeItem = this.buildRuntimeItem(item, item.status === "in_progress" ? "added" : "done");
855
864
  nextItems.set(runtimeItem.id, runtimeItem);
856
865
  nextOrder.push(runtimeItem.id);
@@ -1083,6 +1092,9 @@ var ConversationManager = class {
1083
1092
  ];
1084
1093
  }
1085
1094
  };
1095
+ function hasItemId(item) {
1096
+ return typeof item.id === "string" && item.id.length > 0;
1097
+ }
1086
1098
 
1087
1099
  // src/runtime/managers/trtc-source-manager.ts
1088
1100
  var TAG = "[IVI-TRTC]";
@@ -1527,7 +1539,8 @@ var TrtcSourceManager = class {
1527
1539
  }
1528
1540
  };
1529
1541
  function isRuntimeTrtcSource(source) {
1530
- return source.status === "ready" && source.playback?.type === "trtc" && Boolean(source.playback.trtc);
1542
+ const trtc = source.playback?.trtc;
1543
+ return source.status === "ready" && source.playback?.type === "trtc" && typeof trtc?.app_id === "string" && typeof trtc.user_id === "string" && typeof trtc.user_sig === "string" && typeof trtc.room_id === "string";
1531
1544
  }
1532
1545
  function isSameTrtcConfig(a, b) {
1533
1546
  return a.app_id === b.app_id && a.user_id === b.user_id && a.user_sig === b.user_sig && a.room_id === b.room_id;
@@ -1958,7 +1971,7 @@ var IviRuntimeCoordinator = class {
1958
1971
  return [];
1959
1972
  }
1960
1973
  const missing = /* @__PURE__ */ new Set();
1961
- stage.composition.forEach((item) => {
1974
+ stage.composition?.forEach((item) => {
1962
1975
  if (!hasTrack(item.track_id)) {
1963
1976
  missing.add(item.track_id);
1964
1977
  }
@@ -1993,7 +2006,11 @@ var IviRuntimeCoordinator = class {
1993
2006
  }
1994
2007
  progressUserTextToResponseFlows(event) {
1995
2008
  if (event instanceof ReceiveConversationItemAddedEvent) {
1996
- const flow = this.pendingUserTextToResponseFlows.get(event.item.id);
2009
+ const itemId = event.item.id;
2010
+ if (!itemId) {
2011
+ return;
2012
+ }
2013
+ const flow = this.pendingUserTextToResponseFlows.get(itemId);
1997
2014
  if (!flow) {
1998
2015
  return;
1999
2016
  }
@@ -2003,7 +2020,11 @@ var IviRuntimeCoordinator = class {
2003
2020
  return;
2004
2021
  }
2005
2022
  if (event instanceof ReceiveConversationItemDoneEvent) {
2006
- const flow = this.pendingUserTextToResponseFlows.get(event.item.id);
2023
+ const itemId = event.item.id;
2024
+ if (!itemId) {
2025
+ return;
2026
+ }
2027
+ const flow = this.pendingUserTextToResponseFlows.get(itemId);
2007
2028
  if (!flow) {
2008
2029
  return;
2009
2030
  }
@@ -2144,14 +2165,14 @@ function IVIStageView(props) {
2144
2165
  }
2145
2166
  function buildSlotTrackMapFromState(state) {
2146
2167
  const map = /* @__PURE__ */ new Map();
2147
- state.stage?.composition.forEach((item) => {
2168
+ state.stage?.composition?.forEach((item) => {
2148
2169
  map.set(item.slot, item.track_id);
2149
2170
  });
2150
2171
  return map;
2151
2172
  }
2152
2173
  function buildSlotBindingsFromState(state) {
2153
2174
  const bindings = [];
2154
- state.stage?.composition.forEach((item) => {
2175
+ state.stage?.composition?.forEach((item) => {
2155
2176
  const track = state.tracks.get(item.track_id);
2156
2177
  const sourceId = track?.active_source_id ?? null;
2157
2178
  const source = sourceId ? state.sources.get(sourceId) : void 0;
@@ -2441,6 +2462,7 @@ function useApplyVolumeToSlot(containerRef, volume, enabled, activeSourceId) {
2441
2462
  return () => observer.disconnect();
2442
2463
  }, [containerRef, volume, enabled, activeSourceId]);
2443
2464
  }
2465
+ var ALL_CONVERSATION_ROLES = ["user", "director", "model", "vlm"];
2444
2466
  function useSubtitleEntries(conversations, maxVisible, dismissAfterMs) {
2445
2467
  const [visibleIds, setVisibleIds] = useState([]);
2446
2468
  const timersRef = useRef(/* @__PURE__ */ new Map());
@@ -2516,26 +2538,82 @@ function useSubtitleEntries(conversations, maxVisible, dismissAfterMs) {
2516
2538
  }).filter((entry) => entry !== null);
2517
2539
  }, [visibleIds, conversationMap]);
2518
2540
  }
2541
+ function useSourceWindowSubtitleEntries(conversations, sourceKey) {
2542
+ const normalizedSourceKey = sourceKey ?? null;
2543
+ const [windowState, setWindowState] = useState(() => ({
2544
+ sourceKey: normalizedSourceKey,
2545
+ baselineIds: new Set(conversations.map((item) => item.id)),
2546
+ visibleIds: []
2547
+ }));
2548
+ useEffect(() => {
2549
+ setWindowState((prev) => {
2550
+ if (prev.sourceKey !== normalizedSourceKey) {
2551
+ return {
2552
+ sourceKey: normalizedSourceKey,
2553
+ baselineIds: new Set(conversations.map((item) => item.id)),
2554
+ visibleIds: []
2555
+ };
2556
+ }
2557
+ if (!normalizedSourceKey) {
2558
+ return prev.visibleIds.length === 0 ? prev : { ...prev, visibleIds: [] };
2559
+ }
2560
+ const nextVisibleIds = conversations.filter((item) => {
2561
+ const displayText = item.text || item.transcript;
2562
+ return Boolean(displayText) && !prev.baselineIds.has(item.id);
2563
+ }).map((item) => item.id);
2564
+ if (nextVisibleIds.length === prev.visibleIds.length && nextVisibleIds.every((id, index) => id === prev.visibleIds[index])) {
2565
+ return prev;
2566
+ }
2567
+ return {
2568
+ ...prev,
2569
+ visibleIds: nextVisibleIds
2570
+ };
2571
+ });
2572
+ }, [conversations, normalizedSourceKey]);
2573
+ const conversationMap = useMemo(() => {
2574
+ const map = /* @__PURE__ */ new Map();
2575
+ for (const item of conversations) {
2576
+ map.set(item.id, item);
2577
+ }
2578
+ return map;
2579
+ }, [conversations]);
2580
+ return useMemo(() => {
2581
+ if (windowState.sourceKey !== normalizedSourceKey) {
2582
+ return [];
2583
+ }
2584
+ return windowState.visibleIds.map((id) => {
2585
+ const item = conversationMap.get(id);
2586
+ if (!item) return null;
2587
+ const text = item.text || item.transcript;
2588
+ if (!text) return null;
2589
+ return { id: item.id, role: item.role, text, lifecycle: item.lifecycle };
2590
+ }).filter((entry) => entry !== null);
2591
+ }, [windowState.sourceKey, windowState.visibleIds, conversationMap, normalizedSourceKey]);
2592
+ }
2519
2593
  var BREATHE_KEYFRAMES = `@keyframes ivi-subtitle-breathe{0%,100%{opacity:1}50%{opacity:.55}}`;
2520
2594
  function IVISubtitleOverlay(props) {
2521
2595
  const {
2522
2596
  conversations,
2523
- roles = "user",
2597
+ roles,
2524
2598
  maxVisible = 2,
2525
2599
  dismissAfterMs = 5e3,
2600
+ stickySourceKey,
2526
2601
  subtitleStyle,
2527
2602
  className,
2528
2603
  style
2529
2604
  } = props;
2605
+ const resolvedRoles = roles ?? (stickySourceKey ? ALL_CONVERSATION_ROLES : "user");
2530
2606
  const roleSet = useMemo(
2531
- () => new Set(Array.isArray(roles) ? roles : [roles]),
2532
- [roles]
2607
+ () => new Set(Array.isArray(resolvedRoles) ? resolvedRoles : [resolvedRoles]),
2608
+ [resolvedRoles]
2533
2609
  );
2534
2610
  const filtered = useMemo(
2535
2611
  () => conversations.filter((c) => roleSet.has(c.role)),
2536
2612
  [conversations, roleSet]
2537
2613
  );
2538
- const entries = useSubtitleEntries(filtered, maxVisible, dismissAfterMs);
2614
+ const queueEntries = useSubtitleEntries(filtered, maxVisible, dismissAfterMs);
2615
+ const sourceWindowEntries = useSourceWindowSubtitleEntries(filtered, stickySourceKey);
2616
+ const entries = stickySourceKey ? sourceWindowEntries : queueEntries;
2539
2617
  if (entries.length === 0) return null;
2540
2618
  const fontFamily = subtitleStyle?.fontFamily ?? "system-ui, -apple-system, sans-serif";
2541
2619
  const fontSize = subtitleStyle?.fontSize ?? 14;
@@ -3080,6 +3158,14 @@ function supportsSubtitleOverlay(source) {
3080
3158
  if (source.source.asset_type === "image") return false;
3081
3159
  return source.source.kind === "stream" || source.source.kind === "generation_stream" || source.source.kind === "generated_clip" || source.source.kind === "static";
3082
3160
  }
3161
+ function supportsGeneratedClipStickySubtitles(source) {
3162
+ if (!source) return false;
3163
+ if (source.source.kind !== "generated_clip") return false;
3164
+ if (source.source.asset_type === "image") return false;
3165
+ if (source.playback.type === "trtc") return false;
3166
+ const playbackUrl = source.playback.url;
3167
+ return typeof playbackUrl === "string" && isM3u8Url(playbackUrl);
3168
+ }
3083
3169
  function detectMediaVolumeType(source) {
3084
3170
  if (!source) return null;
3085
3171
  if (source.playback.type === "trtc") return "trtc";
@@ -3115,7 +3201,7 @@ function TrackSlotMediaContent(props) {
3115
3201
  const shouldMute = !isActive;
3116
3202
  if (renderMedia) return renderMedia(renderContext);
3117
3203
  if (source.playback.type === "trtc") {
3118
- if (!source.playback.trtc) return null;
3204
+ if (!isRuntimeTrtcPlayback(source.playback.trtc)) return null;
3119
3205
  if (renderTrtc) return renderTrtc(renderContext);
3120
3206
  const trtcMuted = shouldMute || Boolean(trtcPlayerProps?.muted);
3121
3207
  return /* @__PURE__ */ jsx(
@@ -3180,6 +3266,9 @@ function TrackSlotMediaContent(props) {
3180
3266
  }
3181
3267
  );
3182
3268
  }
3269
+ function isRuntimeTrtcPlayback(trtc) {
3270
+ return typeof trtc?.app_id === "string" && typeof trtc.user_id === "string" && typeof trtc.user_sig === "string" && typeof trtc.room_id === "string";
3271
+ }
3183
3272
  function buildAdaptiveMediaStyle(source, adaptToSourceSize, fitStrategy, background) {
3184
3273
  const objectFitStyle = fitStrategy === "auto" ? {} : {
3185
3274
  objectFit: fitStrategy ?? "contain"
@@ -3276,6 +3365,7 @@ function IVITrackSlot(props) {
3276
3365
  volumeControlProps,
3277
3366
  showSubtitle,
3278
3367
  subtitleProps,
3368
+ keepGeneratedClipSubtitlesUntilEnded = false,
3279
3369
  background = "black"
3280
3370
  } = props;
3281
3371
  const context = useIviStageView();
@@ -3286,6 +3376,7 @@ function IVITrackSlot(props) {
3286
3376
  const preloadEntries = useMultiPreloadSources(context.state.sources, activeSourceId);
3287
3377
  const activeEntry = preloadEntries.find((e) => e.isActive) ?? null;
3288
3378
  const activeSource = activeEntry?.source ?? null;
3379
+ const stickySubtitleSourceKey = keepGeneratedClipSubtitlesUntilEnded && supportsGeneratedClipStickySubtitles(activeSource) ? activeSourceId : null;
3289
3380
  const mediaType = detectMediaVolumeType(activeSource);
3290
3381
  const [volume, setVolume] = useVolumeMemory(showVolumeControl ? mediaType : null);
3291
3382
  useApplyVolumeToSlot(containerRef, volume, !!showVolumeControl && mediaType !== null, activeSourceId);
@@ -3348,7 +3439,8 @@ function IVITrackSlot(props) {
3348
3439
  IVISubtitleOverlay,
3349
3440
  {
3350
3441
  conversations: context.state.conversations,
3351
- ...subtitleProps
3442
+ ...subtitleProps,
3443
+ stickySourceKey: stickySubtitleSourceKey
3352
3444
  }
3353
3445
  ) }),
3354
3446
  showVolumeControl && mediaType !== null && /* @__PURE__ */ jsx("div", { style: VOLUME_OVERLAY_STYLE, children: /* @__PURE__ */ jsx(