@vivix-ai/ivi-frontend-sdk 0.2.3 → 0.2.5

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]";
@@ -1358,8 +1370,7 @@ var TrtcSourceManager = class {
1358
1370
  sdkAppId,
1359
1371
  userId: session.trtc.user_id,
1360
1372
  userSig: session.trtc.user_sig,
1361
- scene: TRTC.TYPE.SCENE_LIVE,
1362
- role: TRTC.TYPE.ROLE_AUDIENCE,
1373
+ scene: TRTC.TYPE.SCENE_RTC,
1363
1374
  autoReceiveAudio: true,
1364
1375
  ...isStringRoomId ? { strRoomId: session.trtc.room_id } : { roomId: Number(session.trtc.room_id) }
1365
1376
  });
@@ -1527,7 +1538,8 @@ var TrtcSourceManager = class {
1527
1538
  }
1528
1539
  };
1529
1540
  function isRuntimeTrtcSource(source) {
1530
- return source.status === "ready" && source.playback?.type === "trtc" && Boolean(source.playback.trtc);
1541
+ const trtc = source.playback?.trtc;
1542
+ 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
1543
  }
1532
1544
  function isSameTrtcConfig(a, b) {
1533
1545
  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 +1970,7 @@ var IviRuntimeCoordinator = class {
1958
1970
  return [];
1959
1971
  }
1960
1972
  const missing = /* @__PURE__ */ new Set();
1961
- stage.composition.forEach((item) => {
1973
+ stage.composition?.forEach((item) => {
1962
1974
  if (!hasTrack(item.track_id)) {
1963
1975
  missing.add(item.track_id);
1964
1976
  }
@@ -1993,7 +2005,11 @@ var IviRuntimeCoordinator = class {
1993
2005
  }
1994
2006
  progressUserTextToResponseFlows(event) {
1995
2007
  if (event instanceof ReceiveConversationItemAddedEvent) {
1996
- const flow = this.pendingUserTextToResponseFlows.get(event.item.id);
2008
+ const itemId = event.item.id;
2009
+ if (!itemId) {
2010
+ return;
2011
+ }
2012
+ const flow = this.pendingUserTextToResponseFlows.get(itemId);
1997
2013
  if (!flow) {
1998
2014
  return;
1999
2015
  }
@@ -2003,7 +2019,11 @@ var IviRuntimeCoordinator = class {
2003
2019
  return;
2004
2020
  }
2005
2021
  if (event instanceof ReceiveConversationItemDoneEvent) {
2006
- const flow = this.pendingUserTextToResponseFlows.get(event.item.id);
2022
+ const itemId = event.item.id;
2023
+ if (!itemId) {
2024
+ return;
2025
+ }
2026
+ const flow = this.pendingUserTextToResponseFlows.get(itemId);
2007
2027
  if (!flow) {
2008
2028
  return;
2009
2029
  }
@@ -2144,14 +2164,14 @@ function IVIStageView(props) {
2144
2164
  }
2145
2165
  function buildSlotTrackMapFromState(state) {
2146
2166
  const map = /* @__PURE__ */ new Map();
2147
- state.stage?.composition.forEach((item) => {
2167
+ state.stage?.composition?.forEach((item) => {
2148
2168
  map.set(item.slot, item.track_id);
2149
2169
  });
2150
2170
  return map;
2151
2171
  }
2152
2172
  function buildSlotBindingsFromState(state) {
2153
2173
  const bindings = [];
2154
- state.stage?.composition.forEach((item) => {
2174
+ state.stage?.composition?.forEach((item) => {
2155
2175
  const track = state.tracks.get(item.track_id);
2156
2176
  const sourceId = track?.active_source_id ?? null;
2157
2177
  const source = sourceId ? state.sources.get(sourceId) : void 0;
@@ -2441,6 +2461,7 @@ function useApplyVolumeToSlot(containerRef, volume, enabled, activeSourceId) {
2441
2461
  return () => observer.disconnect();
2442
2462
  }, [containerRef, volume, enabled, activeSourceId]);
2443
2463
  }
2464
+ var ALL_CONVERSATION_ROLES = ["user", "director", "model", "vlm"];
2444
2465
  function useSubtitleEntries(conversations, maxVisible, dismissAfterMs) {
2445
2466
  const [visibleIds, setVisibleIds] = useState([]);
2446
2467
  const timersRef = useRef(/* @__PURE__ */ new Map());
@@ -2516,26 +2537,82 @@ function useSubtitleEntries(conversations, maxVisible, dismissAfterMs) {
2516
2537
  }).filter((entry) => entry !== null);
2517
2538
  }, [visibleIds, conversationMap]);
2518
2539
  }
2540
+ function useSourceWindowSubtitleEntries(conversations, sourceKey) {
2541
+ const normalizedSourceKey = sourceKey ?? null;
2542
+ const [windowState, setWindowState] = useState(() => ({
2543
+ sourceKey: normalizedSourceKey,
2544
+ baselineIds: new Set(conversations.map((item) => item.id)),
2545
+ visibleIds: []
2546
+ }));
2547
+ useEffect(() => {
2548
+ setWindowState((prev) => {
2549
+ if (prev.sourceKey !== normalizedSourceKey) {
2550
+ return {
2551
+ sourceKey: normalizedSourceKey,
2552
+ baselineIds: new Set(conversations.map((item) => item.id)),
2553
+ visibleIds: []
2554
+ };
2555
+ }
2556
+ if (!normalizedSourceKey) {
2557
+ return prev.visibleIds.length === 0 ? prev : { ...prev, visibleIds: [] };
2558
+ }
2559
+ const nextVisibleIds = conversations.filter((item) => {
2560
+ const displayText = item.text || item.transcript;
2561
+ return Boolean(displayText) && !prev.baselineIds.has(item.id);
2562
+ }).map((item) => item.id);
2563
+ if (nextVisibleIds.length === prev.visibleIds.length && nextVisibleIds.every((id, index) => id === prev.visibleIds[index])) {
2564
+ return prev;
2565
+ }
2566
+ return {
2567
+ ...prev,
2568
+ visibleIds: nextVisibleIds
2569
+ };
2570
+ });
2571
+ }, [conversations, normalizedSourceKey]);
2572
+ const conversationMap = useMemo(() => {
2573
+ const map = /* @__PURE__ */ new Map();
2574
+ for (const item of conversations) {
2575
+ map.set(item.id, item);
2576
+ }
2577
+ return map;
2578
+ }, [conversations]);
2579
+ return useMemo(() => {
2580
+ if (windowState.sourceKey !== normalizedSourceKey) {
2581
+ return [];
2582
+ }
2583
+ return windowState.visibleIds.map((id) => {
2584
+ const item = conversationMap.get(id);
2585
+ if (!item) return null;
2586
+ const text = item.text || item.transcript;
2587
+ if (!text) return null;
2588
+ return { id: item.id, role: item.role, text, lifecycle: item.lifecycle };
2589
+ }).filter((entry) => entry !== null);
2590
+ }, [windowState.sourceKey, windowState.visibleIds, conversationMap, normalizedSourceKey]);
2591
+ }
2519
2592
  var BREATHE_KEYFRAMES = `@keyframes ivi-subtitle-breathe{0%,100%{opacity:1}50%{opacity:.55}}`;
2520
2593
  function IVISubtitleOverlay(props) {
2521
2594
  const {
2522
2595
  conversations,
2523
- roles = "user",
2596
+ roles,
2524
2597
  maxVisible = 2,
2525
2598
  dismissAfterMs = 5e3,
2599
+ stickySourceKey,
2526
2600
  subtitleStyle,
2527
2601
  className,
2528
2602
  style
2529
2603
  } = props;
2604
+ const resolvedRoles = roles ?? (stickySourceKey ? ALL_CONVERSATION_ROLES : "user");
2530
2605
  const roleSet = useMemo(
2531
- () => new Set(Array.isArray(roles) ? roles : [roles]),
2532
- [roles]
2606
+ () => new Set(Array.isArray(resolvedRoles) ? resolvedRoles : [resolvedRoles]),
2607
+ [resolvedRoles]
2533
2608
  );
2534
2609
  const filtered = useMemo(
2535
2610
  () => conversations.filter((c) => roleSet.has(c.role)),
2536
2611
  [conversations, roleSet]
2537
2612
  );
2538
- const entries = useSubtitleEntries(filtered, maxVisible, dismissAfterMs);
2613
+ const queueEntries = useSubtitleEntries(filtered, maxVisible, dismissAfterMs);
2614
+ const sourceWindowEntries = useSourceWindowSubtitleEntries(filtered, stickySourceKey);
2615
+ const entries = stickySourceKey ? sourceWindowEntries : queueEntries;
2539
2616
  if (entries.length === 0) return null;
2540
2617
  const fontFamily = subtitleStyle?.fontFamily ?? "system-ui, -apple-system, sans-serif";
2541
2618
  const fontSize = subtitleStyle?.fontSize ?? 14;
@@ -3080,6 +3157,14 @@ function supportsSubtitleOverlay(source) {
3080
3157
  if (source.source.asset_type === "image") return false;
3081
3158
  return source.source.kind === "stream" || source.source.kind === "generation_stream" || source.source.kind === "generated_clip" || source.source.kind === "static";
3082
3159
  }
3160
+ function supportsGeneratedClipStickySubtitles(source) {
3161
+ if (!source) return false;
3162
+ if (source.source.kind !== "generated_clip") return false;
3163
+ if (source.source.asset_type === "image") return false;
3164
+ if (source.playback.type === "trtc") return false;
3165
+ const playbackUrl = source.playback.url;
3166
+ return typeof playbackUrl === "string" && isM3u8Url(playbackUrl);
3167
+ }
3083
3168
  function detectMediaVolumeType(source) {
3084
3169
  if (!source) return null;
3085
3170
  if (source.playback.type === "trtc") return "trtc";
@@ -3115,7 +3200,7 @@ function TrackSlotMediaContent(props) {
3115
3200
  const shouldMute = !isActive;
3116
3201
  if (renderMedia) return renderMedia(renderContext);
3117
3202
  if (source.playback.type === "trtc") {
3118
- if (!source.playback.trtc) return null;
3203
+ if (!isRuntimeTrtcPlayback(source.playback.trtc)) return null;
3119
3204
  if (renderTrtc) return renderTrtc(renderContext);
3120
3205
  const trtcMuted = shouldMute || Boolean(trtcPlayerProps?.muted);
3121
3206
  return /* @__PURE__ */ jsx(
@@ -3180,6 +3265,9 @@ function TrackSlotMediaContent(props) {
3180
3265
  }
3181
3266
  );
3182
3267
  }
3268
+ function isRuntimeTrtcPlayback(trtc) {
3269
+ return typeof trtc?.app_id === "string" && typeof trtc.user_id === "string" && typeof trtc.user_sig === "string" && typeof trtc.room_id === "string";
3270
+ }
3183
3271
  function buildAdaptiveMediaStyle(source, adaptToSourceSize, fitStrategy, background) {
3184
3272
  const objectFitStyle = fitStrategy === "auto" ? {} : {
3185
3273
  objectFit: fitStrategy ?? "contain"
@@ -3276,6 +3364,7 @@ function IVITrackSlot(props) {
3276
3364
  volumeControlProps,
3277
3365
  showSubtitle,
3278
3366
  subtitleProps,
3367
+ keepGeneratedClipSubtitlesUntilEnded = false,
3279
3368
  background = "black"
3280
3369
  } = props;
3281
3370
  const context = useIviStageView();
@@ -3286,6 +3375,7 @@ function IVITrackSlot(props) {
3286
3375
  const preloadEntries = useMultiPreloadSources(context.state.sources, activeSourceId);
3287
3376
  const activeEntry = preloadEntries.find((e) => e.isActive) ?? null;
3288
3377
  const activeSource = activeEntry?.source ?? null;
3378
+ const stickySubtitleSourceKey = keepGeneratedClipSubtitlesUntilEnded && supportsGeneratedClipStickySubtitles(activeSource) ? activeSourceId : null;
3289
3379
  const mediaType = detectMediaVolumeType(activeSource);
3290
3380
  const [volume, setVolume] = useVolumeMemory(showVolumeControl ? mediaType : null);
3291
3381
  useApplyVolumeToSlot(containerRef, volume, !!showVolumeControl && mediaType !== null, activeSourceId);
@@ -3348,7 +3438,8 @@ function IVITrackSlot(props) {
3348
3438
  IVISubtitleOverlay,
3349
3439
  {
3350
3440
  conversations: context.state.conversations,
3351
- ...subtitleProps
3441
+ ...subtitleProps,
3442
+ stickySourceKey: stickySubtitleSourceKey
3352
3443
  }
3353
3444
  ) }),
3354
3445
  showVolumeControl && mediaType !== null && /* @__PURE__ */ jsx("div", { style: VOLUME_OVERLAY_STYLE, children: /* @__PURE__ */ jsx(