pdfjs-reader-core 0.3.0 → 0.4.0

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.js CHANGED
@@ -117,8 +117,8 @@ async function loadDocument(options) {
117
117
  signal.addEventListener("abort", abortHandler);
118
118
  }
119
119
  if (onProgress) {
120
- loadingTask.onProgress = ({ loaded, total }) => {
121
- onProgress({ loaded, total });
120
+ loadingTask.onProgress = ({ loaded: loaded2, total }) => {
121
+ onProgress({ loaded: loaded2, total });
122
122
  };
123
123
  }
124
124
  let document2;
@@ -248,9 +248,9 @@ function loadDocumentWithCallbacks(options) {
248
248
  };
249
249
  abortController.signal.addEventListener("abort", abortHandler);
250
250
  if (onProgress) {
251
- loadingTask.onProgress = ({ loaded, total }) => {
251
+ loadingTask.onProgress = ({ loaded: loaded2, total }) => {
252
252
  if (!abortController.signal.aborted) {
253
- onProgress({ loaded, total });
253
+ onProgress({ loaded: loaded2, total });
254
254
  }
255
255
  };
256
256
  }
@@ -605,13 +605,13 @@ function createViewerStore(initialOverrides = {}) {
605
605
  },
606
606
  zoomIn: () => {
607
607
  const { scale } = get();
608
- const currentIndex = ZOOM_LEVELS.findIndex((z) => z >= scale);
608
+ const currentIndex = ZOOM_LEVELS.findIndex((z2) => z2 >= scale);
609
609
  const nextIndex = Math.min(currentIndex + 1, ZOOM_LEVELS.length - 1);
610
610
  set({ scale: ZOOM_LEVELS[nextIndex] ?? MAX_SCALE });
611
611
  },
612
612
  zoomOut: () => {
613
613
  const { scale } = get();
614
- const currentIndex = ZOOM_LEVELS.findIndex((z) => z >= scale);
614
+ const currentIndex = ZOOM_LEVELS.findIndex((z2) => z2 >= scale);
615
615
  const prevIndex = Math.max(currentIndex - 1, 0);
616
616
  set({ scale: ZOOM_LEVELS[prevIndex] ?? MIN_SCALE });
617
617
  },
@@ -1839,6 +1839,34 @@ var init_page_turn_sound = __esm({
1839
1839
  }
1840
1840
  });
1841
1841
 
1842
+ // src/utils/camera-math.ts
1843
+ function fitPageScale(page, viewport) {
1844
+ const sx = viewport.width / page.width;
1845
+ const sy = viewport.height / page.height;
1846
+ return Math.min(sx, sy);
1847
+ }
1848
+ function computeCameraForBlock(bbox, page, viewport, opts = {}) {
1849
+ const targetScale = opts.targetScale ?? 1.5;
1850
+ const paddingPdf = opts.paddingPdf ?? 80;
1851
+ const [x1, y1, x2, y2] = bbox;
1852
+ const blockW = Math.max(1, x2 - x1 + paddingPdf * 2);
1853
+ const blockH = Math.max(1, y2 - y1 + paddingPdf * 2);
1854
+ const blockCX = (x1 + x2) / 2;
1855
+ const blockCY = (y1 + y2) / 2;
1856
+ const fitBlock = Math.min(viewport.width / blockW, viewport.height / blockH);
1857
+ const scale = fitBlock * targetScale;
1858
+ const pageCX = page.width / 2;
1859
+ const pageCY = page.height / 2;
1860
+ const x = (pageCX - blockCX) * scale;
1861
+ const y = (pageCY - blockCY) * scale;
1862
+ return { scale, x, y };
1863
+ }
1864
+ var init_camera_math = __esm({
1865
+ "src/utils/camera-math.ts"() {
1866
+ "use strict";
1867
+ }
1868
+ });
1869
+
1842
1870
  // src/utils/index.ts
1843
1871
  var init_utils = __esm({
1844
1872
  "src/utils/index.ts"() {
@@ -2257,6 +2285,78 @@ var init_student_store = __esm({
2257
2285
  }
2258
2286
  });
2259
2287
 
2288
+ // src/store/narration-store.ts
2289
+ import { createStore as createStore6 } from "zustand/vanilla";
2290
+ function createNarrationStore(overrides = {}) {
2291
+ return createStore6()((set) => ({
2292
+ ...initialState6,
2293
+ ...overrides,
2294
+ setCurrentChunk: (chunk) => set({ currentChunk: chunk }),
2295
+ setCurrentPage: (page) => set({ currentPage: page }),
2296
+ pushChunkHistory: (entry) => set((state) => ({
2297
+ chunkHistory: [
2298
+ ...state.chunkHistory.slice(-(MAX_HISTORY - 1)),
2299
+ entry
2300
+ ]
2301
+ })),
2302
+ setCamera: (camera) => set((state) => ({ camera: { ...state.camera, ...camera } })),
2303
+ addOverlay: (overlay) => set((state) => ({ activeOverlays: [...state.activeOverlays, overlay] })),
2304
+ removeOverlay: (id) => set((state) => ({
2305
+ activeOverlays: state.activeOverlays.filter((o) => o.id !== id)
2306
+ })),
2307
+ clearOverlays: (predicate) => set((state) => ({
2308
+ activeOverlays: predicate ? state.activeOverlays.filter((o) => !predicate(o)) : []
2309
+ })),
2310
+ setEngineStatus: (s) => set({ engineStatus: s }),
2311
+ setLlmStatus: (s, error = null) => set({ llmStatus: s, lastError: error }),
2312
+ setLastStoryboard: (sb) => set({ lastStoryboard: sb }),
2313
+ setPaused: (paused) => set({ isPaused: paused }),
2314
+ appendDebugEvent: (event) => set((state) => {
2315
+ debugEventCounter += 1;
2316
+ const next = {
2317
+ ...event,
2318
+ id: `dbg-${debugEventCounter}`,
2319
+ timestamp: Date.now()
2320
+ };
2321
+ return {
2322
+ debugEvents: [
2323
+ ...state.debugEvents.slice(-(MAX_DEBUG_EVENTS - 1)),
2324
+ next
2325
+ ]
2326
+ };
2327
+ }),
2328
+ clearDebugEvents: () => set({ debugEvents: [] }),
2329
+ reset: () => set(initialState6)
2330
+ }));
2331
+ }
2332
+ function makeOverlayId(action) {
2333
+ overlayIdCounter += 1;
2334
+ return `ov-${action.type}-${overlayIdCounter}-${Date.now()}`;
2335
+ }
2336
+ var MAX_HISTORY, initialState6, MAX_DEBUG_EVENTS, debugEventCounter, overlayIdCounter;
2337
+ var init_narration_store = __esm({
2338
+ "src/store/narration-store.ts"() {
2339
+ "use strict";
2340
+ MAX_HISTORY = 5;
2341
+ initialState6 = {
2342
+ currentChunk: null,
2343
+ currentPage: 1,
2344
+ chunkHistory: [],
2345
+ camera: { scale: 1, x: 0, y: 0, easing: "ease-in-out" },
2346
+ activeOverlays: [],
2347
+ engineStatus: "idle",
2348
+ llmStatus: "idle",
2349
+ lastStoryboard: null,
2350
+ lastError: null,
2351
+ isPaused: false,
2352
+ debugEvents: []
2353
+ };
2354
+ MAX_DEBUG_EVENTS = 50;
2355
+ debugEventCounter = 0;
2356
+ overlayIdCounter = 0;
2357
+ }
2358
+ });
2359
+
2260
2360
  // src/store/index.ts
2261
2361
  var init_store = __esm({
2262
2362
  "src/store/index.ts"() {
@@ -2266,6 +2366,7 @@ var init_store = __esm({
2266
2366
  init_search_store();
2267
2367
  init_agent_store();
2268
2368
  init_student_store();
2369
+ init_narration_store();
2269
2370
  }
2270
2371
  });
2271
2372
 
@@ -2275,7 +2376,7 @@ import { useStore } from "zustand";
2275
2376
  import { jsx } from "react/jsx-runtime";
2276
2377
  function PDFViewerProvider({
2277
2378
  children,
2278
- initialState: initialState6,
2379
+ initialState: initialState7,
2279
2380
  theme = "light",
2280
2381
  defaultSidebarPanel = "thumbnails",
2281
2382
  studentMode: _studentMode = false
@@ -2287,22 +2388,22 @@ function PDFViewerProvider({
2287
2388
  const studentStoreRef = useRef(null);
2288
2389
  if (!viewerStoreRef.current) {
2289
2390
  viewerStoreRef.current = createViewerStore({
2290
- ...initialState6?.viewer,
2391
+ ...initialState7?.viewer,
2291
2392
  theme,
2292
2393
  sidebarPanel: defaultSidebarPanel
2293
2394
  });
2294
2395
  }
2295
2396
  if (!annotationStoreRef.current) {
2296
- annotationStoreRef.current = createAnnotationStore(initialState6?.annotation);
2397
+ annotationStoreRef.current = createAnnotationStore(initialState7?.annotation);
2297
2398
  }
2298
2399
  if (!searchStoreRef.current) {
2299
- searchStoreRef.current = createSearchStore(initialState6?.search);
2400
+ searchStoreRef.current = createSearchStore(initialState7?.search);
2300
2401
  }
2301
2402
  if (!agentStoreRef.current) {
2302
- agentStoreRef.current = createAgentStore(initialState6?.agent);
2403
+ agentStoreRef.current = createAgentStore(initialState7?.agent);
2303
2404
  }
2304
2405
  if (!studentStoreRef.current) {
2305
- studentStoreRef.current = createStudentStore(initialState6?.student);
2406
+ studentStoreRef.current = createStudentStore(initialState7?.student);
2306
2407
  }
2307
2408
  useEffect(() => {
2308
2409
  return () => {
@@ -3616,8 +3717,8 @@ var init_PluginManager = __esm({
3616
3717
  /**
3617
3718
  * Get toolbar items by position
3618
3719
  */
3619
- getToolbarItemsByPosition(position) {
3620
- return this.getToolbarItems().filter((item) => item.position === position);
3720
+ getToolbarItemsByPosition(position2) {
3721
+ return this.getToolbarItems().filter((item) => item.position === position2);
3621
3722
  }
3622
3723
  /**
3623
3724
  * Get all sidebar panels from all plugins
@@ -4724,7 +4825,7 @@ var init_MobileToolbar = __esm({
4724
4825
  sidebarOpen,
4725
4826
  theme,
4726
4827
  onThemeChange,
4727
- position = "bottom",
4828
+ position: position2 = "bottom",
4728
4829
  className
4729
4830
  }) {
4730
4831
  const [showMoreMenu, setShowMoreMenu] = useState5(false);
@@ -4758,8 +4859,8 @@ var init_MobileToolbar = __esm({
4758
4859
  "bg-white dark:bg-gray-800",
4759
4860
  "border-gray-200 dark:border-gray-700",
4760
4861
  "px-2 py-1 safe-area-inset",
4761
- position === "top" && "top-0 border-b",
4762
- position === "bottom" && "bottom-0 border-t",
4862
+ position2 === "top" && "top-0 border-b",
4863
+ position2 === "bottom" && "bottom-0 border-t",
4763
4864
  className
4764
4865
  ),
4765
4866
  children: /* @__PURE__ */ jsxs2("div", { className: "flex items-center justify-between gap-1", children: [
@@ -4885,7 +4986,7 @@ var init_MobileToolbar = __esm({
4885
4986
  "bg-white dark:bg-gray-800",
4886
4987
  "rounded-lg shadow-lg",
4887
4988
  "border border-gray-200 dark:border-gray-700",
4888
- position === "bottom" ? "bottom-full mb-2" : "top-full mt-2"
4989
+ position2 === "bottom" ? "bottom-full mb-2" : "top-full mt-2"
4889
4990
  ),
4890
4991
  children: [
4891
4992
  /* @__PURE__ */ jsx3("div", { className: "px-2 py-1 text-xs text-gray-500 dark:text-gray-400 font-medium", children: "Theme" }),
@@ -6711,7 +6812,7 @@ var init_AnnotationToolbar = __esm({
6711
6812
  onShapeTypeChange: onShapeTypeChangeProp,
6712
6813
  onColorChange: onColorChangeProp,
6713
6814
  onStrokeWidthChange: onStrokeWidthChangeProp,
6714
- position = "top",
6815
+ position: position2 = "top",
6715
6816
  className
6716
6817
  }) {
6717
6818
  const storeActiveTool = useAnnotationStore((s) => s.activeAnnotationTool);
@@ -6761,9 +6862,9 @@ var init_AnnotationToolbar = __esm({
6761
6862
  {
6762
6863
  className: cn(
6763
6864
  "annotation-toolbar flex items-center gap-1 p-2 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700",
6764
- position === "floating" && "fixed bottom-20 left-1/2 -translate-x-1/2 z-50",
6765
- position === "top" && "sticky top-0 z-40",
6766
- position === "bottom" && "sticky bottom-0 z-40",
6865
+ position2 === "floating" && "fixed bottom-20 left-1/2 -translate-x-1/2 z-50",
6866
+ position2 === "top" && "sticky top-0 z-40",
6867
+ position2 === "bottom" && "sticky bottom-0 z-40",
6767
6868
  !isActive && "opacity-90",
6768
6869
  className
6769
6870
  ),
@@ -8412,7 +8513,7 @@ var init_SelectionToolbar = __esm({
8412
8513
  activeColor = "yellow",
8413
8514
  className
8414
8515
  }) {
8415
- const [position, setPosition] = useState16({ top: 0, left: 0, visible: false });
8516
+ const [position2, setPosition] = useState16({ top: 0, left: 0, visible: false });
8416
8517
  const toolbarRef = useRef14(null);
8417
8518
  useEffect17(() => {
8418
8519
  if (selection && selection.text && selection.rects.length > 0) {
@@ -8451,7 +8552,7 @@ var init_SelectionToolbar = __esm({
8451
8552
  const handleCopy = useCallback27(() => {
8452
8553
  onCopy?.();
8453
8554
  }, [onCopy]);
8454
- if (!position.visible || !selection?.text) {
8555
+ if (!position2.visible || !selection?.text) {
8455
8556
  return null;
8456
8557
  }
8457
8558
  return /* @__PURE__ */ jsxs18(
@@ -8469,8 +8570,8 @@ var init_SelectionToolbar = __esm({
8469
8570
  className
8470
8571
  ),
8471
8572
  style: {
8472
- top: position.top,
8473
- left: position.left,
8573
+ top: position2.top,
8574
+ left: position2.left,
8474
8575
  transform: "translateX(-50%)"
8475
8576
  },
8476
8577
  onMouseDown: (e) => {
@@ -8587,7 +8688,7 @@ var init_HighlightPopover = __esm({
8587
8688
  }) {
8588
8689
  const [isEditingComment, setIsEditingComment] = useState17(false);
8589
8690
  const [comment, setComment] = useState17(highlight?.comment ?? "");
8590
- const [position, setPosition] = useState17({ top: 0, left: 0, visible: false });
8691
+ const [position2, setPosition] = useState17({ top: 0, left: 0, visible: false });
8591
8692
  const popoverRef = useRef15(null);
8592
8693
  const textareaRef = useRef15(null);
8593
8694
  useEffect18(() => {
@@ -8635,11 +8736,11 @@ var init_HighlightPopover = __esm({
8635
8736
  onClose();
8636
8737
  }
8637
8738
  }
8638
- if (position.visible) {
8739
+ if (position2.visible) {
8639
8740
  document.addEventListener("mousedown", handleClickOutside);
8640
8741
  return () => document.removeEventListener("mousedown", handleClickOutside);
8641
8742
  }
8642
- }, [position.visible, onClose]);
8743
+ }, [position2.visible, onClose]);
8643
8744
  useEffect18(() => {
8644
8745
  function handleKeyDown(event) {
8645
8746
  if (event.key === "Escape") {
@@ -8651,11 +8752,11 @@ var init_HighlightPopover = __esm({
8651
8752
  }
8652
8753
  }
8653
8754
  }
8654
- if (position.visible) {
8755
+ if (position2.visible) {
8655
8756
  document.addEventListener("keydown", handleKeyDown);
8656
8757
  return () => document.removeEventListener("keydown", handleKeyDown);
8657
8758
  }
8658
- }, [position.visible, isEditingComment, highlight?.comment, onClose]);
8759
+ }, [position2.visible, isEditingComment, highlight?.comment, onClose]);
8659
8760
  const handleColorClick = useCallback28(
8660
8761
  (color) => {
8661
8762
  if (highlight) {
@@ -8682,7 +8783,7 @@ var init_HighlightPopover = __esm({
8682
8783
  onCopy?.(highlight.text);
8683
8784
  }
8684
8785
  }, [highlight, onCopy]);
8685
- if (!highlight || !position.visible) {
8786
+ if (!highlight || !position2.visible) {
8686
8787
  return null;
8687
8788
  }
8688
8789
  return /* @__PURE__ */ jsxs19(
@@ -8699,8 +8800,8 @@ var init_HighlightPopover = __esm({
8699
8800
  className
8700
8801
  ),
8701
8802
  style: {
8702
- top: position.top,
8703
- left: position.left,
8803
+ top: position2.top,
8804
+ left: position2.left,
8704
8805
  transform: "translate(-50%, -100%)",
8705
8806
  width: 280
8706
8807
  },
@@ -9927,10 +10028,23 @@ var init_BookModeContainer = __esm({
9927
10028
  const scrollToPageRequest = useViewerStore((s) => s.scrollToPageRequest);
9928
10029
  const { viewerStore } = usePDFViewerStores();
9929
10030
  const [pages, setPages] = useState21([]);
9930
- const [pageDims, setPageDims] = useState21({ width: 612, height: 792 });
10031
+ const [rawPageDims, setRawPageDims] = useState21({ width: 612, height: 792 });
9931
10032
  const [isLoadingPages, setIsLoadingPages] = useState21(false);
10033
+ const containerRef = useRef19(null);
10034
+ const [containerSize, setContainerSize] = useState21({ width: 0, height: 0 });
9932
10035
  const flipBookRef = useRef19(null);
9933
10036
  const isSyncingRef = useRef19(false);
10037
+ useEffect22(() => {
10038
+ const el = containerRef.current;
10039
+ if (!el) return;
10040
+ const measure = () => {
10041
+ setContainerSize({ width: el.clientWidth, height: el.clientHeight });
10042
+ };
10043
+ measure();
10044
+ const ro = new ResizeObserver(measure);
10045
+ ro.observe(el);
10046
+ return () => ro.disconnect();
10047
+ }, []);
9934
10048
  useEffect22(() => {
9935
10049
  if (!document2) {
9936
10050
  setPages([]);
@@ -9946,12 +10060,12 @@ var init_BookModeContainer = __esm({
9946
10060
  }
9947
10061
  const results = await Promise.allSettled(pagePromises);
9948
10062
  if (!cancelled) {
9949
- const loaded = results.map((r) => r.status === "fulfilled" ? r.value : null);
9950
- setPages(loaded);
9951
- const firstPage = loaded[0];
10063
+ const loaded2 = results.map((r) => r.status === "fulfilled" ? r.value : null);
10064
+ setPages(loaded2);
10065
+ const firstPage = loaded2[0];
9952
10066
  if (firstPage) {
9953
- const vp = firstPage.getViewport({ scale, rotation });
9954
- setPageDims({ width: Math.floor(vp.width), height: Math.floor(vp.height) });
10067
+ const vp = firstPage.getViewport({ scale: 1, rotation });
10068
+ setRawPageDims({ width: vp.width, height: vp.height });
9955
10069
  }
9956
10070
  }
9957
10071
  } catch {
@@ -9963,13 +10077,27 @@ var init_BookModeContainer = __esm({
9963
10077
  return () => {
9964
10078
  cancelled = true;
9965
10079
  };
9966
- }, [document2, numPages, scale, rotation]);
10080
+ }, [document2, numPages, rotation]);
9967
10081
  useEffect22(() => {
9968
10082
  if (pages[0]) {
9969
- const vp = pages[0].getViewport({ scale, rotation });
9970
- setPageDims({ width: Math.floor(vp.width), height: Math.floor(vp.height) });
9971
- }
9972
- }, [pages, scale, rotation]);
10083
+ const vp = pages[0].getViewport({ scale: 1, rotation });
10084
+ setRawPageDims({ width: vp.width, height: vp.height });
10085
+ }
10086
+ }, [pages, rotation]);
10087
+ const padding = 8;
10088
+ const fitWidth = Math.max(containerSize.width - padding * 2, 200);
10089
+ const fitHeight = Math.max(containerSize.height - padding * 2, 300);
10090
+ const pageAspect = rawPageDims.width / rawPageDims.height;
10091
+ let displayWidth;
10092
+ let displayHeight;
10093
+ if (fitWidth / fitHeight > pageAspect) {
10094
+ displayHeight = fitHeight;
10095
+ displayWidth = Math.floor(fitHeight * pageAspect);
10096
+ } else {
10097
+ displayWidth = fitWidth;
10098
+ displayHeight = Math.floor(fitWidth / pageAspect);
10099
+ }
10100
+ const renderScale = displayWidth / rawPageDims.width;
9973
10101
  useEffect22(() => {
9974
10102
  const pageFlip = flipBookRef.current?.pageFlip();
9975
10103
  if (!pageFlip) return;
@@ -10003,18 +10131,15 @@ var init_BookModeContainer = __esm({
10003
10131
  sepia: "bg-amber-50"
10004
10132
  };
10005
10133
  const themeClass = theme === "dark" ? "dark" : theme === "sepia" ? "sepia" : "";
10006
- if (!document2) {
10007
- return /* @__PURE__ */ jsx27("div", { className: cn("document-container", "flex-1", themeStyles[theme], className), children: /* @__PURE__ */ jsx27(PDFLoadingScreen, { phase: isLoading ? "fetching" : "initializing" }) });
10008
- }
10009
- if (isLoadingPages || pages.length === 0) {
10010
- return /* @__PURE__ */ jsx27("div", { className: cn("document-container", "flex-1", themeStyles[theme], className), children: /* @__PURE__ */ jsx27(PDFLoadingScreen, { phase: "rendering" }) });
10011
- }
10134
+ const ready = !!document2 && !isLoadingPages && pages.length > 0;
10135
+ const hasContainer = containerSize.width > 0 && containerSize.height > 0;
10012
10136
  return /* @__PURE__ */ jsxs23(
10013
10137
  "div",
10014
10138
  {
10139
+ ref: containerRef,
10015
10140
  className: cn(
10016
10141
  "book-mode-container",
10017
- "flex-1 overflow-hidden",
10142
+ "flex-1 h-full w-full overflow-hidden",
10018
10143
  "flex items-center justify-center",
10019
10144
  themeStyles[theme],
10020
10145
  themeClass,
@@ -10022,24 +10147,30 @@ var init_BookModeContainer = __esm({
10022
10147
  ),
10023
10148
  style: { userSelect: "none", WebkitUserSelect: "none" },
10024
10149
  children: [
10025
- /* @__PURE__ */ jsx27(
10150
+ !ready && /* @__PURE__ */ jsx27(
10151
+ PDFLoadingScreen,
10152
+ {
10153
+ phase: !document2 ? isLoading ? "fetching" : "initializing" : "rendering"
10154
+ }
10155
+ ),
10156
+ ready && hasContainer && /* @__PURE__ */ jsx27(
10026
10157
  HTMLFlipBook,
10027
10158
  {
10028
10159
  ref: flipBookRef,
10029
- width: pageDims.width,
10030
- height: pageDims.height,
10031
- size: "stretch",
10032
- minWidth: 300,
10033
- maxWidth: pageDims.width,
10034
- minHeight: 400,
10035
- maxHeight: pageDims.height,
10160
+ width: displayWidth,
10161
+ height: displayHeight,
10162
+ size: "fixed",
10163
+ minWidth: displayWidth,
10164
+ maxWidth: displayWidth,
10165
+ minHeight: displayHeight,
10166
+ maxHeight: displayHeight,
10036
10167
  drawShadow,
10037
10168
  maxShadowOpacity,
10038
10169
  flippingTime,
10039
10170
  usePortrait: true,
10040
10171
  startPage: currentPage - 1,
10041
10172
  showCover: false,
10042
- mobileScrollSupport: false,
10173
+ mobileScrollSupport: true,
10043
10174
  swipeDistance: 30,
10044
10175
  showPageCorners: true,
10045
10176
  useMouseEvents: true,
@@ -10048,7 +10179,7 @@ var init_BookModeContainer = __esm({
10048
10179
  className: "book-flipbook",
10049
10180
  style: {},
10050
10181
  startZIndex: 0,
10051
- autoSize: true,
10182
+ autoSize: false,
10052
10183
  renderOnlyPageLengthChange: false,
10053
10184
  disableFlipByClick: false,
10054
10185
  children: pages.map((page, index) => /* @__PURE__ */ jsx27(
@@ -10056,20 +10187,15 @@ var init_BookModeContainer = __esm({
10056
10187
  {
10057
10188
  pageNumber: index + 1,
10058
10189
  page,
10059
- scale,
10190
+ scale: renderScale,
10060
10191
  rotation,
10061
- width: pageDims.width,
10062
- height: pageDims.height
10192
+ width: displayWidth,
10193
+ height: displayHeight
10063
10194
  },
10064
10195
  index
10065
10196
  ))
10066
10197
  }
10067
- ),
10068
- /* @__PURE__ */ jsxs23("div", { className: "book-page-indicator", children: [
10069
- currentPage,
10070
- " / ",
10071
- numPages
10072
- ] })
10198
+ )
10073
10199
  ]
10074
10200
  }
10075
10201
  );
@@ -10087,7 +10213,7 @@ var init_FloatingZoomControls = __esm({
10087
10213
  init_hooks();
10088
10214
  init_utils();
10089
10215
  FloatingZoomControls = memo27(function FloatingZoomControls2({
10090
- position = "bottom-right",
10216
+ position: position2 = "bottom-right",
10091
10217
  className,
10092
10218
  showFitToWidth = true,
10093
10219
  showFitToPage = false,
@@ -10128,7 +10254,7 @@ var init_FloatingZoomControls = __esm({
10128
10254
  "bg-white dark:bg-gray-800 rounded-lg shadow-lg",
10129
10255
  "border border-gray-200 dark:border-gray-700",
10130
10256
  "p-1",
10131
- positionClasses[position],
10257
+ positionClasses[position2],
10132
10258
  className
10133
10259
  ),
10134
10260
  children: [
@@ -10841,12 +10967,12 @@ var init_PDFViewerClient = __esm({
10841
10967
  src,
10842
10968
  workerSrc,
10843
10969
  signal: abortController.signal,
10844
- onProgress: ({ loaded, total }) => {
10970
+ onProgress: ({ loaded: loaded2, total }) => {
10845
10971
  if (!mountedRef.current || srcIdRef.current !== loadId || abortController.signal.aborted) {
10846
10972
  return;
10847
10973
  }
10848
10974
  const now = Date.now();
10849
- const percent = total > 0 ? Math.round(loaded / total * 100) : 0;
10975
+ const percent = total > 0 ? Math.round(loaded2 / total * 100) : 0;
10850
10976
  const timePassed = now - lastProgressUpdate >= PROGRESS_THROTTLE_MS;
10851
10977
  const percentChanged = Math.abs(percent - lastPercent) >= PROGRESS_MIN_CHANGE;
10852
10978
  const isComplete = percent >= 100;
@@ -10857,10 +10983,10 @@ var init_PDFViewerClient = __esm({
10857
10983
  loadingProgress: {
10858
10984
  phase: "fetching",
10859
10985
  percent,
10860
- bytesLoaded: loaded,
10986
+ bytesLoaded: loaded2,
10861
10987
  totalBytes: total
10862
10988
  },
10863
- streamingProgress: { loaded, total },
10989
+ streamingProgress: { loaded: loaded2, total },
10864
10990
  documentLoadingState: "loading"
10865
10991
  });
10866
10992
  }
@@ -11808,7 +11934,7 @@ import { jsx as jsx34 } from "react/jsx-runtime";
11808
11934
  var QuickNoteButton = memo33(function QuickNoteButton2({
11809
11935
  pageNumber,
11810
11936
  scale,
11811
- position = "top-right",
11937
+ position: position2 = "top-right",
11812
11938
  onClick,
11813
11939
  className,
11814
11940
  visible = true
@@ -11817,11 +11943,11 @@ var QuickNoteButton = memo33(function QuickNoteButton2({
11817
11943
  const handleClick = useCallback38(
11818
11944
  (e) => {
11819
11945
  e.stopPropagation();
11820
- const x = position === "top-right" ? 80 : 80;
11821
- const y = position === "top-right" ? 20 : 80;
11946
+ const x = position2 === "top-right" ? 80 : 80;
11947
+ const y = position2 === "top-right" ? 20 : 80;
11822
11948
  onClick(pageNumber, x / scale, y / scale);
11823
11949
  },
11824
- [pageNumber, onClick, position, scale]
11950
+ [pageNumber, onClick, position2, scale]
11825
11951
  );
11826
11952
  if (!visible) {
11827
11953
  return null;
@@ -11842,8 +11968,8 @@ var QuickNoteButton = memo33(function QuickNoteButton2({
11842
11968
  "transition-all duration-200",
11843
11969
  "focus:outline-none focus:ring-2 focus:ring-yellow-500 focus:ring-offset-2",
11844
11970
  isHovered && "scale-110",
11845
- position === "top-right" && "top-3 right-3",
11846
- position === "bottom-right" && "bottom-3 right-3",
11971
+ position2 === "top-right" && "top-3 right-3",
11972
+ position2 === "bottom-right" && "bottom-3 right-3",
11847
11973
  className
11848
11974
  ),
11849
11975
  title: "Add quick note",
@@ -11869,7 +11995,7 @@ import { memo as memo34, useCallback as useCallback39, useState as useState27, u
11869
11995
  import { jsx as jsx35, jsxs as jsxs29 } from "react/jsx-runtime";
11870
11996
  var QuickNotePopover = memo34(function QuickNotePopover2({
11871
11997
  visible,
11872
- position,
11998
+ position: position2,
11873
11999
  initialContent = "",
11874
12000
  agentLastStatement,
11875
12001
  onSave,
@@ -11879,7 +12005,7 @@ var QuickNotePopover = memo34(function QuickNotePopover2({
11879
12005
  const [content, setContent] = useState27(initialContent);
11880
12006
  const textareaRef = useRef24(null);
11881
12007
  const popoverRef = useRef24(null);
11882
- const [adjustedPosition, setAdjustedPosition] = useState27(position);
12008
+ const [adjustedPosition, setAdjustedPosition] = useState27(position2);
11883
12009
  useEffect25(() => {
11884
12010
  if (visible && textareaRef.current) {
11885
12011
  textareaRef.current.focus();
@@ -11894,7 +12020,7 @@ var QuickNotePopover = memo34(function QuickNotePopover2({
11894
12020
  if (!visible || !popoverRef.current) return;
11895
12021
  const rect = popoverRef.current.getBoundingClientRect();
11896
12022
  const padding = 10;
11897
- let { x, y } = position;
12023
+ let { x, y } = position2;
11898
12024
  if (x + rect.width > window.innerWidth - padding) {
11899
12025
  x = window.innerWidth - rect.width - padding;
11900
12026
  }
@@ -11908,7 +12034,7 @@ var QuickNotePopover = memo34(function QuickNotePopover2({
11908
12034
  y = padding;
11909
12035
  }
11910
12036
  setAdjustedPosition({ x, y });
11911
- }, [position, visible]);
12037
+ }, [position2, visible]);
11912
12038
  const handleSave = useCallback39(() => {
11913
12039
  if (content.trim()) {
11914
12040
  onSave(content.trim());
@@ -12025,11 +12151,11 @@ import { jsx as jsx36, jsxs as jsxs30 } from "react/jsx-runtime";
12025
12151
  var AskAboutOverlay = memo35(function AskAboutOverlay2({
12026
12152
  visible,
12027
12153
  progress,
12028
- position,
12154
+ position: position2,
12029
12155
  size = 60,
12030
12156
  className
12031
12157
  }) {
12032
- if (!visible || !position) {
12158
+ if (!visible || !position2) {
12033
12159
  return null;
12034
12160
  }
12035
12161
  const strokeWidth = 4;
@@ -12045,8 +12171,8 @@ var AskAboutOverlay = memo35(function AskAboutOverlay2({
12045
12171
  className
12046
12172
  ),
12047
12173
  style: {
12048
- left: position.x - size / 2,
12049
- top: position.y - size / 2
12174
+ left: position2.x - size / 2,
12175
+ top: position2.y - size / 2
12050
12176
  },
12051
12177
  children: [
12052
12178
  /* @__PURE__ */ jsxs30(
@@ -12134,20 +12260,20 @@ init_utils();
12134
12260
  import { memo as memo36, useCallback as useCallback40, useState as useState28, useRef as useRef25, useEffect as useEffect26 } from "react";
12135
12261
  import { jsx as jsx37, jsxs as jsxs31 } from "react/jsx-runtime";
12136
12262
  var AskAboutTrigger = memo36(function AskAboutTrigger2({
12137
- position,
12263
+ position: position2,
12138
12264
  onConfirm,
12139
12265
  onCancel,
12140
12266
  visible,
12141
12267
  autoHideDelay = 5e3,
12142
12268
  className
12143
12269
  }) {
12144
- const [adjustedPosition, setAdjustedPosition] = useState28(position);
12270
+ const [adjustedPosition, setAdjustedPosition] = useState28(position2);
12145
12271
  const triggerRef = useRef25(null);
12146
12272
  useEffect26(() => {
12147
12273
  if (!visible || !triggerRef.current) return;
12148
12274
  const rect = triggerRef.current.getBoundingClientRect();
12149
12275
  const padding = 10;
12150
- let { x, y } = position;
12276
+ let { x, y } = position2;
12151
12277
  if (x + rect.width / 2 > window.innerWidth - padding) {
12152
12278
  x = window.innerWidth - rect.width / 2 - padding;
12153
12279
  }
@@ -12155,10 +12281,10 @@ var AskAboutTrigger = memo36(function AskAboutTrigger2({
12155
12281
  x = rect.width / 2 + padding;
12156
12282
  }
12157
12283
  if (y + rect.height > window.innerHeight - padding) {
12158
- y = position.y - rect.height - 20;
12284
+ y = position2.y - rect.height - 20;
12159
12285
  }
12160
12286
  setAdjustedPosition({ x, y });
12161
- }, [position, visible]);
12287
+ }, [position2, visible]);
12162
12288
  useEffect26(() => {
12163
12289
  if (!visible || autoHideDelay === 0) return;
12164
12290
  const timer = setTimeout(onCancel, autoHideDelay);
@@ -12806,6 +12932,2167 @@ function withErrorBoundary({ component, ...props }) {
12806
12932
  // src/components/index.ts
12807
12933
  init_PDFLoadingScreen2();
12808
12934
 
12935
+ // src/components/TutorMode/TutorModeContainer.tsx
12936
+ init_PDFPage2();
12937
+ init_hooks();
12938
+ import { useEffect as useEffect28, useMemo as useMemo15, useRef as useRef27, useState as useState30 } from "react";
12939
+ import { useStore as useStore2 } from "zustand";
12940
+
12941
+ // src/components/TutorMode/CameraView.tsx
12942
+ import { motion } from "framer-motion";
12943
+ import { jsx as jsx41 } from "react/jsx-runtime";
12944
+ function CameraView({
12945
+ camera,
12946
+ children,
12947
+ durationMs = 700,
12948
+ className
12949
+ }) {
12950
+ return /* @__PURE__ */ jsx41(
12951
+ motion.div,
12952
+ {
12953
+ className,
12954
+ style: {
12955
+ transformOrigin: "50% 50%",
12956
+ willChange: "transform",
12957
+ width: "100%",
12958
+ height: "100%",
12959
+ position: "relative"
12960
+ },
12961
+ animate: {
12962
+ scale: camera.scale,
12963
+ x: camera.x,
12964
+ y: camera.y
12965
+ },
12966
+ transition: {
12967
+ duration: durationMs / 1e3,
12968
+ ease: camera.easing === "linear" ? "linear" : camera.easing === "ease-in" ? [0.42, 0, 1, 1] : camera.easing === "ease-out" ? [0, 0, 0.58, 1] : [0.42, 0, 0.58, 1]
12969
+ },
12970
+ children
12971
+ }
12972
+ );
12973
+ }
12974
+
12975
+ // src/components/TutorMode/CinemaLayer.tsx
12976
+ import { AnimatePresence } from "framer-motion";
12977
+
12978
+ // src/components/TutorMode/SpotlightMask.tsx
12979
+ import { useId } from "react";
12980
+ import { motion as motion2 } from "framer-motion";
12981
+ import { jsx as jsx42, jsxs as jsxs35 } from "react/jsx-runtime";
12982
+ function SpotlightMask({
12983
+ page,
12984
+ bbox,
12985
+ action,
12986
+ durationMs = 400
12987
+ }) {
12988
+ const maskId = useId();
12989
+ const filterId = `${maskId}-blur`;
12990
+ const [x1, y1, x2, y2] = bbox;
12991
+ const w = Math.max(0, x2 - x1);
12992
+ const h = Math.max(0, y2 - y1);
12993
+ const rx = action.shape === "rounded" ? 12 : action.shape === "ellipse" ? w / 2 : 0;
12994
+ const ry = action.shape === "rounded" ? 12 : action.shape === "ellipse" ? h / 2 : 0;
12995
+ const feather = action.feather_px;
12996
+ return /* @__PURE__ */ jsxs35(
12997
+ "svg",
12998
+ {
12999
+ viewBox: `0 0 ${page.width} ${page.height}`,
13000
+ width: page.width,
13001
+ height: page.height,
13002
+ preserveAspectRatio: "none",
13003
+ style: {
13004
+ position: "absolute",
13005
+ inset: 0,
13006
+ pointerEvents: "none",
13007
+ width: page.width,
13008
+ height: page.height
13009
+ },
13010
+ "data-role": "spotlight-mask",
13011
+ children: [
13012
+ /* @__PURE__ */ jsxs35("defs", { children: [
13013
+ /* @__PURE__ */ jsx42("filter", { id: filterId, x: "-20%", y: "-20%", width: "140%", height: "140%", children: /* @__PURE__ */ jsx42("feGaussianBlur", { in: "SourceGraphic", stdDeviation: feather / 4 }) }),
13014
+ /* @__PURE__ */ jsxs35("mask", { id: maskId, children: [
13015
+ /* @__PURE__ */ jsx42("rect", { x: 0, y: 0, width: page.width, height: page.height, fill: "white" }),
13016
+ action.shape === "ellipse" ? /* @__PURE__ */ jsx42(
13017
+ "ellipse",
13018
+ {
13019
+ cx: (x1 + x2) / 2,
13020
+ cy: (y1 + y2) / 2,
13021
+ rx: w / 2,
13022
+ ry: h / 2,
13023
+ fill: "black",
13024
+ filter: `url(#${filterId})`
13025
+ }
13026
+ ) : /* @__PURE__ */ jsx42(
13027
+ "rect",
13028
+ {
13029
+ x: x1,
13030
+ y: y1,
13031
+ width: w,
13032
+ height: h,
13033
+ rx,
13034
+ ry,
13035
+ fill: "black",
13036
+ filter: `url(#${filterId})`
13037
+ }
13038
+ )
13039
+ ] })
13040
+ ] }),
13041
+ /* @__PURE__ */ jsx42(
13042
+ motion2.rect,
13043
+ {
13044
+ x: 0,
13045
+ y: 0,
13046
+ width: page.width,
13047
+ height: page.height,
13048
+ fill: "black",
13049
+ mask: `url(#${maskId})`,
13050
+ initial: { fillOpacity: 0 },
13051
+ animate: { fillOpacity: action.dim_opacity },
13052
+ exit: { fillOpacity: 0 },
13053
+ transition: { duration: durationMs / 1e3, ease: "easeOut" }
13054
+ }
13055
+ )
13056
+ ]
13057
+ }
13058
+ );
13059
+ }
13060
+
13061
+ // src/components/TutorMode/AnimatedUnderline.tsx
13062
+ import { motion as motion3 } from "framer-motion";
13063
+ import { jsx as jsx43 } from "react/jsx-runtime";
13064
+ function pathForStyle(x1, x2, y, style) {
13065
+ if (style === "straight") return `M ${x1} ${y} L ${x2} ${y}`;
13066
+ if (style === "double")
13067
+ return `M ${x1} ${y - 3} L ${x2} ${y - 3} M ${x1} ${y + 3} L ${x2} ${y + 3}`;
13068
+ if (style === "wavy") {
13069
+ const steps = Math.max(8, Math.floor((x2 - x1) / 18));
13070
+ let d2 = `M ${x1} ${y}`;
13071
+ for (let i = 1; i <= steps; i++) {
13072
+ const px = x1 + (x2 - x1) * i / steps;
13073
+ const dy = i % 2 === 0 ? 4 : -4;
13074
+ d2 += ` Q ${px - (x2 - x1) / (2 * steps)} ${y + dy} ${px} ${y}`;
13075
+ }
13076
+ return d2;
13077
+ }
13078
+ const segs = 6;
13079
+ let d = `M ${x1} ${y}`;
13080
+ for (let i = 1; i <= segs; i++) {
13081
+ const px = x1 + (x2 - x1) * i / segs;
13082
+ const jitter = (Math.random() - 0.5) * 4;
13083
+ d += ` L ${px} ${y + jitter}`;
13084
+ }
13085
+ return d;
13086
+ }
13087
+ function AnimatedUnderline({ bbox, action }) {
13088
+ const [x1, , x2, y2] = bbox;
13089
+ const y = y2 + 6;
13090
+ const d = pathForStyle(x1, x2, y, action.style);
13091
+ const duration = action.draw_duration_ms / 1e3;
13092
+ return /* @__PURE__ */ jsx43(
13093
+ "svg",
13094
+ {
13095
+ style: {
13096
+ position: "absolute",
13097
+ inset: 0,
13098
+ pointerEvents: "none",
13099
+ overflow: "visible"
13100
+ },
13101
+ "data-role": "underline",
13102
+ children: /* @__PURE__ */ jsx43(
13103
+ motion3.path,
13104
+ {
13105
+ d,
13106
+ fill: "none",
13107
+ stroke: action.color,
13108
+ strokeWidth: 4,
13109
+ strokeLinecap: "round",
13110
+ initial: { pathLength: 0, opacity: 0 },
13111
+ animate: { pathLength: 1, opacity: 1 },
13112
+ exit: { opacity: 0 },
13113
+ transition: { duration, ease: "easeOut" }
13114
+ }
13115
+ )
13116
+ }
13117
+ );
13118
+ }
13119
+
13120
+ // src/components/TutorMode/AnimatedHighlight.tsx
13121
+ import { motion as motion4 } from "framer-motion";
13122
+ import { jsx as jsx44 } from "react/jsx-runtime";
13123
+ function AnimatedHighlight({ bbox, action }) {
13124
+ const [x1, y1, x2, y2] = bbox;
13125
+ const w = x2 - x1;
13126
+ const h = y2 - y1;
13127
+ return /* @__PURE__ */ jsx44(
13128
+ motion4.div,
13129
+ {
13130
+ style: {
13131
+ position: "absolute",
13132
+ left: x1,
13133
+ top: y1,
13134
+ height: h,
13135
+ background: action.color,
13136
+ borderRadius: 4,
13137
+ mixBlendMode: "multiply",
13138
+ transformOrigin: "0% 50%",
13139
+ pointerEvents: "none"
13140
+ },
13141
+ initial: { width: 0, opacity: 0.9 },
13142
+ animate: { width: w, opacity: 0.9 },
13143
+ exit: { opacity: 0 },
13144
+ transition: { duration: action.draw_duration_ms / 1e3, ease: "easeOut" },
13145
+ "data-role": "highlight"
13146
+ }
13147
+ );
13148
+ }
13149
+
13150
+ // src/components/TutorMode/PulseOverlay.tsx
13151
+ import { motion as motion5 } from "framer-motion";
13152
+ import { jsx as jsx45 } from "react/jsx-runtime";
13153
+ var INTENSITY = {
13154
+ subtle: { scale: 1.02, border: "2px solid rgba(59,130,246,0.6)" },
13155
+ normal: { scale: 1.05, border: "3px solid rgba(59,130,246,0.8)" },
13156
+ strong: { scale: 1.1, border: "4px solid rgba(59,130,246,1.0)" }
13157
+ };
13158
+ function PulseOverlay({ bbox, action }) {
13159
+ const [x1, y1, x2, y2] = bbox;
13160
+ const { scale, border } = INTENSITY[action.intensity];
13161
+ const repeat = action.count === 1 ? 0 : action.count - 1;
13162
+ return /* @__PURE__ */ jsx45(
13163
+ motion5.div,
13164
+ {
13165
+ style: {
13166
+ position: "absolute",
13167
+ left: x1,
13168
+ top: y1,
13169
+ width: x2 - x1,
13170
+ height: y2 - y1,
13171
+ border,
13172
+ borderRadius: 8,
13173
+ pointerEvents: "none",
13174
+ boxSizing: "border-box"
13175
+ },
13176
+ animate: { scale: [1, scale, 1] },
13177
+ transition: {
13178
+ duration: 1.2,
13179
+ times: [0, 0.5, 1],
13180
+ ease: "easeInOut",
13181
+ repeat,
13182
+ repeatType: "loop"
13183
+ },
13184
+ exit: { opacity: 0 },
13185
+ "data-role": "pulse"
13186
+ }
13187
+ );
13188
+ }
13189
+
13190
+ // src/components/TutorMode/CalloutArrow.tsx
13191
+ import { motion as motion6 } from "framer-motion";
13192
+ import { jsx as jsx46, jsxs as jsxs36 } from "react/jsx-runtime";
13193
+ function centerOf(b) {
13194
+ return { x: (b[0] + b[2]) / 2, y: (b[1] + b[3]) / 2 };
13195
+ }
13196
+ function arrowPath(fromBbox, toBbox, curve) {
13197
+ const a = centerOf(fromBbox);
13198
+ const b = centerOf(toBbox);
13199
+ if (curve === "straight") return `M ${a.x} ${a.y} L ${b.x} ${b.y}`;
13200
+ if (curve === "zigzag") {
13201
+ const mx = (a.x + b.x) / 2;
13202
+ return `M ${a.x} ${a.y} L ${mx} ${a.y} L ${mx} ${b.y} L ${b.x} ${b.y}`;
13203
+ }
13204
+ const dx = b.x - a.x;
13205
+ const dy = b.y - a.y;
13206
+ const cx = (a.x + b.x) / 2 - dy * 0.25;
13207
+ const cy = (a.y + b.y) / 2 + dx * 0.25;
13208
+ return `M ${a.x} ${a.y} Q ${cx} ${cy} ${b.x} ${b.y}`;
13209
+ }
13210
+ function CalloutArrow({ fromBbox, toBbox, action }) {
13211
+ const d = arrowPath(fromBbox, toBbox, action.curve);
13212
+ const label = action.label;
13213
+ const target = centerOf(toBbox);
13214
+ return /* @__PURE__ */ jsxs36(
13215
+ "svg",
13216
+ {
13217
+ style: {
13218
+ position: "absolute",
13219
+ inset: 0,
13220
+ pointerEvents: "none",
13221
+ overflow: "visible"
13222
+ },
13223
+ "data-role": "callout",
13224
+ children: [
13225
+ /* @__PURE__ */ jsx46("defs", { children: /* @__PURE__ */ jsx46(
13226
+ "marker",
13227
+ {
13228
+ id: "arrowhead",
13229
+ viewBox: "0 0 10 10",
13230
+ refX: "8",
13231
+ refY: "5",
13232
+ markerWidth: "8",
13233
+ markerHeight: "8",
13234
+ orient: "auto",
13235
+ children: /* @__PURE__ */ jsx46("path", { d: "M 0 0 L 10 5 L 0 10 z", fill: "#3B82F6" })
13236
+ }
13237
+ ) }),
13238
+ /* @__PURE__ */ jsx46(
13239
+ motion6.path,
13240
+ {
13241
+ d,
13242
+ fill: "none",
13243
+ stroke: "#3B82F6",
13244
+ strokeWidth: 3,
13245
+ strokeLinecap: "round",
13246
+ markerEnd: "url(#arrowhead)",
13247
+ initial: { pathLength: 0, opacity: 0 },
13248
+ animate: { pathLength: 1, opacity: 1 },
13249
+ exit: { opacity: 0 },
13250
+ transition: { duration: 0.6, ease: "easeOut" }
13251
+ }
13252
+ ),
13253
+ label ? /* @__PURE__ */ jsxs36(
13254
+ motion6.g,
13255
+ {
13256
+ initial: { opacity: 0 },
13257
+ animate: { opacity: 1 },
13258
+ exit: { opacity: 0 },
13259
+ transition: { delay: 0.3, duration: 0.3 },
13260
+ children: [
13261
+ /* @__PURE__ */ jsx46(
13262
+ "rect",
13263
+ {
13264
+ x: target.x - 4,
13265
+ y: target.y - 28,
13266
+ width: label.length * 9 + 12,
13267
+ height: 22,
13268
+ rx: 4,
13269
+ fill: "#1F2937"
13270
+ }
13271
+ ),
13272
+ /* @__PURE__ */ jsx46(
13273
+ "text",
13274
+ {
13275
+ x: target.x + 2,
13276
+ y: target.y - 12,
13277
+ fill: "white",
13278
+ fontSize: 14,
13279
+ fontFamily: "system-ui, sans-serif",
13280
+ children: label
13281
+ }
13282
+ )
13283
+ ]
13284
+ }
13285
+ ) : null
13286
+ ]
13287
+ }
13288
+ );
13289
+ }
13290
+
13291
+ // src/components/TutorMode/GhostReference.tsx
13292
+ import { motion as motion7 } from "framer-motion";
13293
+ import { jsx as jsx47, jsxs as jsxs37 } from "react/jsx-runtime";
13294
+ var POSITIONS = {
13295
+ "top-right": { top: 40, right: 40 },
13296
+ "top-left": { top: 40, left: 40 },
13297
+ "bottom-right": { bottom: 40, right: 40 },
13298
+ "bottom-left": { bottom: 40, left: 40 }
13299
+ };
13300
+ function GhostReference({
13301
+ page,
13302
+ sourceBbox,
13303
+ sourceBlockText,
13304
+ sourcePageNumber,
13305
+ action
13306
+ }) {
13307
+ const width = 360;
13308
+ const [x1, y1, x2, y2] = sourceBbox;
13309
+ return /* @__PURE__ */ jsxs37(
13310
+ motion7.div,
13311
+ {
13312
+ initial: { opacity: 0, y: 20, scale: 0.95 },
13313
+ animate: { opacity: 1, y: 0, scale: 1 },
13314
+ exit: { opacity: 0, y: 20, scale: 0.95 },
13315
+ transition: { duration: 0.4, ease: "easeOut" },
13316
+ style: {
13317
+ position: "absolute",
13318
+ width,
13319
+ background: "#111",
13320
+ color: "white",
13321
+ borderRadius: 12,
13322
+ padding: 12,
13323
+ boxShadow: "0 10px 40px rgba(0,0,0,0.5)",
13324
+ pointerEvents: "none",
13325
+ fontFamily: "system-ui, sans-serif",
13326
+ fontSize: 13,
13327
+ ...POSITIONS[action.position]
13328
+ },
13329
+ "data-role": "ghost-reference",
13330
+ children: [
13331
+ /* @__PURE__ */ jsxs37("div", { style: { opacity: 0.7, fontSize: 11, marginBottom: 6 }, children: [
13332
+ "Page ",
13333
+ sourcePageNumber,
13334
+ " \u2014 ",
13335
+ action.target_block
13336
+ ] }),
13337
+ /* @__PURE__ */ jsxs37(
13338
+ "svg",
13339
+ {
13340
+ width: width - 24,
13341
+ height: 160,
13342
+ viewBox: `0 0 ${page.width} ${page.height}`,
13343
+ style: { background: "#1F2937", borderRadius: 6, display: "block" },
13344
+ preserveAspectRatio: "xMidYMid meet",
13345
+ children: [
13346
+ /* @__PURE__ */ jsx47(
13347
+ "rect",
13348
+ {
13349
+ x: 0,
13350
+ y: 0,
13351
+ width: page.width,
13352
+ height: page.height,
13353
+ fill: "#1F2937"
13354
+ }
13355
+ ),
13356
+ /* @__PURE__ */ jsx47(
13357
+ "rect",
13358
+ {
13359
+ x: x1,
13360
+ y: y1,
13361
+ width: x2 - x1,
13362
+ height: y2 - y1,
13363
+ fill: "rgba(250,204,21,0.45)",
13364
+ stroke: "#FBBF24",
13365
+ strokeWidth: 8
13366
+ }
13367
+ )
13368
+ ]
13369
+ }
13370
+ ),
13371
+ /* @__PURE__ */ jsx47(
13372
+ "div",
13373
+ {
13374
+ style: {
13375
+ marginTop: 8,
13376
+ fontSize: 12,
13377
+ lineHeight: 1.4,
13378
+ opacity: 0.9
13379
+ },
13380
+ children: sourceBlockText ?? "(figure)"
13381
+ }
13382
+ )
13383
+ ]
13384
+ }
13385
+ );
13386
+ }
13387
+
13388
+ // src/components/TutorMode/BoxOverlay.tsx
13389
+ import { motion as motion8 } from "framer-motion";
13390
+ import { jsx as jsx48 } from "react/jsx-runtime";
13391
+ function BoxOverlay({ bbox, action }) {
13392
+ const [x1, y1, x2, y2] = bbox;
13393
+ return /* @__PURE__ */ jsx48(
13394
+ motion8.div,
13395
+ {
13396
+ initial: { opacity: 0, scale: 0.97 },
13397
+ animate: { opacity: 1, scale: 1 },
13398
+ exit: { opacity: 0 },
13399
+ transition: { duration: 0.35, ease: "easeOut" },
13400
+ style: {
13401
+ position: "absolute",
13402
+ left: x1,
13403
+ top: y1,
13404
+ width: x2 - x1,
13405
+ height: y2 - y1,
13406
+ border: `${action.style === "dashed" ? "3px dashed" : "3px solid"} ${action.color}`,
13407
+ borderRadius: 6,
13408
+ pointerEvents: "none",
13409
+ boxSizing: "border-box"
13410
+ },
13411
+ "data-role": "box"
13412
+ }
13413
+ );
13414
+ }
13415
+
13416
+ // src/components/TutorMode/StickyLabel.tsx
13417
+ import { motion as motion9 } from "framer-motion";
13418
+ import { jsx as jsx49 } from "react/jsx-runtime";
13419
+ function position(bbox, where) {
13420
+ const [x1, y1, x2, y2] = bbox;
13421
+ const cx = (x1 + x2) / 2;
13422
+ const cy = (y1 + y2) / 2;
13423
+ const PAD = 16;
13424
+ switch (where) {
13425
+ case "top":
13426
+ return { left: cx, top: y1 - PAD, transform: "translate(-50%, -100%)" };
13427
+ case "bottom":
13428
+ return { left: cx, top: y2 + PAD, transform: "translate(-50%, 0)" };
13429
+ case "left":
13430
+ return { left: x1 - PAD, top: cy, transform: "translate(-100%, -50%)" };
13431
+ case "right":
13432
+ return { left: x2 + PAD, top: cy, transform: "translate(0, -50%)" };
13433
+ default:
13434
+ return { left: cx, top: y1, transform: "translate(-50%, -100%)" };
13435
+ }
13436
+ }
13437
+ function StickyLabel({ bbox, action }) {
13438
+ return /* @__PURE__ */ jsx49(
13439
+ motion9.div,
13440
+ {
13441
+ initial: { opacity: 0, scale: 0.9 },
13442
+ animate: { opacity: 1, scale: 1 },
13443
+ exit: { opacity: 0 },
13444
+ transition: { duration: 0.35, ease: "easeOut" },
13445
+ style: {
13446
+ position: "absolute",
13447
+ padding: "6px 10px",
13448
+ background: "#FEF3C7",
13449
+ color: "#78350F",
13450
+ borderRadius: 6,
13451
+ boxShadow: "0 3px 10px rgba(0,0,0,0.2)",
13452
+ fontSize: 14,
13453
+ fontFamily: "system-ui, sans-serif",
13454
+ maxWidth: 280,
13455
+ pointerEvents: "none",
13456
+ ...position(bbox, action.position)
13457
+ },
13458
+ "data-role": "label",
13459
+ children: action.text
13460
+ }
13461
+ );
13462
+ }
13463
+
13464
+ // src/components/TutorMode/CinemaLayer.tsx
13465
+ import { jsx as jsx50 } from "react/jsx-runtime";
13466
+ function blockBbox(index, block_id) {
13467
+ return index.blockById.get(block_id)?.block.bbox;
13468
+ }
13469
+ function CinemaLayer({
13470
+ page,
13471
+ index,
13472
+ overlays,
13473
+ scale
13474
+ }) {
13475
+ return /* @__PURE__ */ jsx50(
13476
+ "div",
13477
+ {
13478
+ "data-role": "cinema-layer",
13479
+ style: {
13480
+ position: "absolute",
13481
+ inset: 0,
13482
+ transformOrigin: "0 0",
13483
+ transform: `scale(${scale})`,
13484
+ width: page.page_dimensions.width,
13485
+ height: page.page_dimensions.height,
13486
+ pointerEvents: "none",
13487
+ // PDFPage renders internal layers at z-index 10/20/40/45/50
13488
+ // (canvas / text / highlight / focus / annotation). Without an
13489
+ // explicit z-index here, every tutor overlay stacks UNDER the
13490
+ // AnnotationLayer and becomes invisible. 100 puts us above all of
13491
+ // them while still letting the Exit button (z-index 60) remain
13492
+ // reachable because it sits OUTSIDE this stacking context.
13493
+ zIndex: 100
13494
+ },
13495
+ children: /* @__PURE__ */ jsx50(AnimatePresence, { children: overlays.map((overlay) => {
13496
+ switch (overlay.kind) {
13497
+ case "spotlight": {
13498
+ const a = overlay.action;
13499
+ const b = blockBbox(index, a.target_block);
13500
+ if (!b) return null;
13501
+ return /* @__PURE__ */ jsx50(
13502
+ SpotlightMask,
13503
+ {
13504
+ page: page.page_dimensions,
13505
+ bbox: b,
13506
+ action: a
13507
+ },
13508
+ overlay.id
13509
+ );
13510
+ }
13511
+ case "underline": {
13512
+ const a = overlay.action;
13513
+ const b = blockBbox(index, a.target_block);
13514
+ if (!b) return null;
13515
+ return /* @__PURE__ */ jsx50(AnimatedUnderline, { bbox: b, action: a }, overlay.id);
13516
+ }
13517
+ case "highlight": {
13518
+ const a = overlay.action;
13519
+ const b = blockBbox(index, a.target_block);
13520
+ if (!b) return null;
13521
+ return /* @__PURE__ */ jsx50(AnimatedHighlight, { bbox: b, action: a }, overlay.id);
13522
+ }
13523
+ case "pulse": {
13524
+ const a = overlay.action;
13525
+ const b = blockBbox(index, a.target_block);
13526
+ if (!b) return null;
13527
+ return /* @__PURE__ */ jsx50(PulseOverlay, { bbox: b, action: a }, overlay.id);
13528
+ }
13529
+ case "callout": {
13530
+ const a = overlay.action;
13531
+ const from = blockBbox(index, a.from_block);
13532
+ const to = blockBbox(index, a.to_block);
13533
+ if (!from || !to) return null;
13534
+ return /* @__PURE__ */ jsx50(
13535
+ CalloutArrow,
13536
+ {
13537
+ fromBbox: from,
13538
+ toBbox: to,
13539
+ action: a
13540
+ },
13541
+ overlay.id
13542
+ );
13543
+ }
13544
+ case "ghost_reference": {
13545
+ const a = overlay.action;
13546
+ const hit = index.blockById.get(a.target_block);
13547
+ if (!hit) return null;
13548
+ const targetPage = index.byPage.get(a.target_page);
13549
+ if (!targetPage) return null;
13550
+ return /* @__PURE__ */ jsx50(
13551
+ GhostReference,
13552
+ {
13553
+ page: targetPage.page_dimensions,
13554
+ sourceBbox: hit.block.bbox,
13555
+ sourceBlockText: hit.block.text,
13556
+ sourcePageNumber: hit.pageNumber,
13557
+ action: a
13558
+ },
13559
+ overlay.id
13560
+ );
13561
+ }
13562
+ case "box": {
13563
+ const a = overlay.action;
13564
+ const b = blockBbox(index, a.target_block);
13565
+ if (!b) return null;
13566
+ return /* @__PURE__ */ jsx50(BoxOverlay, { bbox: b, action: a }, overlay.id);
13567
+ }
13568
+ case "label": {
13569
+ const a = overlay.action;
13570
+ const b = blockBbox(index, a.target_block);
13571
+ if (!b) return null;
13572
+ return /* @__PURE__ */ jsx50(StickyLabel, { bbox: b, action: a }, overlay.id);
13573
+ }
13574
+ case "clear":
13575
+ case "camera":
13576
+ return null;
13577
+ }
13578
+ }) })
13579
+ }
13580
+ );
13581
+ }
13582
+
13583
+ // src/components/TutorMode/SubtitleBar.tsx
13584
+ import { AnimatePresence as AnimatePresence2, motion as motion10 } from "framer-motion";
13585
+ import { jsx as jsx51 } from "react/jsx-runtime";
13586
+ function SubtitleBar({ text }) {
13587
+ return /* @__PURE__ */ jsx51(AnimatePresence2, { children: text ? /* @__PURE__ */ jsx51(
13588
+ motion10.div,
13589
+ {
13590
+ initial: { opacity: 0, y: 20 },
13591
+ animate: { opacity: 1, y: 0 },
13592
+ exit: { opacity: 0, y: 20 },
13593
+ transition: { duration: 0.3 },
13594
+ style: {
13595
+ position: "absolute",
13596
+ left: "50%",
13597
+ bottom: 32,
13598
+ transform: "translateX(-50%)",
13599
+ background: "rgba(0,0,0,0.75)",
13600
+ color: "white",
13601
+ padding: "10px 18px",
13602
+ borderRadius: 8,
13603
+ maxWidth: "80%",
13604
+ fontSize: 16,
13605
+ lineHeight: 1.4,
13606
+ fontFamily: "system-ui, sans-serif",
13607
+ pointerEvents: "none",
13608
+ zIndex: 50,
13609
+ textAlign: "center"
13610
+ },
13611
+ "data-role": "subtitle-bar",
13612
+ children: text
13613
+ },
13614
+ text
13615
+ ) : null });
13616
+ }
13617
+
13618
+ // src/director/storyboard-engine.ts
13619
+ init_narration_store();
13620
+ init_camera_math();
13621
+ var DEFAULT_MIN_OVERLAY_MS = 3500;
13622
+ var StoryboardEngine = class {
13623
+ constructor(deps) {
13624
+ this.pendingTimers = /* @__PURE__ */ new Set();
13625
+ this.currentStoryboardId = 0;
13626
+ this.deps = deps;
13627
+ }
13628
+ /**
13629
+ * Execute a new storyboard. Cancels in-flight steps from the previous storyboard
13630
+ * and smoothly transitions the camera/overlays from the current state.
13631
+ */
13632
+ execute(storyboard) {
13633
+ this.cancelPending();
13634
+ this.currentStoryboardId += 1;
13635
+ const storyboardId = this.currentStoryboardId;
13636
+ const { narrationStore } = this.deps;
13637
+ narrationStore.getState().setEngineStatus("transitioning");
13638
+ narrationStore.getState().setLastStoryboard(storyboard);
13639
+ let steps = [...storyboard.steps].sort((a, b) => a.at_ms - b.at_ms);
13640
+ const hasCamera = steps.some((s) => s.action.type === "camera");
13641
+ if (!hasCamera) {
13642
+ const focus = steps.find(
13643
+ (s) => s.action.type !== "clear" && "target_block" in s.action && s.action.target_block
13644
+ );
13645
+ if (focus && focus.action.type !== "clear" && "target_block" in focus.action) {
13646
+ steps = [
13647
+ {
13648
+ at_ms: 0,
13649
+ duration_ms: 700,
13650
+ action: {
13651
+ type: "camera",
13652
+ target_block: focus.action.target_block,
13653
+ scale: 1,
13654
+ padding: 60,
13655
+ easing: "ease-out"
13656
+ }
13657
+ },
13658
+ ...steps
13659
+ ];
13660
+ }
13661
+ }
13662
+ for (const step of steps) {
13663
+ const timer = setTimeout(() => {
13664
+ if (storyboardId !== this.currentStoryboardId) return;
13665
+ this.runStep(step);
13666
+ }, step.at_ms);
13667
+ this.pendingTimers.add(timer);
13668
+ }
13669
+ const markExecuting = setTimeout(() => {
13670
+ if (storyboardId !== this.currentStoryboardId) return;
13671
+ narrationStore.getState().setEngineStatus("executing");
13672
+ }, 0);
13673
+ this.pendingTimers.add(markExecuting);
13674
+ const last = steps[steps.length - 1];
13675
+ if (last) {
13676
+ const totalMs = last.at_ms + last.duration_ms;
13677
+ const markIdle = setTimeout(() => {
13678
+ if (storyboardId !== this.currentStoryboardId) return;
13679
+ narrationStore.getState().setEngineStatus("idle");
13680
+ }, totalMs + 50);
13681
+ this.pendingTimers.add(markIdle);
13682
+ }
13683
+ }
13684
+ /** Abort all pending steps and set engine status to idle. */
13685
+ cancelPending() {
13686
+ for (const t of this.pendingTimers) clearTimeout(t);
13687
+ this.pendingTimers.clear();
13688
+ this.deps.narrationStore.getState().setEngineStatus("idle");
13689
+ }
13690
+ /** Reset visuals: clear overlays, fit camera back to page. */
13691
+ resetVisuals() {
13692
+ this.cancelPending();
13693
+ const { narrationStore, bboxIndex, getViewport } = this.deps;
13694
+ narrationStore.getState().clearOverlays();
13695
+ const viewport = getViewport();
13696
+ const currentPage = narrationStore.getState().currentPage;
13697
+ const pageDims = bboxIndex.byPage.get(currentPage);
13698
+ const fit = pageDims && viewport.width > 0 && viewport.height > 0 ? fitPageScale(pageDims.page_dimensions, viewport) * 0.95 : 1;
13699
+ narrationStore.getState().setCamera({ scale: fit, x: 0, y: 0, easing: "ease-in-out" });
13700
+ }
13701
+ /** Execute one step — dispatch to narrationStore. Returns true if applied. */
13702
+ runStep(step) {
13703
+ const action = step.action;
13704
+ const { narrationStore, bboxIndex } = this.deps;
13705
+ if ("target_block" in action && action.target_block) {
13706
+ if (!bboxIndex.blockById.has(action.target_block)) {
13707
+ narrationStore.getState().appendDebugEvent({
13708
+ kind: "llm-error",
13709
+ summary: `dropped ${action.type} step \u2192 unknown target_block "${action.target_block}"`,
13710
+ payload: { action, validIds: [...bboxIndex.blockById.keys()] }
13711
+ });
13712
+ return false;
13713
+ }
13714
+ }
13715
+ if ("from_block" in action && action.from_block) {
13716
+ if (!bboxIndex.blockById.has(action.from_block)) {
13717
+ narrationStore.getState().appendDebugEvent({
13718
+ kind: "llm-error",
13719
+ summary: `dropped ${action.type} step \u2192 unknown from_block "${action.from_block}"`,
13720
+ payload: { action }
13721
+ });
13722
+ return false;
13723
+ }
13724
+ }
13725
+ if ("to_block" in action && action.to_block) {
13726
+ if (!bboxIndex.blockById.has(action.to_block)) {
13727
+ narrationStore.getState().appendDebugEvent({
13728
+ kind: "llm-error",
13729
+ summary: `dropped ${action.type} step \u2192 unknown to_block "${action.to_block}"`,
13730
+ payload: { action }
13731
+ });
13732
+ return false;
13733
+ }
13734
+ }
13735
+ if (action.type === "camera") {
13736
+ this.applyCamera(action, step.duration_ms);
13737
+ return true;
13738
+ }
13739
+ if (action.type === "clear") {
13740
+ const targets = action.targets;
13741
+ if (targets === "all" || targets === "overlays") {
13742
+ narrationStore.getState().clearOverlays();
13743
+ } else if (targets === "spotlights") {
13744
+ narrationStore.getState().clearOverlays((o) => o.kind === "spotlight");
13745
+ } else if (Array.isArray(targets)) {
13746
+ const ids = new Set(targets);
13747
+ narrationStore.getState().clearOverlays((o) => ids.has(o.id));
13748
+ }
13749
+ return true;
13750
+ }
13751
+ const minMs = this.deps.minOverlayDurationMs ?? DEFAULT_MIN_OVERLAY_MS;
13752
+ const visibleMs = Math.max(step.duration_ms, minMs);
13753
+ const overlay = {
13754
+ id: makeOverlayId(action),
13755
+ kind: action.type,
13756
+ action,
13757
+ createdAt: Date.now(),
13758
+ expiresAt: Date.now() + visibleMs
13759
+ };
13760
+ narrationStore.getState().addOverlay(overlay);
13761
+ const timer = setTimeout(() => {
13762
+ narrationStore.getState().removeOverlay(overlay.id);
13763
+ }, visibleMs);
13764
+ this.pendingTimers.add(timer);
13765
+ return true;
13766
+ }
13767
+ applyCamera(action, durationMs) {
13768
+ const { narrationStore, bboxIndex, getViewport } = this.deps;
13769
+ const viewport = getViewport();
13770
+ let bbox = action.target_bbox;
13771
+ let pageDims = void 0;
13772
+ if (!bbox && action.target_block) {
13773
+ const hit = bboxIndex.blockById.get(action.target_block);
13774
+ if (!hit) return;
13775
+ bbox = hit.block.bbox;
13776
+ pageDims = bboxIndex.byPage.get(hit.pageNumber);
13777
+ } else if (bbox) {
13778
+ pageDims = bboxIndex.byPage.get(narrationStore.getState().currentPage);
13779
+ }
13780
+ if (!bbox || !pageDims) return;
13781
+ const fit = fitPageScale(pageDims.page_dimensions, viewport);
13782
+ const requested = Math.max(0.5, Math.min(3, action.scale ?? 1));
13783
+ const finalScale = fit * requested;
13784
+ const [x1, y1, x2, y2] = bbox;
13785
+ const blockCX = (x1 + x2) / 2;
13786
+ const blockCY = (y1 + y2) / 2;
13787
+ const pageCX = pageDims.page_dimensions.width / 2;
13788
+ const pageCY = pageDims.page_dimensions.height / 2;
13789
+ const x = (pageCX - blockCX) * finalScale;
13790
+ const y = (pageCY - blockCY) * finalScale;
13791
+ const camera = {
13792
+ scale: finalScale,
13793
+ x,
13794
+ y,
13795
+ easing: action.easing
13796
+ };
13797
+ narrationStore.getState().setCamera(camera);
13798
+ void durationMs;
13799
+ void computeCameraForBlock;
13800
+ }
13801
+ };
13802
+
13803
+ // src/director/storyboard-schema.ts
13804
+ import { z } from "zod";
13805
+ var BBoxCoordsSchema = z.tuple([z.number(), z.number(), z.number(), z.number()]);
13806
+ var CameraSchema = z.object({
13807
+ type: z.literal("camera"),
13808
+ target_block: z.string().optional(),
13809
+ target_bbox: BBoxCoordsSchema.optional(),
13810
+ scale: z.number().min(0.5).max(4).default(1),
13811
+ padding: z.number().min(0).max(400).default(80),
13812
+ easing: z.enum(["linear", "ease-in", "ease-out", "ease-in-out"]).default("ease-in-out")
13813
+ }).refine((a) => !!a.target_block || !!a.target_bbox, {
13814
+ message: "camera requires target_block or target_bbox"
13815
+ });
13816
+ var SpotlightSchema = z.object({
13817
+ type: z.literal("spotlight"),
13818
+ target_block: z.string(),
13819
+ dim_opacity: z.number().min(0).max(1).default(0.65),
13820
+ feather_px: z.number().min(0).max(200).default(40),
13821
+ shape: z.enum(["rect", "rounded", "ellipse"]).default("rounded")
13822
+ });
13823
+ var UnderlineSchema = z.object({
13824
+ type: z.literal("underline"),
13825
+ target_block: z.string(),
13826
+ color: z.string().default("#FBBF24"),
13827
+ style: z.enum(["straight", "sketch", "double", "wavy"]).default("sketch"),
13828
+ draw_duration_ms: z.number().min(100).max(3e3).default(600)
13829
+ });
13830
+ var HighlightSchema = z.object({
13831
+ type: z.literal("highlight"),
13832
+ target_block: z.string(),
13833
+ color: z.string().default("rgba(250, 204, 21, 0.35)"),
13834
+ draw_duration_ms: z.number().min(100).max(3e3).default(500)
13835
+ });
13836
+ var PulseSchema = z.object({
13837
+ type: z.literal("pulse"),
13838
+ target_block: z.string(),
13839
+ count: z.number().int().min(1).max(5).default(2),
13840
+ intensity: z.enum(["subtle", "normal", "strong"]).default("normal")
13841
+ });
13842
+ var CalloutSchema = z.object({
13843
+ type: z.literal("callout"),
13844
+ from_block: z.string(),
13845
+ to_block: z.string(),
13846
+ label: z.string().max(120).optional(),
13847
+ curve: z.enum(["straight", "curved", "zigzag"]).default("curved")
13848
+ });
13849
+ var GhostReferenceSchema = z.object({
13850
+ type: z.literal("ghost_reference"),
13851
+ target_page: z.number().int().min(1),
13852
+ target_block: z.string(),
13853
+ position: z.enum(["top-right", "top-left", "bottom-right", "bottom-left"]).default("top-right")
13854
+ });
13855
+ var BoxSchema = z.object({
13856
+ type: z.literal("box"),
13857
+ target_block: z.string(),
13858
+ color: z.string().default("#3B82F6"),
13859
+ style: z.enum(["solid", "dashed"]).default("solid")
13860
+ });
13861
+ var LabelSchema = z.object({
13862
+ type: z.literal("label"),
13863
+ target_block: z.string(),
13864
+ text: z.string().min(1).max(120),
13865
+ position: z.enum(["top", "bottom", "left", "right"]).default("top")
13866
+ });
13867
+ var ClearSchema = z.object({
13868
+ type: z.literal("clear"),
13869
+ targets: z.union([z.enum(["all", "spotlights", "overlays"]), z.array(z.string())]).default("overlays")
13870
+ });
13871
+ var StoryboardActionSchema = z.union([
13872
+ CameraSchema,
13873
+ SpotlightSchema,
13874
+ UnderlineSchema,
13875
+ HighlightSchema,
13876
+ PulseSchema,
13877
+ CalloutSchema,
13878
+ GhostReferenceSchema,
13879
+ BoxSchema,
13880
+ LabelSchema,
13881
+ ClearSchema
13882
+ ]);
13883
+ var StoryboardStepSchema = z.object({
13884
+ at_ms: z.number().min(0).max(5e3).default(0),
13885
+ duration_ms: z.number().min(100).max(5e3).default(800),
13886
+ action: StoryboardActionSchema
13887
+ });
13888
+ var StoryboardSchema = z.object({
13889
+ version: z.literal(1),
13890
+ reasoning: z.string().max(500).default(""),
13891
+ steps: z.array(StoryboardStepSchema).min(1).max(4)
13892
+ });
13893
+ function storyboardJsonSchema(opts = {}) {
13894
+ const { validBlockIds, validCrossPageBlockIds } = opts;
13895
+ const blockIdSchema = validBlockIds && validBlockIds.length > 0 ? { type: ["string", "null"], enum: [...validBlockIds, null] } : { type: ["string", "null"] };
13896
+ const crossPageBlockIdSchema = validCrossPageBlockIds && validCrossPageBlockIds.length > 0 ? {
13897
+ type: ["string", "null"],
13898
+ enum: [...validCrossPageBlockIds, ...validBlockIds ?? [], null]
13899
+ } : blockIdSchema;
13900
+ const actionSchema = {
13901
+ type: "object",
13902
+ additionalProperties: false,
13903
+ required: [
13904
+ "type",
13905
+ "target_block",
13906
+ "target_bbox",
13907
+ "scale",
13908
+ "padding",
13909
+ "easing",
13910
+ "dim_opacity",
13911
+ "feather_px",
13912
+ "shape",
13913
+ "color",
13914
+ "style",
13915
+ "draw_duration_ms",
13916
+ "count",
13917
+ "intensity",
13918
+ "from_block",
13919
+ "to_block",
13920
+ "label",
13921
+ "curve",
13922
+ "target_page",
13923
+ "position",
13924
+ "text",
13925
+ "targets"
13926
+ ],
13927
+ properties: {
13928
+ type: {
13929
+ type: "string",
13930
+ enum: [
13931
+ "camera",
13932
+ "spotlight",
13933
+ "underline",
13934
+ "highlight",
13935
+ "pulse",
13936
+ "callout",
13937
+ "ghost_reference",
13938
+ "box",
13939
+ "label",
13940
+ "clear"
13941
+ ]
13942
+ },
13943
+ target_block: blockIdSchema,
13944
+ target_bbox: {
13945
+ type: ["array", "null"],
13946
+ items: { type: "number" },
13947
+ minItems: 4,
13948
+ maxItems: 4
13949
+ },
13950
+ scale: { type: ["number", "null"] },
13951
+ padding: { type: ["number", "null"] },
13952
+ easing: {
13953
+ type: ["string", "null"],
13954
+ enum: ["linear", "ease-in", "ease-out", "ease-in-out", null]
13955
+ },
13956
+ dim_opacity: { type: ["number", "null"] },
13957
+ feather_px: { type: ["number", "null"] },
13958
+ shape: {
13959
+ type: ["string", "null"],
13960
+ enum: ["rect", "rounded", "ellipse", null]
13961
+ },
13962
+ color: { type: ["string", "null"] },
13963
+ style: {
13964
+ type: ["string", "null"],
13965
+ enum: ["straight", "sketch", "double", "wavy", "solid", "dashed", null]
13966
+ },
13967
+ draw_duration_ms: { type: ["number", "null"] },
13968
+ count: { type: ["integer", "null"] },
13969
+ intensity: {
13970
+ type: ["string", "null"],
13971
+ enum: ["subtle", "normal", "strong", null]
13972
+ },
13973
+ from_block: blockIdSchema,
13974
+ to_block: crossPageBlockIdSchema,
13975
+ label: { type: ["string", "null"] },
13976
+ curve: {
13977
+ type: ["string", "null"],
13978
+ enum: ["straight", "curved", "zigzag", null]
13979
+ },
13980
+ target_page: { type: ["integer", "null"] },
13981
+ position: {
13982
+ type: ["string", "null"],
13983
+ enum: [
13984
+ "top",
13985
+ "bottom",
13986
+ "left",
13987
+ "right",
13988
+ "top-right",
13989
+ "top-left",
13990
+ "bottom-right",
13991
+ "bottom-left",
13992
+ null
13993
+ ]
13994
+ },
13995
+ text: { type: ["string", "null"] },
13996
+ targets: {
13997
+ type: ["string", "null"],
13998
+ enum: ["all", "spotlights", "overlays", null]
13999
+ }
14000
+ }
14001
+ };
14002
+ return {
14003
+ type: "object",
14004
+ additionalProperties: false,
14005
+ required: ["version", "reasoning", "steps"],
14006
+ properties: {
14007
+ version: { type: "integer", enum: [1] },
14008
+ reasoning: { type: "string" },
14009
+ steps: {
14010
+ type: "array",
14011
+ minItems: 1,
14012
+ maxItems: 4,
14013
+ items: {
14014
+ type: "object",
14015
+ additionalProperties: false,
14016
+ required: ["at_ms", "duration_ms", "action"],
14017
+ properties: {
14018
+ at_ms: { type: "number" },
14019
+ duration_ms: { type: "number" },
14020
+ action: actionSchema
14021
+ }
14022
+ }
14023
+ }
14024
+ }
14025
+ };
14026
+ }
14027
+
14028
+ // src/director/prompts.ts
14029
+ var SYSTEM_PROMPT = `You are the cinematic director of an AI tutor's PDF visualization. The tutor speaks one "chunk" at a time; for each chunk you anchor the visuals onto the EXACT blocks on the page the tutor is talking about, so the reader sees the page react like a produced teaching video. Think of yourself as a motion designer layering effects on top of a document \u2014 zoom is only one tool in the kit, and often not the right one.
14030
+
14031
+ # Your primary task
14032
+ You are given a list of blocks for the current page under "Page blocks", each with a \`block_id\`, \`text\`, \`type\`, \`bbox\`, and \`default_action\`. Your #1 job is to decide WHICH block(s) the current chunk is referring to, and then pick the right visual action(s) to anchor there.
14033
+
14034
+ Anchoring rules:
14035
+ - EVERY action that references a block MUST set \`target_block\` (or \`from_block\`/\`to_block\` for callouts) to an EXISTING \`block_id\` from "Page blocks" (or "Cross-page figures index" for cross-page refs). Never invent an id. Never emit a step whose target can't be found in the provided lists.
14036
+ - Match the chunk to blocks by semantic overlap with the block's \`text\`: quoted phrases, named entities, keywords, figure references ("Fig 3.2", "the suture"), list enumerations.
14037
+ - If no block clearly matches, emit a single \`camera\` step that fits the page (no overlays) and explain in \`reasoning\`. Do NOT spray overlays onto random blocks.
14038
+ - If multiple blocks match, pick the most specific one, or use a \`callout\` from one to the other.
14039
+
14040
+ # Output shape
14041
+ Output ONLY this JSON, nothing else:
14042
+ {
14043
+ "version": 1,
14044
+ "reasoning": "<which block(s) you picked, which intent you used, and why \u2014 name the block_id>",
14045
+ "steps": [ { "at_ms": <int>, "duration_ms": <int>, "action": <action> }, ... ]
14046
+ }
14047
+
14048
+ # Action shapes \u2014 ALL fields shown are REQUIRED per action type
14049
+ - camera: { "type":"camera", "target_block":"<id>", "scale":1.1, "padding":80, "easing":"ease-out" }
14050
+ - spotlight: { "type":"spotlight", "target_block":"<id>", "dim_opacity":0.65, "feather_px":40, "shape":"rounded" }
14051
+ - underline: { "type":"underline", "target_block":"<id>", "color":"#FBBF24", "style":"sketch", "draw_duration_ms":600 }
14052
+ - highlight: { "type":"highlight", "target_block":"<id>", "color":"rgba(250,204,21,0.35)", "draw_duration_ms":500 }
14053
+ - pulse: { "type":"pulse", "target_block":"<id>", "count":2, "intensity":"normal" }
14054
+ - callout: { "type":"callout", "from_block":"<id>", "to_block":"<id>", "label":"<text>", "curve":"curved" }
14055
+ - ghost_reference: { "type":"ghost_reference", "target_page":<int>, "target_block":"<id>", "position":"top-right" }
14056
+ - box: { "type":"box", "target_block":"<id>", "color":"#3B82F6", "style":"solid" }
14057
+ - label: { "type":"label", "target_block":"<id>", "text":"<text>", "position":"top" }
14058
+ - clear: { "type":"clear", "targets":"overlays" }
14059
+
14060
+ # When to use each action (match the effect to the narration's intent, not just the block type)
14061
+ - camera \u2014 when focus SHIFTS to a new region. If the primary block is already on-screen and roughly centered, you may skip camera entirely and start with an overlay. When you do use camera, prefer scale 1.1\u20131.4 for gentle re-centering; reserve 1.5+ for dense figures you need to inspect closely.
14062
+ - spotlight \u2014 when narration ISOLATES one idea, term, or sentence. Great for definitions, principles, and "the key insight is\u2026" moments.
14063
+ - underline \u2014 when narration QUOTES a phrase or reads a line word-by-word. Use style "sketch" for handwritten feel, "straight" for formal, "wavy" for emphasis. Pairs well with spotlight.
14064
+ - highlight \u2014 when narration FLAGS a keyword inline without full focus. Cheap, fast, great for list items, definitions-in-context, callback references.
14065
+ - pulse \u2014 when narration says "notice this" / "see here" / "look at the diagram". Use with figures, icons, and anchor blocks that should catch the eye without blocking surrounding content.
14066
+ - callout \u2014 when narration CONNECTS two things on the page: a caption to its figure, a label to a region, one list item to another. Arrow implies directional meaning.
14067
+ - ghost_reference \u2014 when narration REFERS to a block on a DIFFERENT page ("as we saw on page 2\u2026"). Never use for same-page references.
14068
+ - box \u2014 when narration FRAMES a structural region: a table, a sidebar, a group of related items, a diagram subpart. Use dashed style for "in-progress" / "under discussion" regions.
14069
+ - label \u2014 when narration attaches a NAMED TAG to a block: "this is the definition", "this is an example", "step 3". Keep text \u226440 chars for readability.
14070
+ - clear \u2014 rarely needed; the engine auto-expires overlays. Use only when you explicitly want to wipe prior state before a new beat.
14071
+
14072
+ Respect each block's \`default_action\` as a soft hint, but override it freely when narration intent calls for a different effect. A paragraph labeled \`default_action: spotlight\` can absolutely take a highlight or underline if that's what the narration asks for.
14073
+
14074
+ # Intent Taxonomy \u2014 canonical "recipes" you should use as your default vocabulary
14075
+ When narration fits one of these patterns, emit the corresponding storyboard shape (fill in real block_ids from the page). You may also COMPOSE freely beyond these \u2014 they are a floor, not a ceiling.
14076
+
14077
+ ## define \u2014 the narration introduces or defines a term
14078
+ Shape: spotlight the term + underline it + drop a label tag. No camera move if the block is already on-screen.
14079
+ {
14080
+ "version": 1,
14081
+ "reasoning": "define recipe: spotlighting and underlining the term, labeling as 'definition'",
14082
+ "steps": [
14083
+ { "at_ms":0, "duration_ms":700, "action": { "type":"spotlight", "target_block":"p1_para0", "dim_opacity":0.6, "feather_px":40, "shape":"rounded" } },
14084
+ { "at_ms":200, "duration_ms":800, "action": { "type":"underline", "target_block":"p1_para0", "color":"#FBBF24", "style":"sketch", "draw_duration_ms":700 } },
14085
+ { "at_ms":900, "duration_ms":1200, "action": { "type":"label", "target_block":"p1_para0", "text":"definition", "position":"top" } }
14086
+ ]
14087
+ }
14088
+
14089
+ ## point_out \u2014 the narration directs the viewer's eye to a figure, diagram, or specific region
14090
+ Shape: gentle camera move + callout arrow from caption to figure + pulse the figure.
14091
+ {
14092
+ "version": 1,
14093
+ "reasoning": "point_out recipe: drawing attention from caption p1_cap1 to figure p1_fig0",
14094
+ "steps": [
14095
+ { "at_ms":0, "duration_ms":600, "action": { "type":"camera", "target_block":"p1_fig0", "scale":1.3, "padding":80, "easing":"ease-out" } },
14096
+ { "at_ms":400, "duration_ms":900, "action": { "type":"callout", "from_block":"p1_cap1", "to_block":"p1_fig0", "label":"see here", "curve":"curved" } },
14097
+ { "at_ms":900, "duration_ms":1200, "action": { "type":"pulse", "target_block":"p1_fig0", "count":2, "intensity":"normal" } }
14098
+ ]
14099
+ }
14100
+
14101
+ ## compare \u2014 the narration contrasts two things on the page
14102
+ Shape: box A + box B + callout between them with a relational label.
14103
+ {
14104
+ "version": 1,
14105
+ "reasoning": "compare recipe: framing fibrous vs synovial joints",
14106
+ "steps": [
14107
+ { "at_ms":0, "duration_ms":600, "action": { "type":"box", "target_block":"p1_list5", "color":"#3B82F6", "style":"solid" } },
14108
+ { "at_ms":300, "duration_ms":600, "action": { "type":"box", "target_block":"p1_list12", "color":"#F472B6", "style":"solid" } },
14109
+ { "at_ms":800, "duration_ms":1000, "action": { "type":"callout", "from_block":"p1_list5", "to_block":"p1_list12", "label":"vs", "curve":"curved" } }
14110
+ ]
14111
+ }
14112
+
14113
+ ## emphasize \u2014 the narration stresses a keyword, warning, or takeaway
14114
+ Shape: highlight + pulse. Fast, punchy, no camera.
14115
+ {
14116
+ "version": 1,
14117
+ "reasoning": "emphasize recipe: highlighting key keyword and pulsing for stress",
14118
+ "steps": [
14119
+ { "at_ms":0, "duration_ms":500, "action": { "type":"highlight", "target_block":"p1_list0", "color":"rgba(250,204,21,0.35)", "draw_duration_ms":450 } },
14120
+ { "at_ms":350, "duration_ms":800, "action": { "type":"pulse", "target_block":"p1_list0", "count":2, "intensity":"strong" } }
14121
+ ]
14122
+ }
14123
+
14124
+ # Choreography rules
14125
+ - HARD REQUIREMENT: every storyboard MUST contain at least ONE non-camera step. A lone camera step is NEVER a valid output \u2014 the viewer needs an overlay to know WHY the camera moved. If you cannot find a good overlay target, emit a \`highlight\` or \`pulse\` on your primary block as the second step.
14126
+ - Favor VARIETY that matches narration texture \u2014 a definition earns different visuals than a comparison. Don't send every chunk through the same zoom+box motion.
14127
+ - Include a camera step only when focus genuinely shifts to a new region. If the primary target is already on-screen and roughly centred, SKIP the camera entirely and start directly with an overlay.
14128
+ - Camera scale: default to **1.1** (gentle re-centre). Use **1.2\u20131.3** for normal reading distance. Use **1.4\u20131.6** ONLY for dense figures or small inline details. NEVER use a scale below 0.5 or above 4.0 \u2014 the engine rejects those. When in doubt, use 1.1.
14129
+ - Prefer overlays that OVERLAP the camera move. A camera that takes 700ms to finish while overlays fire at 200ms feels cinematic; overlays waiting until after the camera settles feel sluggish. Stagger \`at_ms\`: camera at 0, first overlay at 150\u2013300ms, second overlay at 600\u2013900ms.
14130
+ - 2\u20134 steps is typical; single-step overlays (no camera) are PREFERRED when the target is already visible.
14131
+ - When narration compares two things, USE the compare recipe (box + box + callout), not a single camera step. When narration says "key takeaway", USE emphasize (highlight + pulse).
14132
+ - Never target a block_id not present in the provided lists. If no block matches, emit a single camera step at scale 1.0 + a subtle pulse on the nearest heading; explain in \`reasoning\`.
14133
+ - Output ONLY valid JSON. No markdown, no code fences, no commentary, no trailing whitespace inside property values.
14134
+
14135
+ # Forbidden outputs \u2014 these will be rejected:
14136
+ - A storyboard with only a camera step.
14137
+ - A camera step with scale < 0.5 or > 4.0.
14138
+ - target_block values not listed in "Page blocks" or "Cross-page figures index".
14139
+ - Tab characters, newlines, or explanatory text inside JSON string values.`;
14140
+ function truncate(text, max = 200) {
14141
+ if (!text) return "";
14142
+ if (text.length <= max) return text;
14143
+ const slice = text.slice(0, max);
14144
+ const last = slice.lastIndexOf(" ");
14145
+ return (last > 40 ? slice.slice(0, last) : slice) + "\u2026";
14146
+ }
14147
+ function buildUserPrompt(input) {
14148
+ const {
14149
+ chunk,
14150
+ pageNumber,
14151
+ page,
14152
+ index,
14153
+ history,
14154
+ camera,
14155
+ activeOverlays,
14156
+ maxSteps = 4
14157
+ } = input;
14158
+ const pageBlocks = page.blocks.map((b) => ({
14159
+ block_id: b.block_id,
14160
+ type: b.type,
14161
+ text: truncate(b.text, 200),
14162
+ bbox: b.bbox,
14163
+ default_action: b.default_action
14164
+ }));
14165
+ const xPageFigures = index.crossPageFigures.filter((f) => f.page !== pageNumber).slice(0, 20).map((f) => ({
14166
+ block_id: f.block_id,
14167
+ page: f.page,
14168
+ type: f.type,
14169
+ text: truncate(f.text, 200)
14170
+ }));
14171
+ const recent = history.slice(-3).map((h) => h.text);
14172
+ const overlaySummary = activeOverlays.map((o) => ({ id: o.id, kind: o.kind }));
14173
+ const blockIdList = pageBlocks.map((b) => b.block_id);
14174
+ return [
14175
+ `Current page: ${pageNumber}`,
14176
+ `Page blocks (${pageBlocks.length}) \u2014 you MUST pick target_block from this list:`,
14177
+ JSON.stringify(pageBlocks),
14178
+ "",
14179
+ `Valid block_ids for this page: ${JSON.stringify(blockIdList)}`,
14180
+ "",
14181
+ `Cross-page figures index: ${JSON.stringify(xPageFigures)}`,
14182
+ "",
14183
+ `Current chunk (what the tutor just said): ${JSON.stringify(chunk)}`,
14184
+ `Recent chunks: ${JSON.stringify(recent)}`,
14185
+ `Current camera: ${JSON.stringify(camera)}`,
14186
+ `Active overlays: ${JSON.stringify(overlaySummary)}`,
14187
+ "",
14188
+ `Max steps: ${maxSteps}`,
14189
+ `Output JSON storyboard. Every target_block MUST be one of the ids above.`
14190
+ ].join("\n");
14191
+ }
14192
+
14193
+ // src/director/sse-parser.ts
14194
+ async function* parseSse(body) {
14195
+ const reader = body.getReader();
14196
+ const decoder = new TextDecoder();
14197
+ let buffer = "";
14198
+ try {
14199
+ while (true) {
14200
+ const { value, done } = await reader.read();
14201
+ if (done) break;
14202
+ buffer += decoder.decode(value, { stream: true });
14203
+ let idx;
14204
+ while ((idx = buffer.indexOf("\n")) !== -1) {
14205
+ const rawLine = buffer.slice(0, idx).trim();
14206
+ buffer = buffer.slice(idx + 1);
14207
+ if (!rawLine.startsWith("data:")) continue;
14208
+ const payload = rawLine.slice(5).trim();
14209
+ if (!payload || payload === "[DONE]") continue;
14210
+ try {
14211
+ yield JSON.parse(payload);
14212
+ } catch {
14213
+ }
14214
+ }
14215
+ }
14216
+ } finally {
14217
+ reader.releaseLock();
14218
+ }
14219
+ }
14220
+ function extractDelta(chunk) {
14221
+ if (!chunk || typeof chunk !== "object") return null;
14222
+ const choices = chunk.choices;
14223
+ if (!choices || !choices.length) return null;
14224
+ return choices[0].delta?.content ?? null;
14225
+ }
14226
+
14227
+ // src/director/llm-director.ts
14228
+ async function directStoryboard(config, input) {
14229
+ const {
14230
+ endpointUrl,
14231
+ model,
14232
+ authToken,
14233
+ extraBody,
14234
+ maxTokens = 1024,
14235
+ temperature = 0.3,
14236
+ useJsonSchema = true,
14237
+ stream = false
14238
+ } = config;
14239
+ const userContent = buildUserPrompt(input);
14240
+ const body = {
14241
+ model,
14242
+ stream,
14243
+ temperature,
14244
+ max_tokens: maxTokens,
14245
+ messages: [
14246
+ { role: "system", content: SYSTEM_PROMPT },
14247
+ { role: "user", content: userContent }
14248
+ ],
14249
+ ...extraBody ?? {}
14250
+ };
14251
+ if (useJsonSchema) {
14252
+ const validBlockIds = input.page.blocks.map((b) => b.block_id);
14253
+ const validCrossPageBlockIds = input.index.crossPageFigures.filter((f) => f.page !== input.pageNumber).map((f) => f.block_id);
14254
+ body.response_format = {
14255
+ type: "json_schema",
14256
+ json_schema: {
14257
+ name: "storyboard",
14258
+ strict: true,
14259
+ schema: storyboardJsonSchema({
14260
+ validBlockIds,
14261
+ validCrossPageBlockIds
14262
+ })
14263
+ }
14264
+ };
14265
+ }
14266
+ const headers = {
14267
+ "Content-Type": "application/json",
14268
+ Accept: stream ? "text/event-stream" : "application/json"
14269
+ };
14270
+ if (authToken) headers.Authorization = `Bearer ${authToken}`;
14271
+ const timeoutController = new AbortController();
14272
+ const timer = setTimeout(
14273
+ () => timeoutController.abort(),
14274
+ input.timeoutMs ?? 2500
14275
+ );
14276
+ const signal = mergeSignals(input.signal, timeoutController.signal);
14277
+ try {
14278
+ const response = await fetch(endpointUrl, {
14279
+ method: "POST",
14280
+ headers,
14281
+ body: JSON.stringify(body),
14282
+ signal
14283
+ });
14284
+ if (!response.ok || !response.body) {
14285
+ return {
14286
+ storyboard: null,
14287
+ raw: "",
14288
+ error: `HTTP ${response.status}`
14289
+ };
14290
+ }
14291
+ let raw = "";
14292
+ if (stream && response.body) {
14293
+ for await (const chunk of parseSse(response.body)) {
14294
+ const delta = extractDelta(chunk);
14295
+ if (delta) raw += delta;
14296
+ }
14297
+ } else {
14298
+ const json = await response.json();
14299
+ raw = json.choices?.[0]?.message?.content ?? "";
14300
+ }
14301
+ const stripped = collapseWhitespaceRuns(stripCodeFences(raw).trim());
14302
+ let parsed;
14303
+ try {
14304
+ parsed = JSON.parse(stripped);
14305
+ } catch (e) {
14306
+ return {
14307
+ storyboard: null,
14308
+ raw,
14309
+ error: `parse error: ${e.message}`
14310
+ };
14311
+ }
14312
+ const cleaned = clampNumericRanges(stripNullsDeep(parsed));
14313
+ const validation = StoryboardSchema.safeParse(cleaned);
14314
+ if (validation.success) {
14315
+ return {
14316
+ storyboard: enforceOverlayPresence(validation.data),
14317
+ raw
14318
+ };
14319
+ }
14320
+ const salvaged = salvageStoryboard(cleaned);
14321
+ if (salvaged) {
14322
+ return { storyboard: enforceOverlayPresence(salvaged), raw };
14323
+ }
14324
+ return {
14325
+ storyboard: null,
14326
+ raw,
14327
+ error: `validation failed: ${validation.error.message}`
14328
+ };
14329
+ } catch (e) {
14330
+ const name = e.name;
14331
+ const msg = name === "AbortError" ? "aborted" : e.message;
14332
+ return { storyboard: null, raw: "", error: msg };
14333
+ } finally {
14334
+ clearTimeout(timer);
14335
+ }
14336
+ }
14337
+ function stripCodeFences(s) {
14338
+ const m = s.match(/```(?:json)?\s*([\s\S]*?)```/);
14339
+ return m ? m[1] : s;
14340
+ }
14341
+ function collapseWhitespaceRuns(src) {
14342
+ let out = "";
14343
+ let inString = false;
14344
+ let escape = false;
14345
+ let run = 0;
14346
+ for (let i = 0; i < src.length; i++) {
14347
+ const c = src[i];
14348
+ if (inString) {
14349
+ out += c;
14350
+ if (escape) {
14351
+ escape = false;
14352
+ } else if (c === "\\") {
14353
+ escape = true;
14354
+ } else if (c === '"') {
14355
+ inString = false;
14356
+ }
14357
+ continue;
14358
+ }
14359
+ if (c === '"') {
14360
+ out += c;
14361
+ inString = true;
14362
+ run = 0;
14363
+ continue;
14364
+ }
14365
+ if (c === " " || c === " " || c === "\n" || c === "\r") {
14366
+ run++;
14367
+ if (run <= 1) out += " ";
14368
+ continue;
14369
+ }
14370
+ run = 0;
14371
+ out += c;
14372
+ }
14373
+ return out;
14374
+ }
14375
+ function clampNumericRanges(input) {
14376
+ if (input === null || input === void 0) return input;
14377
+ if (Array.isArray(input)) return input.map(clampNumericRanges);
14378
+ if (typeof input !== "object") return input;
14379
+ const obj = input;
14380
+ const out = {};
14381
+ for (const [k, v] of Object.entries(obj)) {
14382
+ out[k] = clampNumericRanges(v);
14383
+ }
14384
+ const type = typeof out.type === "string" ? out.type : void 0;
14385
+ if (type === "camera") {
14386
+ if (typeof out.scale === "number") out.scale = clamp(out.scale, 0.5, 4);
14387
+ if (typeof out.padding === "number") {
14388
+ out.padding = clamp(out.padding, 0, 400);
14389
+ }
14390
+ }
14391
+ if (typeof out.dim_opacity === "number") {
14392
+ out.dim_opacity = clamp(out.dim_opacity, 0, 1);
14393
+ }
14394
+ if (typeof out.feather_px === "number") {
14395
+ out.feather_px = clamp(out.feather_px, 0, 200);
14396
+ }
14397
+ if (typeof out.draw_duration_ms === "number") {
14398
+ out.draw_duration_ms = clamp(out.draw_duration_ms, 100, 3e3);
14399
+ }
14400
+ if (typeof out.count === "number") {
14401
+ out.count = Math.round(clamp(out.count, 1, 5));
14402
+ }
14403
+ if (typeof out.at_ms === "number") {
14404
+ out.at_ms = clamp(out.at_ms, 0, 5e3);
14405
+ }
14406
+ if (typeof out.duration_ms === "number" && type === void 0) {
14407
+ out.duration_ms = clamp(out.duration_ms, 100, 5e3);
14408
+ }
14409
+ return out;
14410
+ }
14411
+ function clamp(v, lo, hi) {
14412
+ return Math.min(hi, Math.max(lo, v));
14413
+ }
14414
+ function enforceOverlayPresence(sb) {
14415
+ if (sb.steps.length === 0) return sb;
14416
+ const hasOverlay = sb.steps.some(
14417
+ (s) => s.action.type !== "camera" && s.action.type !== "clear"
14418
+ );
14419
+ if (hasOverlay) return sb;
14420
+ const cameraStep = sb.steps.find((s) => s.action.type === "camera");
14421
+ if (!cameraStep || cameraStep.action.type !== "camera") return sb;
14422
+ const target = cameraStep.action.target_block;
14423
+ if (!target) return sb;
14424
+ return {
14425
+ ...sb,
14426
+ reasoning: `${sb.reasoning} [auto-appended pulse: camera-only storyboards are forbidden]`,
14427
+ steps: [
14428
+ ...sb.steps,
14429
+ {
14430
+ at_ms: Math.min(4800, (cameraStep.at_ms ?? 0) + 200),
14431
+ duration_ms: 900,
14432
+ action: {
14433
+ type: "pulse",
14434
+ target_block: target,
14435
+ count: 2,
14436
+ intensity: "normal"
14437
+ }
14438
+ }
14439
+ ]
14440
+ };
14441
+ }
14442
+ function stripNullsDeep(input) {
14443
+ if (input === null) return void 0;
14444
+ if (Array.isArray(input)) {
14445
+ return input.map(stripNullsDeep).filter((v) => v !== void 0);
14446
+ }
14447
+ if (input && typeof input === "object") {
14448
+ const out = {};
14449
+ for (const [k, v] of Object.entries(input)) {
14450
+ const cleaned = stripNullsDeep(v);
14451
+ if (cleaned !== void 0) out[k] = cleaned;
14452
+ }
14453
+ return out;
14454
+ }
14455
+ return input;
14456
+ }
14457
+ function salvageStoryboard(parsed) {
14458
+ if (!parsed || typeof parsed !== "object") return null;
14459
+ const obj = parsed;
14460
+ if (!Array.isArray(obj.steps)) return null;
14461
+ const goodSteps = [];
14462
+ for (const step of obj.steps) {
14463
+ const r = StoryboardStepSchema.safeParse(step);
14464
+ if (r.success) goodSteps.push(r.data);
14465
+ if (goodSteps.length >= 4) break;
14466
+ }
14467
+ if (goodSteps.length === 0) return null;
14468
+ return {
14469
+ version: 1,
14470
+ reasoning: typeof obj.reasoning === "string" ? obj.reasoning + " (salvaged)" : "salvaged",
14471
+ steps: goodSteps
14472
+ };
14473
+ }
14474
+ function mergeSignals(a, b) {
14475
+ if (!a) return b;
14476
+ if (!b) return a;
14477
+ const ctrl = new AbortController();
14478
+ const onAbort = () => ctrl.abort();
14479
+ a.addEventListener("abort", onAbort);
14480
+ b.addEventListener("abort", onAbort);
14481
+ return ctrl.signal;
14482
+ }
14483
+
14484
+ // src/director/embedding-fallback.ts
14485
+ function cosineSimilarity(a, b) {
14486
+ let dot = 0;
14487
+ let na = 0;
14488
+ let nb = 0;
14489
+ const n = Math.min(a.length, b.length);
14490
+ for (let i = 0; i < n; i++) {
14491
+ dot += a[i] * b[i];
14492
+ na += a[i] * a[i];
14493
+ nb += b[i] * b[i];
14494
+ }
14495
+ const denom = Math.sqrt(na) * Math.sqrt(nb);
14496
+ return denom === 0 ? 0 : dot / denom;
14497
+ }
14498
+ async function matchChunkToBlock(chunk, page, provider) {
14499
+ const textBlocks = page.blocks.filter(
14500
+ (b) => typeof b.text === "string" && b.text.trim().length > 0
14501
+ );
14502
+ if (textBlocks.length === 0) return null;
14503
+ const inputs = [chunk, ...textBlocks.map((b) => b.text)];
14504
+ const embeds = await provider.embed(inputs);
14505
+ if (embeds.length < 2) return null;
14506
+ const chunkEmbed = embeds[0];
14507
+ let best = null;
14508
+ for (let i = 0; i < textBlocks.length; i++) {
14509
+ const score = cosineSimilarity(chunkEmbed, embeds[i + 1]);
14510
+ if (!best || score > best.score) best = { block: textBlocks[i], score };
14511
+ }
14512
+ return best;
14513
+ }
14514
+ function nearestFigureOnPage(caption, page) {
14515
+ if (!page) return null;
14516
+ const [cx1, cy1, cx2, cy2] = caption.bbox;
14517
+ const ccx = (cx1 + cx2) / 2;
14518
+ const ccy = (cy1 + cy2) / 2;
14519
+ let best = null;
14520
+ for (const b of page.blocks) {
14521
+ if (b.block_id === caption.block_id) continue;
14522
+ if (b.type !== "figure" && b.type !== "figure_region") continue;
14523
+ const [x1, y1, x2, y2] = b.bbox;
14524
+ const fx = (x1 + x2) / 2;
14525
+ const fy = (y1 + y2) / 2;
14526
+ const dist = Math.hypot(fx - ccx, fy - ccy);
14527
+ if (!best || dist < best.dist) best = { block: b, dist };
14528
+ }
14529
+ return best?.block ?? null;
14530
+ }
14531
+ function truncateLabel(text, max) {
14532
+ if (!text) return "";
14533
+ const clean = text.replace(/\s+/g, " ").trim();
14534
+ if (clean.length <= max) return clean;
14535
+ return clean.slice(0, max - 1) + "\u2026";
14536
+ }
14537
+ function storyboardFromMatch(match, page) {
14538
+ if (!match) {
14539
+ return {
14540
+ version: 1,
14541
+ reasoning: "fallback: no match \u2014 clearing overlays",
14542
+ steps: [
14543
+ {
14544
+ at_ms: 0,
14545
+ duration_ms: 800,
14546
+ action: { type: "clear", targets: "overlays" }
14547
+ }
14548
+ ]
14549
+ };
14550
+ }
14551
+ const { block } = match;
14552
+ const id = block.block_id;
14553
+ const reason = `fallback (block.type=${block.type}): matched ${id} (${match.score.toFixed(2)})`;
14554
+ switch (block.type) {
14555
+ case "heading": {
14556
+ return {
14557
+ version: 1,
14558
+ reasoning: reason,
14559
+ steps: [
14560
+ {
14561
+ at_ms: 0,
14562
+ duration_ms: 700,
14563
+ action: {
14564
+ type: "spotlight",
14565
+ target_block: id,
14566
+ dim_opacity: 0.6,
14567
+ feather_px: 40,
14568
+ shape: "rounded"
14569
+ }
14570
+ },
14571
+ {
14572
+ at_ms: 300,
14573
+ duration_ms: 1200,
14574
+ action: {
14575
+ type: "label",
14576
+ target_block: id,
14577
+ text: truncateLabel(block.text, 32) || "section",
14578
+ position: "top"
14579
+ }
14580
+ }
14581
+ ]
14582
+ };
14583
+ }
14584
+ case "paragraph": {
14585
+ return {
14586
+ version: 1,
14587
+ reasoning: reason,
14588
+ steps: [
14589
+ {
14590
+ at_ms: 0,
14591
+ duration_ms: 600,
14592
+ action: {
14593
+ type: "camera",
14594
+ target_block: id,
14595
+ scale: 1.1,
14596
+ padding: 80,
14597
+ easing: "ease-out"
14598
+ }
14599
+ },
14600
+ {
14601
+ at_ms: 300,
14602
+ duration_ms: 900,
14603
+ action: {
14604
+ type: "underline",
14605
+ target_block: id,
14606
+ color: "#FBBF24",
14607
+ style: "sketch",
14608
+ draw_duration_ms: 800
14609
+ }
14610
+ }
14611
+ ]
14612
+ };
14613
+ }
14614
+ case "list_item":
14615
+ case "mcq_option": {
14616
+ return {
14617
+ version: 1,
14618
+ reasoning: reason,
14619
+ steps: [
14620
+ {
14621
+ at_ms: 0,
14622
+ duration_ms: 500,
14623
+ action: {
14624
+ type: "highlight",
14625
+ target_block: id,
14626
+ color: "rgba(250, 204, 21, 0.35)",
14627
+ draw_duration_ms: 450
14628
+ }
14629
+ }
14630
+ ]
14631
+ };
14632
+ }
14633
+ case "caption": {
14634
+ const figure = nearestFigureOnPage(block, page);
14635
+ if (figure) {
14636
+ return {
14637
+ version: 1,
14638
+ reasoning: `${reason}; caption \u2192 figure ${figure.block_id}`,
14639
+ steps: [
14640
+ {
14641
+ at_ms: 0,
14642
+ duration_ms: 900,
14643
+ action: {
14644
+ type: "callout",
14645
+ from_block: id,
14646
+ to_block: figure.block_id,
14647
+ label: "see",
14648
+ curve: "curved"
14649
+ }
14650
+ },
14651
+ {
14652
+ at_ms: 600,
14653
+ duration_ms: 1e3,
14654
+ action: {
14655
+ type: "pulse",
14656
+ target_block: figure.block_id,
14657
+ count: 2,
14658
+ intensity: "normal"
14659
+ }
14660
+ }
14661
+ ]
14662
+ };
14663
+ }
14664
+ return {
14665
+ version: 1,
14666
+ reasoning: `${reason}; no figure on page, underlining caption`,
14667
+ steps: [
14668
+ {
14669
+ at_ms: 0,
14670
+ duration_ms: 800,
14671
+ action: {
14672
+ type: "underline",
14673
+ target_block: id,
14674
+ color: "#FBBF24",
14675
+ style: "sketch",
14676
+ draw_duration_ms: 700
14677
+ }
14678
+ }
14679
+ ]
14680
+ };
14681
+ }
14682
+ case "figure": {
14683
+ return {
14684
+ version: 1,
14685
+ reasoning: reason,
14686
+ steps: [
14687
+ {
14688
+ at_ms: 0,
14689
+ duration_ms: 900,
14690
+ action: {
14691
+ type: "pulse",
14692
+ target_block: id,
14693
+ count: 2,
14694
+ intensity: "strong"
14695
+ }
14696
+ },
14697
+ {
14698
+ at_ms: 400,
14699
+ duration_ms: 1200,
14700
+ action: {
14701
+ type: "box",
14702
+ target_block: id,
14703
+ color: "#3B82F6",
14704
+ style: "solid"
14705
+ }
14706
+ }
14707
+ ]
14708
+ };
14709
+ }
14710
+ case "figure_region": {
14711
+ return {
14712
+ version: 1,
14713
+ reasoning: reason,
14714
+ steps: [
14715
+ {
14716
+ at_ms: 0,
14717
+ duration_ms: 900,
14718
+ action: {
14719
+ type: "pulse",
14720
+ target_block: id,
14721
+ count: 2,
14722
+ intensity: "normal"
14723
+ }
14724
+ }
14725
+ ]
14726
+ };
14727
+ }
14728
+ case "table": {
14729
+ return {
14730
+ version: 1,
14731
+ reasoning: reason,
14732
+ steps: [
14733
+ {
14734
+ at_ms: 0,
14735
+ duration_ms: 700,
14736
+ action: {
14737
+ type: "camera",
14738
+ target_block: id,
14739
+ scale: 1.2,
14740
+ padding: 60,
14741
+ easing: "ease-out"
14742
+ }
14743
+ },
14744
+ {
14745
+ at_ms: 300,
14746
+ duration_ms: 1e3,
14747
+ action: {
14748
+ type: "box",
14749
+ target_block: id,
14750
+ color: "#3B82F6",
14751
+ style: "dashed"
14752
+ }
14753
+ }
14754
+ ]
14755
+ };
14756
+ }
14757
+ default: {
14758
+ return {
14759
+ version: 1,
14760
+ reasoning: `${reason}; unknown block.type, using highlight`,
14761
+ steps: [
14762
+ {
14763
+ at_ms: 0,
14764
+ duration_ms: 600,
14765
+ action: {
14766
+ type: "highlight",
14767
+ target_block: id,
14768
+ color: "rgba(250, 204, 21, 0.35)",
14769
+ draw_duration_ms: 500
14770
+ }
14771
+ }
14772
+ ]
14773
+ };
14774
+ }
14775
+ }
14776
+ }
14777
+
14778
+ // src/components/TutorMode/TutorModeContainer.tsx
14779
+ import { jsx as jsx52, jsxs as jsxs38 } from "react/jsx-runtime";
14780
+ function buildBBoxIndex(bboxData) {
14781
+ const byPage = /* @__PURE__ */ new Map();
14782
+ const blockById = /* @__PURE__ */ new Map();
14783
+ const crossPageFigures = [];
14784
+ for (const page of bboxData) {
14785
+ byPage.set(page.page_number, page);
14786
+ for (const block of page.blocks) {
14787
+ blockById.set(block.block_id, { block, pageNumber: page.page_number });
14788
+ if ((block.type === "figure" || block.type === "figure_region" || block.type === "caption") && typeof block.text === "string" && block.text.length > 0) {
14789
+ crossPageFigures.push({
14790
+ block_id: block.block_id,
14791
+ page: page.page_number,
14792
+ type: block.type,
14793
+ text: block.text
14794
+ });
14795
+ }
14796
+ }
14797
+ }
14798
+ return { byPage, blockById, crossPageFigures };
14799
+ }
14800
+ function TutorModeContainer({
14801
+ pageNumber,
14802
+ bboxData,
14803
+ narrationStore,
14804
+ scale,
14805
+ rotation = 0,
14806
+ currentChunk,
14807
+ llm,
14808
+ idleTimeoutMs = 5e3,
14809
+ llmTimeoutMs = 3e4,
14810
+ embeddingProvider,
14811
+ showSubtitles = false,
14812
+ showExitButton = true,
14813
+ onExitTutorMode,
14814
+ minOverlayDurationMs,
14815
+ className
14816
+ }) {
14817
+ const containerRef = useRef27(null);
14818
+ const index = useMemo15(() => buildBBoxIndex(bboxData), [bboxData]);
14819
+ const { document: document2 } = usePDFViewer();
14820
+ const [pageProxy, setPageProxy] = useState30(null);
14821
+ const [viewport, setViewport] = useState30({ width: 800, height: 1e3 });
14822
+ const camera = useStore2(narrationStore, (s) => s.camera);
14823
+ const activeOverlays = useStore2(narrationStore, (s) => s.activeOverlays);
14824
+ useEffect28(() => {
14825
+ if (!containerRef.current) return;
14826
+ const el = containerRef.current;
14827
+ const update = () => setViewport({ width: el.clientWidth, height: el.clientHeight });
14828
+ update();
14829
+ const ro = new ResizeObserver(update);
14830
+ ro.observe(el);
14831
+ return () => ro.disconnect();
14832
+ }, []);
14833
+ useEffect28(() => {
14834
+ if (!document2) {
14835
+ setPageProxy(null);
14836
+ return;
14837
+ }
14838
+ let cancelled = false;
14839
+ document2.getPage(pageNumber).then((p) => {
14840
+ if (!cancelled) setPageProxy(p);
14841
+ }).catch(() => {
14842
+ if (!cancelled) setPageProxy(null);
14843
+ });
14844
+ return () => {
14845
+ cancelled = true;
14846
+ };
14847
+ }, [document2, pageNumber]);
14848
+ useEffect28(() => {
14849
+ narrationStore.getState().setCurrentPage(pageNumber);
14850
+ }, [pageNumber, narrationStore]);
14851
+ useEffect28(() => {
14852
+ const page2 = index.byPage.get(pageNumber);
14853
+ if (!page2) return;
14854
+ if (viewport.width === 0 || viewport.height === 0) return;
14855
+ if (narrationStore.getState().activeOverlays.length > 0) return;
14856
+ const fit = Math.min(
14857
+ viewport.width / page2.page_dimensions.width,
14858
+ viewport.height / page2.page_dimensions.height
14859
+ ) * 0.95;
14860
+ narrationStore.getState().setCamera({ scale: fit, x: 0, y: 0 });
14861
+ }, [pageNumber, viewport, index, narrationStore]);
14862
+ const engineRef = useRef27(null);
14863
+ useEffect28(() => {
14864
+ engineRef.current = new StoryboardEngine({
14865
+ narrationStore,
14866
+ bboxIndex: index,
14867
+ getViewport: () => viewport,
14868
+ minOverlayDurationMs
14869
+ });
14870
+ return () => engineRef.current?.cancelPending();
14871
+ }, [narrationStore, index, viewport, minOverlayDurationMs]);
14872
+ const abortRef = useRef27(null);
14873
+ const debounceRef = useRef27(null);
14874
+ const lastChunkRef = useRef27(null);
14875
+ useEffect28(() => {
14876
+ if (!llm) return;
14877
+ if (!currentChunk || currentChunk === lastChunkRef.current) return;
14878
+ if (debounceRef.current) clearTimeout(debounceRef.current);
14879
+ debounceRef.current = setTimeout(async () => {
14880
+ const chunk = currentChunk;
14881
+ if (chunk === lastChunkRef.current) return;
14882
+ lastChunkRef.current = chunk;
14883
+ const page2 = index.byPage.get(pageNumber);
14884
+ if (!page2) return;
14885
+ narrationStore.getState().pushChunkHistory({
14886
+ text: chunk,
14887
+ pageNumber,
14888
+ timestamp: Date.now()
14889
+ });
14890
+ narrationStore.getState().appendDebugEvent({
14891
+ kind: "chunk",
14892
+ summary: `chunk \u2192 ${chunk.slice(0, 80)}${chunk.length > 80 ? "\u2026" : ""}`,
14893
+ payload: { chunk, pageNumber }
14894
+ });
14895
+ abortRef.current?.abort();
14896
+ abortRef.current = new AbortController();
14897
+ narrationStore.getState().setLlmStatus("in-flight");
14898
+ narrationStore.getState().appendDebugEvent({
14899
+ kind: "llm-request",
14900
+ summary: `LLM ${llm.model} (page ${pageNumber}, ${page2.blocks.length} blocks)`,
14901
+ payload: { model: llm.model, pageNumber, blockCount: page2.blocks.length }
14902
+ });
14903
+ const result = await directStoryboard(llm, {
14904
+ chunk,
14905
+ pageNumber,
14906
+ page: page2,
14907
+ index,
14908
+ history: narrationStore.getState().chunkHistory,
14909
+ camera: narrationStore.getState().camera,
14910
+ activeOverlays: narrationStore.getState().activeOverlays,
14911
+ signal: abortRef.current.signal,
14912
+ timeoutMs: llmTimeoutMs
14913
+ });
14914
+ if (result.storyboard) {
14915
+ narrationStore.getState().setLlmStatus("idle");
14916
+ narrationStore.getState().appendDebugEvent({
14917
+ kind: "llm-response",
14918
+ summary: `storyboard \u2713 ${result.storyboard.steps.length} steps \u2014 ${result.storyboard.reasoning.slice(0, 60)}`,
14919
+ payload: { raw: result.raw, storyboard: result.storyboard }
14920
+ });
14921
+ engineRef.current?.execute(result.storyboard);
14922
+ narrationStore.getState().appendDebugEvent({
14923
+ kind: "storyboard-execute",
14924
+ summary: `engine executing ${result.storyboard.steps.length} steps`,
14925
+ payload: result.storyboard.steps.map((s) => ({
14926
+ at_ms: s.at_ms,
14927
+ type: s.action.type,
14928
+ target: "target_block" in s.action ? s.action.target_block : "target" in s.action ? s.action.target : void 0
14929
+ }))
14930
+ });
14931
+ } else {
14932
+ narrationStore.getState().setLlmStatus("failed", result.error ?? "unknown");
14933
+ narrationStore.getState().appendDebugEvent({
14934
+ kind: "llm-error",
14935
+ summary: `LLM failed: ${(result.error ?? "unknown").slice(0, 80)}`,
14936
+ payload: { error: result.error, raw: result.raw }
14937
+ });
14938
+ if (embeddingProvider) {
14939
+ try {
14940
+ const match = await matchChunkToBlock(chunk, page2, embeddingProvider);
14941
+ const fallbackSb = storyboardFromMatch(match, page2);
14942
+ narrationStore.getState().appendDebugEvent({
14943
+ kind: "fallback-fired",
14944
+ summary: `embedding fallback \u2192 ${match?.block.block_id ?? "no match"}`,
14945
+ payload: { match, storyboard: fallbackSb }
14946
+ });
14947
+ engineRef.current?.execute(fallbackSb);
14948
+ } catch (e) {
14949
+ narrationStore.getState().appendDebugEvent({
14950
+ kind: "llm-error",
14951
+ summary: `fallback also failed: ${e.message}`,
14952
+ payload: e
14953
+ });
14954
+ }
14955
+ }
14956
+ }
14957
+ }, 200);
14958
+ return () => {
14959
+ if (debounceRef.current) clearTimeout(debounceRef.current);
14960
+ };
14961
+ }, [currentChunk, llm, index, pageNumber, narrationStore, embeddingProvider, llmTimeoutMs]);
14962
+ useEffect28(() => {
14963
+ if (!currentChunk) return;
14964
+ const t = setTimeout(() => {
14965
+ if (!engineRef.current) return;
14966
+ const hist = narrationStore.getState().chunkHistory;
14967
+ const latest = hist.length > 0 ? hist[hist.length - 1] : null;
14968
+ if (!latest) return;
14969
+ if (Date.now() - latest.timestamp < idleTimeoutMs) return;
14970
+ engineRef.current.resetVisuals();
14971
+ }, idleTimeoutMs + 100);
14972
+ return () => clearTimeout(t);
14973
+ }, [currentChunk, idleTimeoutMs, narrationStore]);
14974
+ const page = index.byPage.get(pageNumber);
14975
+ const dpiScale = page ? page.page_dimensions.dpi / 72 : 1;
14976
+ const rasterScale = dpiScale * (scale || 1);
14977
+ const baseW = page ? page.page_dimensions.width * (scale || 1) : 0;
14978
+ const baseH = page ? page.page_dimensions.height * (scale || 1) : 0;
14979
+ return /* @__PURE__ */ jsxs38(
14980
+ "div",
14981
+ {
14982
+ ref: containerRef,
14983
+ className,
14984
+ style: {
14985
+ position: "relative",
14986
+ width: "100%",
14987
+ height: "100%",
14988
+ overflow: "hidden",
14989
+ background: "#111"
14990
+ },
14991
+ "data-role": "tutor-mode-container",
14992
+ "data-page-loaded": page ? "true" : "false",
14993
+ children: [
14994
+ showExitButton ? /* @__PURE__ */ jsx52(
14995
+ "button",
14996
+ {
14997
+ onClick: () => {
14998
+ engineRef.current?.resetVisuals();
14999
+ onExitTutorMode?.();
15000
+ },
15001
+ style: {
15002
+ position: "absolute",
15003
+ top: 12,
15004
+ right: 12,
15005
+ zIndex: 60,
15006
+ minHeight: 40,
15007
+ minWidth: 40,
15008
+ padding: "8px 14px",
15009
+ border: "none",
15010
+ borderRadius: 8,
15011
+ background: "rgba(255,255,255,0.12)",
15012
+ color: "white",
15013
+ cursor: "pointer",
15014
+ fontFamily: "system-ui, sans-serif",
15015
+ fontSize: 14,
15016
+ touchAction: "manipulation"
15017
+ },
15018
+ "aria-label": "Reset view \u2014 clear overlays and fit the page",
15019
+ "data-role": "exit-tutor",
15020
+ children: "Reset view"
15021
+ }
15022
+ ) : null,
15023
+ page ? /* @__PURE__ */ jsx52(CameraView, { camera, children: /* @__PURE__ */ jsxs38(
15024
+ "div",
15025
+ {
15026
+ style: {
15027
+ position: "absolute",
15028
+ top: "50%",
15029
+ left: "50%",
15030
+ width: baseW,
15031
+ height: baseH,
15032
+ transform: "translate(-50%, -50%)"
15033
+ },
15034
+ children: [
15035
+ /* @__PURE__ */ jsx52(
15036
+ PDFPage,
15037
+ {
15038
+ pageNumber,
15039
+ page: pageProxy,
15040
+ scale: rasterScale,
15041
+ rotation,
15042
+ showTextLayer: false,
15043
+ showHighlightLayer: false,
15044
+ showAnnotationLayer: false
15045
+ }
15046
+ ),
15047
+ /* @__PURE__ */ jsx52(
15048
+ CinemaLayer,
15049
+ {
15050
+ page,
15051
+ index,
15052
+ overlays: activeOverlays,
15053
+ scale: scale || 1
15054
+ }
15055
+ )
15056
+ ]
15057
+ }
15058
+ ) }) : null,
15059
+ showSubtitles ? /* @__PURE__ */ jsx52(SubtitleBar, { text: currentChunk ?? null }) : null
15060
+ ]
15061
+ }
15062
+ );
15063
+ }
15064
+
15065
+ // src/director/transformers-embedding.ts
15066
+ var loaded = null;
15067
+ function getLocalMiniLM() {
15068
+ if (loaded) return loaded;
15069
+ loaded = (async () => {
15070
+ const mod = await import(
15071
+ /* webpackIgnore: true */
15072
+ "@xenova/transformers"
15073
+ );
15074
+ const { pipeline } = mod;
15075
+ const extractor = await pipeline(
15076
+ "feature-extraction",
15077
+ "Xenova/all-MiniLM-L6-v2"
15078
+ );
15079
+ return {
15080
+ async embed(texts) {
15081
+ const out = [];
15082
+ for (const t of texts) {
15083
+ const result = await extractor(t, {
15084
+ pooling: "mean",
15085
+ normalize: true
15086
+ });
15087
+ out.push(new Float32Array(result.data.slice()));
15088
+ }
15089
+ return out;
15090
+ }
15091
+ };
15092
+ })();
15093
+ return loaded;
15094
+ }
15095
+
12809
15096
  // src/index.ts
12810
15097
  init_hooks();
12811
15098
  init_store();
@@ -12816,19 +15103,26 @@ init_PluginManager();
12816
15103
  // src/index.ts
12817
15104
  init_utils();
12818
15105
  export {
15106
+ AnimatedHighlight,
15107
+ AnimatedUnderline,
12819
15108
  AnnotationLayer,
12820
15109
  AnnotationToolbar,
12821
15110
  AskAboutOverlay,
12822
15111
  AskAboutTrigger,
12823
15112
  BookModeContainer,
12824
15113
  BookmarksPanel,
15114
+ BoxOverlay,
15115
+ CalloutArrow,
15116
+ CameraView,
12825
15117
  CanvasLayer,
15118
+ CinemaLayer,
12826
15119
  ContinuousScrollContainer,
12827
15120
  DocumentContainer,
12828
15121
  DrawingCanvas,
12829
15122
  DualPageContainer,
12830
15123
  FloatingZoomControls,
12831
15124
  FocusRegionLayer,
15125
+ GhostReference,
12832
15126
  HighlightLayer,
12833
15127
  HighlightPopover,
12834
15128
  HighlightsPanel,
@@ -12845,32 +15139,46 @@ export {
12845
15139
  PDFViewerContext,
12846
15140
  PDFViewerProvider,
12847
15141
  PluginManager,
15142
+ PulseOverlay,
12848
15143
  QuickNoteButton,
12849
15144
  QuickNotePopover,
15145
+ SYSTEM_PROMPT,
12850
15146
  SearchPanel,
12851
15147
  SelectionToolbar,
12852
15148
  ShapePreview,
12853
15149
  ShapeRenderer,
12854
15150
  Sidebar,
15151
+ SpotlightMask,
15152
+ StickyLabel,
12855
15153
  StickyNote,
15154
+ StoryboardActionSchema,
15155
+ StoryboardEngine,
15156
+ StoryboardSchema,
15157
+ SubtitleBar,
12856
15158
  TakeawaysPanel,
12857
15159
  TextLayer,
12858
15160
  ThumbnailPanel,
12859
15161
  Toolbar,
15162
+ TutorModeContainer,
12860
15163
  VirtualizedDocumentContainer,
12861
15164
  applyRotation,
15165
+ buildBBoxIndex,
15166
+ buildUserPrompt,
12862
15167
  clearHighlights,
12863
15168
  clearStudentData,
12864
15169
  cn,
15170
+ cosineSimilarity,
12865
15171
  countTextOnPage,
12866
15172
  createAgentAPI,
12867
15173
  createAgentStore,
12868
15174
  createAnnotationStore,
15175
+ createNarrationStore,
12869
15176
  createPDFViewer,
12870
15177
  createPluginManager,
12871
15178
  createSearchStore,
12872
15179
  createStudentStore,
12873
15180
  createViewerStore,
15181
+ directStoryboard,
12874
15182
  doRectsIntersect,
12875
15183
  downloadAnnotationsAsJSON,
12876
15184
  downloadAnnotationsAsMarkdown,
@@ -12885,6 +15193,7 @@ export {
12885
15193
  generateDocumentId,
12886
15194
  getAllDocumentIds,
12887
15195
  getAllStudentDataDocumentIds,
15196
+ getLocalMiniLM,
12888
15197
  getMetadata,
12889
15198
  getOutline,
12890
15199
  getPage,
@@ -12902,6 +15211,8 @@ export {
12902
15211
  loadDocumentWithCallbacks,
12903
15212
  loadHighlights,
12904
15213
  loadStudentData,
15214
+ makeOverlayId,
15215
+ matchChunkToBlock,
12905
15216
  mergeAdjacentRects,
12906
15217
  pdfToPercent,
12907
15218
  pdfToViewport,
@@ -12914,6 +15225,9 @@ export {
12914
15225
  saveHighlights,
12915
15226
  saveStudentData,
12916
15227
  scaleRect,
15228
+ storyboardFromMatch,
15229
+ storyboardJsonSchema,
15230
+ truncate,
12917
15231
  useAgentContext,
12918
15232
  useAgentStore,
12919
15233
  useAnnotationStore,