pdfjs-reader-core 0.2.17 → 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
  },
@@ -1760,6 +1760,113 @@ var init_coordinates = __esm({
1760
1760
  }
1761
1761
  });
1762
1762
 
1763
+ // src/utils/page-turn-sound.ts
1764
+ function getAudioContext() {
1765
+ if (typeof window === "undefined") return null;
1766
+ if (!audioContext) {
1767
+ try {
1768
+ audioContext = new (window.AudioContext || window.webkitAudioContext)();
1769
+ } catch {
1770
+ return null;
1771
+ }
1772
+ }
1773
+ return audioContext;
1774
+ }
1775
+ function playPageTurnSound(volume = 0.3) {
1776
+ const ctx = getAudioContext();
1777
+ if (!ctx) return;
1778
+ if (ctx.state === "suspended") {
1779
+ ctx.resume();
1780
+ }
1781
+ const now = ctx.currentTime;
1782
+ const duration = 0.35;
1783
+ const bufferSize = Math.floor(ctx.sampleRate * duration);
1784
+ const noiseBuffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
1785
+ const data = noiseBuffer.getChannelData(0);
1786
+ for (let i = 0; i < bufferSize; i++) {
1787
+ data[i] = Math.random() * 2 - 1;
1788
+ }
1789
+ const noiseSource = ctx.createBufferSource();
1790
+ noiseSource.buffer = noiseBuffer;
1791
+ const bandpass = ctx.createBiquadFilter();
1792
+ bandpass.type = "bandpass";
1793
+ bandpass.frequency.setValueAtTime(3e3, now);
1794
+ bandpass.frequency.exponentialRampToValueAtTime(800, now + duration * 0.6);
1795
+ bandpass.Q.setValueAtTime(0.8, now);
1796
+ const highpass = ctx.createBiquadFilter();
1797
+ highpass.type = "highpass";
1798
+ highpass.frequency.setValueAtTime(400, now);
1799
+ highpass.frequency.linearRampToValueAtTime(200, now + duration);
1800
+ const envelope = ctx.createGain();
1801
+ envelope.gain.setValueAtTime(0, now);
1802
+ envelope.gain.linearRampToValueAtTime(volume * 0.6, now + 0.02);
1803
+ envelope.gain.setValueAtTime(volume * 0.6, now + 0.05);
1804
+ envelope.gain.linearRampToValueAtTime(volume, now + duration * 0.3);
1805
+ envelope.gain.exponentialRampToValueAtTime(1e-3, now + duration);
1806
+ const snapBuffer = ctx.createBuffer(1, Math.floor(ctx.sampleRate * 0.08), ctx.sampleRate);
1807
+ const snapData = snapBuffer.getChannelData(0);
1808
+ for (let i = 0; i < snapData.length; i++) {
1809
+ snapData[i] = Math.random() * 2 - 1;
1810
+ }
1811
+ const snapSource = ctx.createBufferSource();
1812
+ snapSource.buffer = snapBuffer;
1813
+ const snapFilter = ctx.createBiquadFilter();
1814
+ snapFilter.type = "bandpass";
1815
+ snapFilter.frequency.setValueAtTime(2e3, now);
1816
+ snapFilter.Q.setValueAtTime(1.5, now);
1817
+ const snapEnvelope = ctx.createGain();
1818
+ snapEnvelope.gain.setValueAtTime(0, now);
1819
+ snapEnvelope.gain.setValueAtTime(0, now + duration * 0.7);
1820
+ snapEnvelope.gain.linearRampToValueAtTime(volume * 0.8, now + duration * 0.75);
1821
+ snapEnvelope.gain.exponentialRampToValueAtTime(1e-3, now + duration);
1822
+ noiseSource.connect(bandpass);
1823
+ bandpass.connect(highpass);
1824
+ highpass.connect(envelope);
1825
+ envelope.connect(ctx.destination);
1826
+ snapSource.connect(snapFilter);
1827
+ snapFilter.connect(snapEnvelope);
1828
+ snapEnvelope.connect(ctx.destination);
1829
+ noiseSource.start(now);
1830
+ noiseSource.stop(now + duration);
1831
+ snapSource.start(now);
1832
+ snapSource.stop(now + duration);
1833
+ }
1834
+ var audioContext;
1835
+ var init_page_turn_sound = __esm({
1836
+ "src/utils/page-turn-sound.ts"() {
1837
+ "use strict";
1838
+ audioContext = null;
1839
+ }
1840
+ });
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
+
1763
1870
  // src/utils/index.ts
1764
1871
  var init_utils = __esm({
1765
1872
  "src/utils/index.ts"() {
@@ -1774,6 +1881,7 @@ var init_utils = __esm({
1774
1881
  init_agent_api();
1775
1882
  init_text_search();
1776
1883
  init_coordinates();
1884
+ init_page_turn_sound();
1777
1885
  }
1778
1886
  });
1779
1887
 
@@ -2177,6 +2285,78 @@ var init_student_store = __esm({
2177
2285
  }
2178
2286
  });
2179
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
+
2180
2360
  // src/store/index.ts
2181
2361
  var init_store = __esm({
2182
2362
  "src/store/index.ts"() {
@@ -2186,6 +2366,7 @@ var init_store = __esm({
2186
2366
  init_search_store();
2187
2367
  init_agent_store();
2188
2368
  init_student_store();
2369
+ init_narration_store();
2189
2370
  }
2190
2371
  });
2191
2372
 
@@ -2195,7 +2376,7 @@ import { useStore } from "zustand";
2195
2376
  import { jsx } from "react/jsx-runtime";
2196
2377
  function PDFViewerProvider({
2197
2378
  children,
2198
- initialState: initialState6,
2379
+ initialState: initialState7,
2199
2380
  theme = "light",
2200
2381
  defaultSidebarPanel = "thumbnails",
2201
2382
  studentMode: _studentMode = false
@@ -2207,22 +2388,22 @@ function PDFViewerProvider({
2207
2388
  const studentStoreRef = useRef(null);
2208
2389
  if (!viewerStoreRef.current) {
2209
2390
  viewerStoreRef.current = createViewerStore({
2210
- ...initialState6?.viewer,
2391
+ ...initialState7?.viewer,
2211
2392
  theme,
2212
2393
  sidebarPanel: defaultSidebarPanel
2213
2394
  });
2214
2395
  }
2215
2396
  if (!annotationStoreRef.current) {
2216
- annotationStoreRef.current = createAnnotationStore(initialState6?.annotation);
2397
+ annotationStoreRef.current = createAnnotationStore(initialState7?.annotation);
2217
2398
  }
2218
2399
  if (!searchStoreRef.current) {
2219
- searchStoreRef.current = createSearchStore(initialState6?.search);
2400
+ searchStoreRef.current = createSearchStore(initialState7?.search);
2220
2401
  }
2221
2402
  if (!agentStoreRef.current) {
2222
- agentStoreRef.current = createAgentStore(initialState6?.agent);
2403
+ agentStoreRef.current = createAgentStore(initialState7?.agent);
2223
2404
  }
2224
2405
  if (!studentStoreRef.current) {
2225
- studentStoreRef.current = createStudentStore(initialState6?.student);
2406
+ studentStoreRef.current = createStudentStore(initialState7?.student);
2226
2407
  }
2227
2408
  useEffect(() => {
2228
2409
  return () => {
@@ -3536,8 +3717,8 @@ var init_PluginManager = __esm({
3536
3717
  /**
3537
3718
  * Get toolbar items by position
3538
3719
  */
3539
- getToolbarItemsByPosition(position) {
3540
- return this.getToolbarItems().filter((item) => item.position === position);
3720
+ getToolbarItemsByPosition(position2) {
3721
+ return this.getToolbarItems().filter((item) => item.position === position2);
3541
3722
  }
3542
3723
  /**
3543
3724
  * Get all sidebar panels from all plugins
@@ -4644,7 +4825,7 @@ var init_MobileToolbar = __esm({
4644
4825
  sidebarOpen,
4645
4826
  theme,
4646
4827
  onThemeChange,
4647
- position = "bottom",
4828
+ position: position2 = "bottom",
4648
4829
  className
4649
4830
  }) {
4650
4831
  const [showMoreMenu, setShowMoreMenu] = useState5(false);
@@ -4678,8 +4859,8 @@ var init_MobileToolbar = __esm({
4678
4859
  "bg-white dark:bg-gray-800",
4679
4860
  "border-gray-200 dark:border-gray-700",
4680
4861
  "px-2 py-1 safe-area-inset",
4681
- position === "top" && "top-0 border-b",
4682
- position === "bottom" && "bottom-0 border-t",
4862
+ position2 === "top" && "top-0 border-b",
4863
+ position2 === "bottom" && "bottom-0 border-t",
4683
4864
  className
4684
4865
  ),
4685
4866
  children: /* @__PURE__ */ jsxs2("div", { className: "flex items-center justify-between gap-1", children: [
@@ -4805,7 +4986,7 @@ var init_MobileToolbar = __esm({
4805
4986
  "bg-white dark:bg-gray-800",
4806
4987
  "rounded-lg shadow-lg",
4807
4988
  "border border-gray-200 dark:border-gray-700",
4808
- position === "bottom" ? "bottom-full mb-2" : "top-full mt-2"
4989
+ position2 === "bottom" ? "bottom-full mb-2" : "top-full mt-2"
4809
4990
  ),
4810
4991
  children: [
4811
4992
  /* @__PURE__ */ jsx3("div", { className: "px-2 py-1 text-xs text-gray-500 dark:text-gray-400 font-medium", children: "Theme" }),
@@ -6631,7 +6812,7 @@ var init_AnnotationToolbar = __esm({
6631
6812
  onShapeTypeChange: onShapeTypeChangeProp,
6632
6813
  onColorChange: onColorChangeProp,
6633
6814
  onStrokeWidthChange: onStrokeWidthChangeProp,
6634
- position = "top",
6815
+ position: position2 = "top",
6635
6816
  className
6636
6817
  }) {
6637
6818
  const storeActiveTool = useAnnotationStore((s) => s.activeAnnotationTool);
@@ -6681,9 +6862,9 @@ var init_AnnotationToolbar = __esm({
6681
6862
  {
6682
6863
  className: cn(
6683
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",
6684
- position === "floating" && "fixed bottom-20 left-1/2 -translate-x-1/2 z-50",
6685
- position === "top" && "sticky top-0 z-40",
6686
- 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",
6687
6868
  !isActive && "opacity-90",
6688
6869
  className
6689
6870
  ),
@@ -8332,7 +8513,7 @@ var init_SelectionToolbar = __esm({
8332
8513
  activeColor = "yellow",
8333
8514
  className
8334
8515
  }) {
8335
- const [position, setPosition] = useState16({ top: 0, left: 0, visible: false });
8516
+ const [position2, setPosition] = useState16({ top: 0, left: 0, visible: false });
8336
8517
  const toolbarRef = useRef14(null);
8337
8518
  useEffect17(() => {
8338
8519
  if (selection && selection.text && selection.rects.length > 0) {
@@ -8371,7 +8552,7 @@ var init_SelectionToolbar = __esm({
8371
8552
  const handleCopy = useCallback27(() => {
8372
8553
  onCopy?.();
8373
8554
  }, [onCopy]);
8374
- if (!position.visible || !selection?.text) {
8555
+ if (!position2.visible || !selection?.text) {
8375
8556
  return null;
8376
8557
  }
8377
8558
  return /* @__PURE__ */ jsxs18(
@@ -8389,8 +8570,8 @@ var init_SelectionToolbar = __esm({
8389
8570
  className
8390
8571
  ),
8391
8572
  style: {
8392
- top: position.top,
8393
- left: position.left,
8573
+ top: position2.top,
8574
+ left: position2.left,
8394
8575
  transform: "translateX(-50%)"
8395
8576
  },
8396
8577
  onMouseDown: (e) => {
@@ -8507,7 +8688,7 @@ var init_HighlightPopover = __esm({
8507
8688
  }) {
8508
8689
  const [isEditingComment, setIsEditingComment] = useState17(false);
8509
8690
  const [comment, setComment] = useState17(highlight?.comment ?? "");
8510
- const [position, setPosition] = useState17({ top: 0, left: 0, visible: false });
8691
+ const [position2, setPosition] = useState17({ top: 0, left: 0, visible: false });
8511
8692
  const popoverRef = useRef15(null);
8512
8693
  const textareaRef = useRef15(null);
8513
8694
  useEffect18(() => {
@@ -8555,11 +8736,11 @@ var init_HighlightPopover = __esm({
8555
8736
  onClose();
8556
8737
  }
8557
8738
  }
8558
- if (position.visible) {
8739
+ if (position2.visible) {
8559
8740
  document.addEventListener("mousedown", handleClickOutside);
8560
8741
  return () => document.removeEventListener("mousedown", handleClickOutside);
8561
8742
  }
8562
- }, [position.visible, onClose]);
8743
+ }, [position2.visible, onClose]);
8563
8744
  useEffect18(() => {
8564
8745
  function handleKeyDown(event) {
8565
8746
  if (event.key === "Escape") {
@@ -8571,11 +8752,11 @@ var init_HighlightPopover = __esm({
8571
8752
  }
8572
8753
  }
8573
8754
  }
8574
- if (position.visible) {
8755
+ if (position2.visible) {
8575
8756
  document.addEventListener("keydown", handleKeyDown);
8576
8757
  return () => document.removeEventListener("keydown", handleKeyDown);
8577
8758
  }
8578
- }, [position.visible, isEditingComment, highlight?.comment, onClose]);
8759
+ }, [position2.visible, isEditingComment, highlight?.comment, onClose]);
8579
8760
  const handleColorClick = useCallback28(
8580
8761
  (color) => {
8581
8762
  if (highlight) {
@@ -8602,7 +8783,7 @@ var init_HighlightPopover = __esm({
8602
8783
  onCopy?.(highlight.text);
8603
8784
  }
8604
8785
  }, [highlight, onCopy]);
8605
- if (!highlight || !position.visible) {
8786
+ if (!highlight || !position2.visible) {
8606
8787
  return null;
8607
8788
  }
8608
8789
  return /* @__PURE__ */ jsxs19(
@@ -8619,8 +8800,8 @@ var init_HighlightPopover = __esm({
8619
8800
  className
8620
8801
  ),
8621
8802
  style: {
8622
- top: position.top,
8623
- left: position.left,
8803
+ top: position2.top,
8804
+ left: position2.left,
8624
8805
  transform: "translate(-50%, -100%)",
8625
8806
  width: 280
8626
8807
  },
@@ -9802,17 +9983,237 @@ var init_DualPageContainer = __esm({
9802
9983
  }
9803
9984
  });
9804
9985
 
9805
- // src/components/FloatingZoomControls/FloatingZoomControls.tsx
9806
- import { memo as memo26, useCallback as useCallback32 } from "react";
9986
+ // src/components/PDFViewer/BookModeContainer.tsx
9987
+ import React, { memo as memo26, useEffect as useEffect22, useState as useState21, useRef as useRef19, useCallback as useCallback32 } from "react";
9988
+ import HTMLFlipBook from "react-pageflip";
9807
9989
  import { jsx as jsx27, jsxs as jsxs23 } from "react/jsx-runtime";
9990
+ var BookPage, BookModeContainer;
9991
+ var init_BookModeContainer = __esm({
9992
+ "src/components/PDFViewer/BookModeContainer.tsx"() {
9993
+ "use strict";
9994
+ init_PDFPage2();
9995
+ init_PDFLoadingScreen2();
9996
+ init_hooks();
9997
+ init_utils();
9998
+ BookPage = React.forwardRef(function BookPage2({ pageNumber, page, scale, rotation, width, height }, ref) {
9999
+ return /* @__PURE__ */ jsx27("div", { ref, className: "book-page", "data-page-number": pageNumber, children: /* @__PURE__ */ jsx27("div", { style: { width, height, overflow: "hidden" }, children: /* @__PURE__ */ jsx27(
10000
+ PDFPage,
10001
+ {
10002
+ pageNumber,
10003
+ page,
10004
+ scale,
10005
+ rotation,
10006
+ showTextLayer: false,
10007
+ showAnnotationLayer: false,
10008
+ showHighlightLayer: false
10009
+ }
10010
+ ) }) });
10011
+ });
10012
+ BookModeContainer = memo26(function BookModeContainer2({
10013
+ className,
10014
+ flippingTime = 800,
10015
+ drawShadow = true,
10016
+ maxShadowOpacity = 0.7
10017
+ }) {
10018
+ const {
10019
+ document: document2,
10020
+ currentPage,
10021
+ numPages,
10022
+ scale,
10023
+ rotation,
10024
+ theme,
10025
+ isLoading,
10026
+ goToPage
10027
+ } = usePDFViewer();
10028
+ const scrollToPageRequest = useViewerStore((s) => s.scrollToPageRequest);
10029
+ const { viewerStore } = usePDFViewerStores();
10030
+ const [pages, setPages] = useState21([]);
10031
+ const [rawPageDims, setRawPageDims] = useState21({ width: 612, height: 792 });
10032
+ const [isLoadingPages, setIsLoadingPages] = useState21(false);
10033
+ const containerRef = useRef19(null);
10034
+ const [containerSize, setContainerSize] = useState21({ width: 0, height: 0 });
10035
+ const flipBookRef = useRef19(null);
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
+ }, []);
10048
+ useEffect22(() => {
10049
+ if (!document2) {
10050
+ setPages([]);
10051
+ return;
10052
+ }
10053
+ let cancelled = false;
10054
+ const loadAllPages = async () => {
10055
+ setIsLoadingPages(true);
10056
+ try {
10057
+ const pagePromises = [];
10058
+ for (let i = 1; i <= numPages; i++) {
10059
+ pagePromises.push(document2.getPage(i));
10060
+ }
10061
+ const results = await Promise.allSettled(pagePromises);
10062
+ if (!cancelled) {
10063
+ const loaded2 = results.map((r) => r.status === "fulfilled" ? r.value : null);
10064
+ setPages(loaded2);
10065
+ const firstPage = loaded2[0];
10066
+ if (firstPage) {
10067
+ const vp = firstPage.getViewport({ scale: 1, rotation });
10068
+ setRawPageDims({ width: vp.width, height: vp.height });
10069
+ }
10070
+ }
10071
+ } catch {
10072
+ } finally {
10073
+ if (!cancelled) setIsLoadingPages(false);
10074
+ }
10075
+ };
10076
+ loadAllPages();
10077
+ return () => {
10078
+ cancelled = true;
10079
+ };
10080
+ }, [document2, numPages, rotation]);
10081
+ useEffect22(() => {
10082
+ if (pages[0]) {
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;
10101
+ useEffect22(() => {
10102
+ const pageFlip = flipBookRef.current?.pageFlip();
10103
+ if (!pageFlip) return;
10104
+ const flipBookPage = pageFlip.getCurrentPageIndex();
10105
+ const targetIndex = currentPage - 1;
10106
+ if (flipBookPage !== targetIndex) {
10107
+ isSyncingRef.current = true;
10108
+ pageFlip.turnToPage(targetIndex);
10109
+ setTimeout(() => {
10110
+ isSyncingRef.current = false;
10111
+ }, 100);
10112
+ }
10113
+ }, [currentPage]);
10114
+ useEffect22(() => {
10115
+ if (scrollToPageRequest) {
10116
+ requestAnimationFrame(() => {
10117
+ viewerStore.getState().completeScrollRequest(scrollToPageRequest.requestId);
10118
+ });
10119
+ }
10120
+ }, [scrollToPageRequest, viewerStore]);
10121
+ const handleFlip = useCallback32((e) => {
10122
+ if (isSyncingRef.current) return;
10123
+ const newPage = e.data + 1;
10124
+ if (newPage !== currentPage && newPage >= 1 && newPage <= numPages) {
10125
+ goToPage(newPage);
10126
+ }
10127
+ }, [currentPage, numPages, goToPage]);
10128
+ const themeStyles = {
10129
+ light: "bg-gray-100",
10130
+ dark: "bg-gray-900",
10131
+ sepia: "bg-amber-50"
10132
+ };
10133
+ const themeClass = theme === "dark" ? "dark" : theme === "sepia" ? "sepia" : "";
10134
+ const ready = !!document2 && !isLoadingPages && pages.length > 0;
10135
+ const hasContainer = containerSize.width > 0 && containerSize.height > 0;
10136
+ return /* @__PURE__ */ jsxs23(
10137
+ "div",
10138
+ {
10139
+ ref: containerRef,
10140
+ className: cn(
10141
+ "book-mode-container",
10142
+ "flex-1 h-full w-full overflow-hidden",
10143
+ "flex items-center justify-center",
10144
+ themeStyles[theme],
10145
+ themeClass,
10146
+ className
10147
+ ),
10148
+ style: { userSelect: "none", WebkitUserSelect: "none" },
10149
+ children: [
10150
+ !ready && /* @__PURE__ */ jsx27(
10151
+ PDFLoadingScreen,
10152
+ {
10153
+ phase: !document2 ? isLoading ? "fetching" : "initializing" : "rendering"
10154
+ }
10155
+ ),
10156
+ ready && hasContainer && /* @__PURE__ */ jsx27(
10157
+ HTMLFlipBook,
10158
+ {
10159
+ ref: flipBookRef,
10160
+ width: displayWidth,
10161
+ height: displayHeight,
10162
+ size: "fixed",
10163
+ minWidth: displayWidth,
10164
+ maxWidth: displayWidth,
10165
+ minHeight: displayHeight,
10166
+ maxHeight: displayHeight,
10167
+ drawShadow,
10168
+ maxShadowOpacity,
10169
+ flippingTime,
10170
+ usePortrait: true,
10171
+ startPage: currentPage - 1,
10172
+ showCover: false,
10173
+ mobileScrollSupport: true,
10174
+ swipeDistance: 30,
10175
+ showPageCorners: true,
10176
+ useMouseEvents: true,
10177
+ clickEventForward: false,
10178
+ onFlip: handleFlip,
10179
+ className: "book-flipbook",
10180
+ style: {},
10181
+ startZIndex: 0,
10182
+ autoSize: false,
10183
+ renderOnlyPageLengthChange: false,
10184
+ disableFlipByClick: false,
10185
+ children: pages.map((page, index) => /* @__PURE__ */ jsx27(
10186
+ BookPage,
10187
+ {
10188
+ pageNumber: index + 1,
10189
+ page,
10190
+ scale: renderScale,
10191
+ rotation,
10192
+ width: displayWidth,
10193
+ height: displayHeight
10194
+ },
10195
+ index
10196
+ ))
10197
+ }
10198
+ )
10199
+ ]
10200
+ }
10201
+ );
10202
+ });
10203
+ }
10204
+ });
10205
+
10206
+ // src/components/FloatingZoomControls/FloatingZoomControls.tsx
10207
+ import { memo as memo27, useCallback as useCallback33 } from "react";
10208
+ import { jsx as jsx28, jsxs as jsxs24 } from "react/jsx-runtime";
9808
10209
  var FloatingZoomControls;
9809
10210
  var init_FloatingZoomControls = __esm({
9810
10211
  "src/components/FloatingZoomControls/FloatingZoomControls.tsx"() {
9811
10212
  "use strict";
9812
10213
  init_hooks();
9813
10214
  init_utils();
9814
- FloatingZoomControls = memo26(function FloatingZoomControls2({
9815
- position = "bottom-right",
10215
+ FloatingZoomControls = memo27(function FloatingZoomControls2({
10216
+ position: position2 = "bottom-right",
9816
10217
  className,
9817
10218
  showFitToWidth = true,
9818
10219
  showFitToPage = false,
@@ -9821,20 +10222,20 @@ var init_FloatingZoomControls = __esm({
9821
10222
  const { viewerStore } = usePDFViewerStores();
9822
10223
  const scale = useViewerStore((s) => s.scale);
9823
10224
  const document2 = useViewerStore((s) => s.document);
9824
- const handleZoomIn = useCallback32(() => {
10225
+ const handleZoomIn = useCallback33(() => {
9825
10226
  const currentScale = viewerStore.getState().scale;
9826
10227
  const newScale = Math.min(4, currentScale + 0.05);
9827
10228
  viewerStore.getState().setScale(newScale);
9828
10229
  }, [viewerStore]);
9829
- const handleZoomOut = useCallback32(() => {
10230
+ const handleZoomOut = useCallback33(() => {
9830
10231
  const currentScale = viewerStore.getState().scale;
9831
10232
  const newScale = Math.max(0.1, currentScale - 0.05);
9832
10233
  viewerStore.getState().setScale(newScale);
9833
10234
  }, [viewerStore]);
9834
- const handleFitToWidth = useCallback32(() => {
10235
+ const handleFitToWidth = useCallback33(() => {
9835
10236
  viewerStore.getState().setScale(1);
9836
10237
  }, [viewerStore]);
9837
- const handleFitToPage = useCallback32(() => {
10238
+ const handleFitToPage = useCallback33(() => {
9838
10239
  viewerStore.getState().setScale(0.75);
9839
10240
  }, [viewerStore]);
9840
10241
  if (!document2) return null;
@@ -9845,7 +10246,7 @@ var init_FloatingZoomControls = __esm({
9845
10246
  "top-left": "top-4 left-4"
9846
10247
  };
9847
10248
  const zoomPercentage = Math.round(scale * 100);
9848
- return /* @__PURE__ */ jsxs23(
10249
+ return /* @__PURE__ */ jsxs24(
9849
10250
  "div",
9850
10251
  {
9851
10252
  className: cn(
@@ -9853,11 +10254,11 @@ var init_FloatingZoomControls = __esm({
9853
10254
  "bg-white dark:bg-gray-800 rounded-lg shadow-lg",
9854
10255
  "border border-gray-200 dark:border-gray-700",
9855
10256
  "p-1",
9856
- positionClasses[position],
10257
+ positionClasses[position2],
9857
10258
  className
9858
10259
  ),
9859
10260
  children: [
9860
- /* @__PURE__ */ jsx27(
10261
+ /* @__PURE__ */ jsx28(
9861
10262
  "button",
9862
10263
  {
9863
10264
  onClick: handleZoomOut,
@@ -9871,14 +10272,14 @@ var init_FloatingZoomControls = __esm({
9871
10272
  disabled: scale <= 0.25,
9872
10273
  title: "Zoom Out",
9873
10274
  "aria-label": "Zoom Out",
9874
- children: /* @__PURE__ */ jsx27("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx27("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M20 12H4" }) })
10275
+ children: /* @__PURE__ */ jsx28("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx28("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M20 12H4" }) })
9875
10276
  }
9876
10277
  ),
9877
- showZoomLevel && /* @__PURE__ */ jsxs23("span", { className: "min-w-[48px] text-center text-sm font-medium text-gray-700 dark:text-gray-300", children: [
10278
+ showZoomLevel && /* @__PURE__ */ jsxs24("span", { className: "min-w-[48px] text-center text-sm font-medium text-gray-700 dark:text-gray-300", children: [
9878
10279
  zoomPercentage,
9879
10280
  "%"
9880
10281
  ] }),
9881
- /* @__PURE__ */ jsx27(
10282
+ /* @__PURE__ */ jsx28(
9882
10283
  "button",
9883
10284
  {
9884
10285
  onClick: handleZoomIn,
@@ -9892,11 +10293,11 @@ var init_FloatingZoomControls = __esm({
9892
10293
  disabled: scale >= 4,
9893
10294
  title: "Zoom In",
9894
10295
  "aria-label": "Zoom In",
9895
- children: /* @__PURE__ */ jsx27("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx27("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M12 4v16m8-8H4" }) })
10296
+ children: /* @__PURE__ */ jsx28("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx28("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M12 4v16m8-8H4" }) })
9896
10297
  }
9897
10298
  ),
9898
- (showFitToWidth || showFitToPage) && /* @__PURE__ */ jsx27("div", { className: "w-px h-6 bg-gray-200 dark:bg-gray-700 mx-1" }),
9899
- showFitToWidth && /* @__PURE__ */ jsx27(
10299
+ (showFitToWidth || showFitToPage) && /* @__PURE__ */ jsx28("div", { className: "w-px h-6 bg-gray-200 dark:bg-gray-700 mx-1" }),
10300
+ showFitToWidth && /* @__PURE__ */ jsx28(
9900
10301
  "button",
9901
10302
  {
9902
10303
  onClick: handleFitToWidth,
@@ -9908,10 +10309,10 @@ var init_FloatingZoomControls = __esm({
9908
10309
  ),
9909
10310
  title: "Fit to Width",
9910
10311
  "aria-label": "Fit to Width",
9911
- children: /* @__PURE__ */ jsx27("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx27("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4" }) })
10312
+ children: /* @__PURE__ */ jsx28("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx28("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4" }) })
9912
10313
  }
9913
10314
  ),
9914
- showFitToPage && /* @__PURE__ */ jsx27(
10315
+ showFitToPage && /* @__PURE__ */ jsx28(
9915
10316
  "button",
9916
10317
  {
9917
10318
  onClick: handleFitToPage,
@@ -9923,7 +10324,7 @@ var init_FloatingZoomControls = __esm({
9923
10324
  ),
9924
10325
  title: "Fit to Page",
9925
10326
  "aria-label": "Fit to Page",
9926
- children: /* @__PURE__ */ jsx27("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx27("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" }) })
10327
+ children: /* @__PURE__ */ jsx28("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx28("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" }) })
9927
10328
  }
9928
10329
  )
9929
10330
  ]
@@ -9947,14 +10348,14 @@ __export(PDFViewerClient_exports, {
9947
10348
  PDFViewerClient: () => PDFViewerClient
9948
10349
  });
9949
10350
  import {
9950
- useEffect as useEffect22,
9951
- useCallback as useCallback33,
9952
- memo as memo27,
9953
- useRef as useRef19,
9954
- useState as useState21,
10351
+ useEffect as useEffect23,
10352
+ useCallback as useCallback34,
10353
+ memo as memo28,
10354
+ useRef as useRef20,
10355
+ useState as useState22,
9955
10356
  forwardRef
9956
10357
  } from "react";
9957
- import { jsx as jsx28, jsxs as jsxs24 } from "react/jsx-runtime";
10358
+ import { jsx as jsx29, jsxs as jsxs25 } from "react/jsx-runtime";
9958
10359
  function getSrcIdentifier(src) {
9959
10360
  if (typeof src === "string") {
9960
10361
  return src;
@@ -10006,10 +10407,11 @@ var init_PDFViewerClient = __esm({
10006
10407
  init_DocumentContainer();
10007
10408
  init_ContinuousScrollContainer();
10008
10409
  init_DualPageContainer();
10410
+ init_BookModeContainer();
10009
10411
  init_FloatingZoomControls2();
10010
10412
  init_PDFLoadingScreen2();
10011
10413
  init_utils();
10012
- PDFViewerInner = memo27(function PDFViewerInner2({
10414
+ PDFViewerInner = memo28(function PDFViewerInner2({
10013
10415
  src,
10014
10416
  initialPage = 1,
10015
10417
  page: controlledPage,
@@ -10036,19 +10438,19 @@ var init_PDFViewerClient = __esm({
10036
10438
  onReady
10037
10439
  }) {
10038
10440
  const { viewerStore, annotationStore, searchStore } = usePDFViewerStores();
10039
- const mountedRef = useRef19(true);
10040
- const [, setLoadState] = useState21("idle");
10041
- const onDocumentLoadRef = useRef19(onDocumentLoad);
10042
- const onErrorRef = useRef19(onError);
10043
- const onPageChangeRef = useRef19(onPageChange);
10044
- const onScaleChangeRef = useRef19(onScaleChange);
10045
- const onZoomChangeRef = useRef19(onZoomChange);
10046
- const onPageRenderStartRef = useRef19(onPageRenderStart);
10047
- const onPageRenderCompleteRef = useRef19(onPageRenderComplete);
10048
- const onHighlightAddedRef = useRef19(onHighlightAdded);
10049
- const onHighlightRemovedRef = useRef19(onHighlightRemoved);
10050
- const onAnnotationAddedRef = useRef19(onAnnotationAdded);
10051
- const onReadyRef = useRef19(onReady);
10441
+ const mountedRef = useRef20(true);
10442
+ const [, setLoadState] = useState22("idle");
10443
+ const onDocumentLoadRef = useRef20(onDocumentLoad);
10444
+ const onErrorRef = useRef20(onError);
10445
+ const onPageChangeRef = useRef20(onPageChange);
10446
+ const onScaleChangeRef = useRef20(onScaleChange);
10447
+ const onZoomChangeRef = useRef20(onZoomChange);
10448
+ const onPageRenderStartRef = useRef20(onPageRenderStart);
10449
+ const onPageRenderCompleteRef = useRef20(onPageRenderComplete);
10450
+ const onHighlightAddedRef = useRef20(onHighlightAdded);
10451
+ const onHighlightRemovedRef = useRef20(onHighlightRemoved);
10452
+ const onAnnotationAddedRef = useRef20(onAnnotationAdded);
10453
+ const onReadyRef = useRef20(onReady);
10052
10454
  onDocumentLoadRef.current = onDocumentLoad;
10053
10455
  onErrorRef.current = onError;
10054
10456
  onPageChangeRef.current = onPageChange;
@@ -10061,8 +10463,8 @@ var init_PDFViewerClient = __esm({
10061
10463
  onAnnotationAddedRef.current = onAnnotationAdded;
10062
10464
  onReadyRef.current = onReady;
10063
10465
  const isControlled = controlledPage !== void 0;
10064
- const prevControlledPageRef = useRef19(controlledPage);
10065
- const srcIdRef = useRef19(null);
10466
+ const prevControlledPageRef = useRef20(controlledPage);
10467
+ const srcIdRef = useRef20(null);
10066
10468
  const currentPage = useViewerStore((s) => s.currentPage);
10067
10469
  const scale = useViewerStore((s) => s.scale);
10068
10470
  const theme = useViewerStore((s) => s.theme);
@@ -10072,8 +10474,8 @@ var init_PDFViewerClient = __esm({
10072
10474
  const sidebarOpen = useViewerStore((s) => s.sidebarOpen);
10073
10475
  const streamingProgress = useViewerStore((s) => s.streamingProgress);
10074
10476
  const srcId = getSrcIdentifier(src);
10075
- const handleRef = useRef19(null);
10076
- useEffect22(() => {
10477
+ const handleRef = useRef20(null);
10478
+ useEffect23(() => {
10077
10479
  const handle = {
10078
10480
  // ==================== Text Highlighting ====================
10079
10481
  highlightText: async (text, options) => {
@@ -10491,14 +10893,14 @@ var init_PDFViewerClient = __esm({
10491
10893
  handleRef.current = handle;
10492
10894
  onReadyRef.current?.(handle);
10493
10895
  }, [viewerStore, annotationStore, searchStore]);
10494
- const handleRetry = useCallback33(() => {
10896
+ const handleRetry = useCallback34(() => {
10495
10897
  srcIdRef.current = null;
10496
10898
  viewerStore.getState().setError(null);
10497
10899
  setLoadState("idle");
10498
10900
  }, [viewerStore]);
10499
- const abortControllerRef = useRef19(null);
10500
- const currentSrcRef = useRef19(null);
10501
- useEffect22(() => {
10901
+ const abortControllerRef = useRef20(null);
10902
+ const currentSrcRef = useRef20(null);
10903
+ useEffect23(() => {
10502
10904
  mountedRef.current = true;
10503
10905
  return () => {
10504
10906
  mountedRef.current = false;
@@ -10523,8 +10925,8 @@ var init_PDFViewerClient = __esm({
10523
10925
  viewerStore.getState().setError(null);
10524
10926
  };
10525
10927
  }, [viewerStore]);
10526
- const cancelLoaderRef = useRef19(null);
10527
- useEffect22(() => {
10928
+ const cancelLoaderRef = useRef20(null);
10929
+ useEffect23(() => {
10528
10930
  if (srcIdRef.current === srcId && viewerStore.getState().document) {
10529
10931
  return;
10530
10932
  }
@@ -10565,12 +10967,12 @@ var init_PDFViewerClient = __esm({
10565
10967
  src,
10566
10968
  workerSrc,
10567
10969
  signal: abortController.signal,
10568
- onProgress: ({ loaded, total }) => {
10970
+ onProgress: ({ loaded: loaded2, total }) => {
10569
10971
  if (!mountedRef.current || srcIdRef.current !== loadId || abortController.signal.aborted) {
10570
10972
  return;
10571
10973
  }
10572
10974
  const now = Date.now();
10573
- const percent = total > 0 ? Math.round(loaded / total * 100) : 0;
10975
+ const percent = total > 0 ? Math.round(loaded2 / total * 100) : 0;
10574
10976
  const timePassed = now - lastProgressUpdate >= PROGRESS_THROTTLE_MS;
10575
10977
  const percentChanged = Math.abs(percent - lastPercent) >= PROGRESS_MIN_CHANGE;
10576
10978
  const isComplete = percent >= 100;
@@ -10581,10 +10983,10 @@ var init_PDFViewerClient = __esm({
10581
10983
  loadingProgress: {
10582
10984
  phase: "fetching",
10583
10985
  percent,
10584
- bytesLoaded: loaded,
10986
+ bytesLoaded: loaded2,
10585
10987
  totalBytes: total
10586
10988
  },
10587
- streamingProgress: { loaded, total },
10989
+ streamingProgress: { loaded: loaded2, total },
10588
10990
  documentLoadingState: "loading"
10589
10991
  });
10590
10992
  }
@@ -10658,22 +11060,22 @@ var init_PDFViewerClient = __esm({
10658
11060
  }
10659
11061
  };
10660
11062
  }, [srcId, src, workerSrc, initialPage, initialScale, viewerStore]);
10661
- const prevPageRef = useRef19(currentPage);
10662
- useEffect22(() => {
11063
+ const prevPageRef = useRef20(currentPage);
11064
+ useEffect23(() => {
10663
11065
  if (prevPageRef.current !== currentPage) {
10664
11066
  prevPageRef.current = currentPage;
10665
11067
  onPageChangeRef.current?.(currentPage);
10666
11068
  }
10667
11069
  }, [currentPage]);
10668
- const prevScaleRef = useRef19(scale);
10669
- useEffect22(() => {
11070
+ const prevScaleRef = useRef20(scale);
11071
+ useEffect23(() => {
10670
11072
  if (prevScaleRef.current !== scale) {
10671
11073
  prevScaleRef.current = scale;
10672
11074
  onScaleChangeRef.current?.(scale);
10673
11075
  onZoomChangeRef.current?.(scale);
10674
11076
  }
10675
11077
  }, [scale]);
10676
- useEffect22(() => {
11078
+ useEffect23(() => {
10677
11079
  if (!isControlled || controlledPage === void 0) return;
10678
11080
  if (prevControlledPageRef.current === controlledPage) return;
10679
11081
  prevControlledPageRef.current = controlledPage;
@@ -10686,7 +11088,7 @@ var init_PDFViewerClient = __esm({
10686
11088
  if (error) {
10687
11089
  if (errorComponent) {
10688
11090
  const errorContent = typeof errorComponent === "function" ? errorComponent(error, handleRetry) : errorComponent;
10689
- return /* @__PURE__ */ jsx28(
11091
+ return /* @__PURE__ */ jsx29(
10690
11092
  "div",
10691
11093
  {
10692
11094
  className: cn(
@@ -10700,7 +11102,7 @@ var init_PDFViewerClient = __esm({
10700
11102
  }
10701
11103
  );
10702
11104
  }
10703
- return /* @__PURE__ */ jsx28(
11105
+ return /* @__PURE__ */ jsx29(
10704
11106
  "div",
10705
11107
  {
10706
11108
  className: cn(
@@ -10710,10 +11112,10 @@ var init_PDFViewerClient = __esm({
10710
11112
  themeClass,
10711
11113
  className
10712
11114
  ),
10713
- children: /* @__PURE__ */ jsx28("div", { className: "flex-1 flex items-center justify-center", children: /* @__PURE__ */ jsxs24("div", { className: "text-center p-8", children: [
10714
- /* @__PURE__ */ jsx28("div", { className: "text-red-500 text-lg font-semibold mb-2", children: "Failed to load PDF" }),
10715
- /* @__PURE__ */ jsx28("div", { className: "text-gray-500 text-sm", children: error.message }),
10716
- /* @__PURE__ */ jsx28(
11115
+ children: /* @__PURE__ */ jsx29("div", { className: "flex-1 flex items-center justify-center", children: /* @__PURE__ */ jsxs25("div", { className: "text-center p-8", children: [
11116
+ /* @__PURE__ */ jsx29("div", { className: "text-red-500 text-lg font-semibold mb-2", children: "Failed to load PDF" }),
11117
+ /* @__PURE__ */ jsx29("div", { className: "text-gray-500 text-sm", children: error.message }),
11118
+ /* @__PURE__ */ jsx29(
10717
11119
  "button",
10718
11120
  {
10719
11121
  onClick: handleRetry,
@@ -10728,15 +11130,17 @@ var init_PDFViewerClient = __esm({
10728
11130
  const renderContainer = () => {
10729
11131
  switch (viewMode) {
10730
11132
  case "continuous":
10731
- return /* @__PURE__ */ jsx28(ContinuousScrollContainer, {});
11133
+ return /* @__PURE__ */ jsx29(ContinuousScrollContainer, {});
10732
11134
  case "dual":
10733
- return /* @__PURE__ */ jsx28(DualPageContainer, {});
11135
+ return /* @__PURE__ */ jsx29(DualPageContainer, {});
11136
+ case "book":
11137
+ return /* @__PURE__ */ jsx29(BookModeContainer, {});
10734
11138
  case "single":
10735
11139
  default:
10736
- return /* @__PURE__ */ jsx28(DocumentContainer, {});
11140
+ return /* @__PURE__ */ jsx29(DocumentContainer, {});
10737
11141
  }
10738
11142
  };
10739
- return /* @__PURE__ */ jsxs24(
11143
+ return /* @__PURE__ */ jsxs25(
10740
11144
  "div",
10741
11145
  {
10742
11146
  className: cn(
@@ -10748,14 +11152,14 @@ var init_PDFViewerClient = __esm({
10748
11152
  className
10749
11153
  ),
10750
11154
  children: [
10751
- showToolbar && /* @__PURE__ */ jsx28(Toolbar, {}),
10752
- showAnnotationToolbar && /* @__PURE__ */ jsx28(AnnotationToolbar, {}),
10753
- /* @__PURE__ */ jsxs24("div", { className: "flex flex-1 overflow-hidden", children: [
10754
- showSidebar && sidebarOpen && /* @__PURE__ */ jsx28(Sidebar, {}),
11155
+ showToolbar && /* @__PURE__ */ jsx29(Toolbar, {}),
11156
+ showAnnotationToolbar && /* @__PURE__ */ jsx29(AnnotationToolbar, {}),
11157
+ /* @__PURE__ */ jsxs25("div", { className: "flex flex-1 overflow-hidden", children: [
11158
+ showSidebar && sidebarOpen && /* @__PURE__ */ jsx29(Sidebar, {}),
10755
11159
  renderContainer()
10756
11160
  ] }),
10757
- showFloatingZoom && /* @__PURE__ */ jsx28(FloatingZoomControls, { position: "bottom-right" }),
10758
- isLoading && /* @__PURE__ */ jsx28("div", { className: "absolute inset-0 z-50", children: loadingComponent ?? /* @__PURE__ */ jsx28(
11161
+ showFloatingZoom && /* @__PURE__ */ jsx29(FloatingZoomControls, { position: "bottom-right" }),
11162
+ isLoading && /* @__PURE__ */ jsx29("div", { className: "absolute inset-0 z-50", children: loadingComponent ?? /* @__PURE__ */ jsx29(
10759
11163
  PDFLoadingScreen,
10760
11164
  {
10761
11165
  phase: loadingProgress?.phase ?? "fetching",
@@ -10764,10 +11168,10 @@ var init_PDFViewerClient = __esm({
10764
11168
  totalBytes: loadingProgress?.totalBytes
10765
11169
  }
10766
11170
  ) }),
10767
- !isLoading && streamingProgress && streamingProgress.total > 0 && streamingProgress.loaded < streamingProgress.total && /* @__PURE__ */ jsxs24("div", { className: "absolute bottom-20 right-4 z-40 px-3 py-2 bg-gray-900/80 text-white text-xs rounded-lg shadow-lg flex items-center gap-2", children: [
10768
- /* @__PURE__ */ jsx28("div", { className: "w-3 h-3 border-2 border-white/30 border-t-white rounded-full animate-spin" }),
10769
- /* @__PURE__ */ jsx28("span", { children: "Loading pages..." }),
10770
- /* @__PURE__ */ jsxs24("span", { className: "text-white/60", children: [
11171
+ !isLoading && streamingProgress && streamingProgress.total > 0 && streamingProgress.loaded < streamingProgress.total && /* @__PURE__ */ jsxs25("div", { className: "absolute bottom-20 right-4 z-40 px-3 py-2 bg-gray-900/80 text-white text-xs rounded-lg shadow-lg flex items-center gap-2", children: [
11172
+ /* @__PURE__ */ jsx29("div", { className: "w-3 h-3 border-2 border-white/30 border-t-white rounded-full animate-spin" }),
11173
+ /* @__PURE__ */ jsx29("span", { children: "Loading pages..." }),
11174
+ /* @__PURE__ */ jsxs25("span", { className: "text-white/60", children: [
10771
11175
  Math.round(streamingProgress.loaded / streamingProgress.total * 100),
10772
11176
  "%"
10773
11177
  ] })
@@ -10778,8 +11182,8 @@ var init_PDFViewerClient = __esm({
10778
11182
  });
10779
11183
  PDFViewerInnerWithRef = forwardRef(
10780
11184
  function PDFViewerInnerWithRef2(props, ref) {
10781
- const handleRef = useRef19(null);
10782
- const handleReady = useCallback33((handle) => {
11185
+ const handleRef = useRef20(null);
11186
+ const handleReady = useCallback34((handle) => {
10783
11187
  handleRef.current = handle;
10784
11188
  if (typeof ref === "function") {
10785
11189
  ref(handle);
@@ -10787,17 +11191,17 @@ var init_PDFViewerClient = __esm({
10787
11191
  ref.current = handle;
10788
11192
  }
10789
11193
  }, [ref]);
10790
- return /* @__PURE__ */ jsx28(PDFViewerInner, { ...props, onReady: handleReady });
11194
+ return /* @__PURE__ */ jsx29(PDFViewerInner, { ...props, onReady: handleReady });
10791
11195
  }
10792
11196
  );
10793
- PDFViewerClient = memo27(
11197
+ PDFViewerClient = memo28(
10794
11198
  forwardRef(function PDFViewerClient2(props, ref) {
10795
- return /* @__PURE__ */ jsx28(
11199
+ return /* @__PURE__ */ jsx29(
10796
11200
  PDFViewerProvider,
10797
11201
  {
10798
11202
  theme: props.theme,
10799
11203
  defaultSidebarPanel: props.defaultSidebarPanel,
10800
- children: /* @__PURE__ */ jsx28(PDFViewerInnerWithRef, { ref, ...props })
11204
+ children: /* @__PURE__ */ jsx29(PDFViewerInnerWithRef, { ref, ...props })
10801
11205
  }
10802
11206
  );
10803
11207
  })
@@ -10806,8 +11210,8 @@ var init_PDFViewerClient = __esm({
10806
11210
  });
10807
11211
 
10808
11212
  // src/components/PDFViewer/PDFViewer.tsx
10809
- import { lazy, Suspense, memo as memo28 } from "react";
10810
- import { jsx as jsx29, jsxs as jsxs25 } from "react/jsx-runtime";
11213
+ import { lazy, Suspense, memo as memo29 } from "react";
11214
+ import { jsx as jsx30, jsxs as jsxs26 } from "react/jsx-runtime";
10811
11215
  var PDFViewerClient3, PDFViewerLoading, PDFViewer;
10812
11216
  var init_PDFViewer = __esm({
10813
11217
  "src/components/PDFViewer/PDFViewer.tsx"() {
@@ -10816,10 +11220,10 @@ var init_PDFViewer = __esm({
10816
11220
  PDFViewerClient3 = lazy(
10817
11221
  () => Promise.resolve().then(() => (init_PDFViewerClient(), PDFViewerClient_exports)).then((mod) => ({ default: mod.PDFViewerClient }))
10818
11222
  );
10819
- PDFViewerLoading = memo28(function PDFViewerLoading2({
11223
+ PDFViewerLoading = memo29(function PDFViewerLoading2({
10820
11224
  className
10821
11225
  }) {
10822
- return /* @__PURE__ */ jsx29(
11226
+ return /* @__PURE__ */ jsx30(
10823
11227
  "div",
10824
11228
  {
10825
11229
  className: cn(
@@ -10828,18 +11232,18 @@ var init_PDFViewer = __esm({
10828
11232
  "bg-white dark:bg-gray-900",
10829
11233
  className
10830
11234
  ),
10831
- children: /* @__PURE__ */ jsx29("div", { className: "flex-1 flex items-center justify-center", children: /* @__PURE__ */ jsxs25("div", { className: "flex flex-col items-center", children: [
10832
- /* @__PURE__ */ jsx29("div", { className: "w-8 h-8 border-4 border-blue-500 border-t-transparent rounded-full animate-spin" }),
10833
- /* @__PURE__ */ jsx29("div", { className: "mt-2 text-sm text-gray-500", children: "Loading PDF viewer..." })
11235
+ children: /* @__PURE__ */ jsx30("div", { className: "flex-1 flex items-center justify-center", children: /* @__PURE__ */ jsxs26("div", { className: "flex flex-col items-center", children: [
11236
+ /* @__PURE__ */ jsx30("div", { className: "w-8 h-8 border-4 border-blue-500 border-t-transparent rounded-full animate-spin" }),
11237
+ /* @__PURE__ */ jsx30("div", { className: "mt-2 text-sm text-gray-500", children: "Loading PDF viewer..." })
10834
11238
  ] }) })
10835
11239
  }
10836
11240
  );
10837
11241
  });
10838
- PDFViewer = memo28(function PDFViewer2(props) {
11242
+ PDFViewer = memo29(function PDFViewer2(props) {
10839
11243
  if (typeof window === "undefined") {
10840
- return /* @__PURE__ */ jsx29(PDFViewerLoading, { className: props.className });
11244
+ return /* @__PURE__ */ jsx30(PDFViewerLoading, { className: props.className });
10841
11245
  }
10842
- return /* @__PURE__ */ jsx29(Suspense, { fallback: /* @__PURE__ */ jsx29(PDFViewerLoading, { className: props.className }), children: /* @__PURE__ */ jsx29(PDFViewerClient3, { ...props }) });
11246
+ return /* @__PURE__ */ jsx30(Suspense, { fallback: /* @__PURE__ */ jsx30(PDFViewerLoading, { className: props.className }), children: /* @__PURE__ */ jsx30(PDFViewerClient3, { ...props }) });
10843
11247
  });
10844
11248
  }
10845
11249
  });
@@ -10854,6 +11258,7 @@ var init_PDFViewer2 = __esm({
10854
11258
  init_VirtualizedDocumentContainer();
10855
11259
  init_ContinuousScrollContainer();
10856
11260
  init_DualPageContainer();
11261
+ init_BookModeContainer();
10857
11262
  }
10858
11263
  });
10859
11264
 
@@ -10868,8 +11273,8 @@ init_AnnotationToolbar2();
10868
11273
 
10869
11274
  // src/components/Annotations/StickyNote.tsx
10870
11275
  init_utils();
10871
- import { memo as memo29, useState as useState22, useRef as useRef20, useEffect as useEffect23, useCallback as useCallback34 } from "react";
10872
- import { jsx as jsx30, jsxs as jsxs26 } from "react/jsx-runtime";
11276
+ import { memo as memo30, useState as useState23, useRef as useRef21, useEffect as useEffect24, useCallback as useCallback35 } from "react";
11277
+ import { jsx as jsx31, jsxs as jsxs27 } from "react/jsx-runtime";
10873
11278
  var NOTE_COLORS = [
10874
11279
  "#fef08a",
10875
11280
  // yellow
@@ -10882,7 +11287,7 @@ var NOTE_COLORS = [
10882
11287
  "#fed7aa"
10883
11288
  // orange
10884
11289
  ];
10885
- var StickyNote = memo29(function StickyNote2({
11290
+ var StickyNote = memo30(function StickyNote2({
10886
11291
  note,
10887
11292
  scale,
10888
11293
  isSelected,
@@ -10895,37 +11300,37 @@ var StickyNote = memo29(function StickyNote2({
10895
11300
  onDragStart,
10896
11301
  className
10897
11302
  }) {
10898
- const [isExpanded, setIsExpanded] = useState22(false);
10899
- const [localContent, setLocalContent] = useState22(note.content);
10900
- const textareaRef = useRef20(null);
10901
- const noteRef = useRef20(null);
10902
- useEffect23(() => {
11303
+ const [isExpanded, setIsExpanded] = useState23(false);
11304
+ const [localContent, setLocalContent] = useState23(note.content);
11305
+ const textareaRef = useRef21(null);
11306
+ const noteRef = useRef21(null);
11307
+ useEffect24(() => {
10903
11308
  setLocalContent(note.content);
10904
11309
  }, [note.content]);
10905
- useEffect23(() => {
11310
+ useEffect24(() => {
10906
11311
  if (isEditing && textareaRef.current) {
10907
11312
  textareaRef.current.focus();
10908
11313
  textareaRef.current.select();
10909
11314
  }
10910
11315
  }, [isEditing]);
10911
- const handleClick = useCallback34((e) => {
11316
+ const handleClick = useCallback35((e) => {
10912
11317
  e.stopPropagation();
10913
11318
  onSelect?.();
10914
11319
  if (!isExpanded) {
10915
11320
  setIsExpanded(true);
10916
11321
  }
10917
11322
  }, [isExpanded, onSelect]);
10918
- const handleDoubleClick = useCallback34((e) => {
11323
+ const handleDoubleClick = useCallback35((e) => {
10919
11324
  e.stopPropagation();
10920
11325
  onStartEdit?.();
10921
11326
  }, [onStartEdit]);
10922
- const handleBlur = useCallback34(() => {
11327
+ const handleBlur = useCallback35(() => {
10923
11328
  if (isEditing && localContent !== note.content) {
10924
11329
  onUpdate?.({ content: localContent });
10925
11330
  }
10926
11331
  onEndEdit?.();
10927
11332
  }, [isEditing, localContent, note.content, onUpdate, onEndEdit]);
10928
- const handleKeyDown = useCallback34((e) => {
11333
+ const handleKeyDown = useCallback35((e) => {
10929
11334
  if (e.key === "Escape") {
10930
11335
  setLocalContent(note.content);
10931
11336
  onEndEdit?.();
@@ -10933,16 +11338,16 @@ var StickyNote = memo29(function StickyNote2({
10933
11338
  handleBlur();
10934
11339
  }
10935
11340
  }, [note.content, onEndEdit, handleBlur]);
10936
- const handleColorChange = useCallback34((color) => {
11341
+ const handleColorChange = useCallback35((color) => {
10937
11342
  onUpdate?.({ color });
10938
11343
  }, [onUpdate]);
10939
- const handleCollapse = useCallback34((e) => {
11344
+ const handleCollapse = useCallback35((e) => {
10940
11345
  e.stopPropagation();
10941
11346
  setIsExpanded(false);
10942
11347
  onEndEdit?.();
10943
11348
  }, [onEndEdit]);
10944
11349
  if (!isExpanded) {
10945
- return /* @__PURE__ */ jsx30(
11350
+ return /* @__PURE__ */ jsx31(
10946
11351
  "div",
10947
11352
  {
10948
11353
  ref: noteRef,
@@ -10963,14 +11368,14 @@ var StickyNote = memo29(function StickyNote2({
10963
11368
  onMouseDown: onDragStart,
10964
11369
  onTouchStart: onDragStart,
10965
11370
  title: note.content || "Empty note",
10966
- children: /* @__PURE__ */ jsx30(
11371
+ children: /* @__PURE__ */ jsx31(
10967
11372
  "svg",
10968
11373
  {
10969
11374
  className: "w-4 h-4 opacity-70",
10970
11375
  fill: "currentColor",
10971
11376
  viewBox: "0 0 20 20",
10972
11377
  style: { color: "#333" },
10973
- children: /* @__PURE__ */ jsx30(
11378
+ children: /* @__PURE__ */ jsx31(
10974
11379
  "path",
10975
11380
  {
10976
11381
  fillRule: "evenodd",
@@ -10983,7 +11388,7 @@ var StickyNote = memo29(function StickyNote2({
10983
11388
  }
10984
11389
  );
10985
11390
  }
10986
- return /* @__PURE__ */ jsxs26(
11391
+ return /* @__PURE__ */ jsxs27(
10987
11392
  "div",
10988
11393
  {
10989
11394
  ref: noteRef,
@@ -11001,14 +11406,14 @@ var StickyNote = memo29(function StickyNote2({
11001
11406
  },
11002
11407
  onClick: handleClick,
11003
11408
  children: [
11004
- /* @__PURE__ */ jsxs26(
11409
+ /* @__PURE__ */ jsxs27(
11005
11410
  "div",
11006
11411
  {
11007
11412
  className: "flex items-center justify-between px-2 py-1 border-b border-black/10 cursor-move",
11008
11413
  onMouseDown: onDragStart,
11009
11414
  onTouchStart: onDragStart,
11010
11415
  children: [
11011
- /* @__PURE__ */ jsx30("div", { className: "flex gap-1", children: NOTE_COLORS.map((color) => /* @__PURE__ */ jsx30(
11416
+ /* @__PURE__ */ jsx31("div", { className: "flex gap-1", children: NOTE_COLORS.map((color) => /* @__PURE__ */ jsx31(
11012
11417
  "button",
11013
11418
  {
11014
11419
  className: cn(
@@ -11025,8 +11430,8 @@ var StickyNote = memo29(function StickyNote2({
11025
11430
  },
11026
11431
  color
11027
11432
  )) }),
11028
- /* @__PURE__ */ jsxs26("div", { className: "flex gap-1", children: [
11029
- /* @__PURE__ */ jsx30(
11433
+ /* @__PURE__ */ jsxs27("div", { className: "flex gap-1", children: [
11434
+ /* @__PURE__ */ jsx31(
11030
11435
  "button",
11031
11436
  {
11032
11437
  className: "p-0.5 hover:bg-black/10 rounded",
@@ -11035,23 +11440,23 @@ var StickyNote = memo29(function StickyNote2({
11035
11440
  onDelete?.();
11036
11441
  },
11037
11442
  title: "Delete note",
11038
- children: /* @__PURE__ */ jsx30("svg", { className: "w-3.5 h-3.5 text-gray-600", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ jsx30("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" }) })
11443
+ children: /* @__PURE__ */ jsx31("svg", { className: "w-3.5 h-3.5 text-gray-600", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ jsx31("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" }) })
11039
11444
  }
11040
11445
  ),
11041
- /* @__PURE__ */ jsx30(
11446
+ /* @__PURE__ */ jsx31(
11042
11447
  "button",
11043
11448
  {
11044
11449
  className: "p-0.5 hover:bg-black/10 rounded",
11045
11450
  onClick: handleCollapse,
11046
11451
  title: "Collapse note",
11047
- children: /* @__PURE__ */ jsx30("svg", { className: "w-3.5 h-3.5 text-gray-600", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ jsx30("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) })
11452
+ children: /* @__PURE__ */ jsx31("svg", { className: "w-3.5 h-3.5 text-gray-600", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ jsx31("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) })
11048
11453
  }
11049
11454
  )
11050
11455
  ] })
11051
11456
  ]
11052
11457
  }
11053
11458
  ),
11054
- /* @__PURE__ */ jsx30("div", { className: "p-2", children: isEditing ? /* @__PURE__ */ jsx30(
11459
+ /* @__PURE__ */ jsx31("div", { className: "p-2", children: isEditing ? /* @__PURE__ */ jsx31(
11055
11460
  "textarea",
11056
11461
  {
11057
11462
  ref: textareaRef,
@@ -11066,7 +11471,7 @@ var StickyNote = memo29(function StickyNote2({
11066
11471
  onKeyDown: handleKeyDown,
11067
11472
  placeholder: "Enter note..."
11068
11473
  }
11069
- ) : /* @__PURE__ */ jsx30(
11474
+ ) : /* @__PURE__ */ jsx31(
11070
11475
  "div",
11071
11476
  {
11072
11477
  className: cn(
@@ -11077,7 +11482,7 @@ var StickyNote = memo29(function StickyNote2({
11077
11482
  children: note.content || "Double-click to edit..."
11078
11483
  }
11079
11484
  ) }),
11080
- /* @__PURE__ */ jsx30("div", { className: "px-2 pb-1 text-[10px] text-gray-500", children: new Date(note.updatedAt).toLocaleDateString() })
11485
+ /* @__PURE__ */ jsx31("div", { className: "px-2 pb-1 text-[10px] text-gray-500", children: new Date(note.updatedAt).toLocaleDateString() })
11081
11486
  ]
11082
11487
  }
11083
11488
  );
@@ -11085,8 +11490,8 @@ var StickyNote = memo29(function StickyNote2({
11085
11490
 
11086
11491
  // src/components/Annotations/DrawingCanvas.tsx
11087
11492
  init_utils();
11088
- import { memo as memo30, useRef as useRef21, useCallback as useCallback35, useState as useState23 } from "react";
11089
- import { jsx as jsx31 } from "react/jsx-runtime";
11493
+ import { memo as memo31, useRef as useRef22, useCallback as useCallback36, useState as useState24 } from "react";
11494
+ import { jsx as jsx32 } from "react/jsx-runtime";
11090
11495
  function pointsToSvgPath(points) {
11091
11496
  if (points.length === 0) return "";
11092
11497
  if (points.length === 1) {
@@ -11124,7 +11529,7 @@ function simplifyPath(points, tolerance = 1) {
11124
11529
  result.push(points[points.length - 1]);
11125
11530
  return result;
11126
11531
  }
11127
- var DrawingCanvas = memo30(function DrawingCanvas2({
11532
+ var DrawingCanvas = memo31(function DrawingCanvas2({
11128
11533
  width,
11129
11534
  height,
11130
11535
  scale,
@@ -11134,10 +11539,10 @@ var DrawingCanvas = memo30(function DrawingCanvas2({
11134
11539
  onDrawingComplete,
11135
11540
  className
11136
11541
  }) {
11137
- const svgRef = useRef21(null);
11138
- const [isDrawing, setIsDrawing] = useState23(false);
11139
- const [currentPath, setCurrentPath] = useState23([]);
11140
- const getPoint = useCallback35((e) => {
11542
+ const svgRef = useRef22(null);
11543
+ const [isDrawing, setIsDrawing] = useState24(false);
11544
+ const [currentPath, setCurrentPath] = useState24([]);
11545
+ const getPoint = useCallback36((e) => {
11141
11546
  if (!svgRef.current) return null;
11142
11547
  const svg = svgRef.current;
11143
11548
  const rect = svg.getBoundingClientRect();
@@ -11157,7 +11562,7 @@ var DrawingCanvas = memo30(function DrawingCanvas2({
11157
11562
  y: (clientY - rect.top) / scale
11158
11563
  };
11159
11564
  }, [scale]);
11160
- const handleStart = useCallback35((e) => {
11565
+ const handleStart = useCallback36((e) => {
11161
11566
  if (!isActive) return;
11162
11567
  const point = getPoint(e);
11163
11568
  if (point) {
@@ -11165,14 +11570,14 @@ var DrawingCanvas = memo30(function DrawingCanvas2({
11165
11570
  setCurrentPath([point]);
11166
11571
  }
11167
11572
  }, [isActive, getPoint]);
11168
- const handleMove = useCallback35((e) => {
11573
+ const handleMove = useCallback36((e) => {
11169
11574
  if (!isDrawing || !isActive) return;
11170
11575
  const point = getPoint(e);
11171
11576
  if (point) {
11172
11577
  setCurrentPath((prev) => [...prev, point]);
11173
11578
  }
11174
11579
  }, [isDrawing, isActive, getPoint]);
11175
- const handleEnd = useCallback35(() => {
11580
+ const handleEnd = useCallback36(() => {
11176
11581
  if (!isDrawing) return;
11177
11582
  setIsDrawing(false);
11178
11583
  if (currentPath.length >= 2) {
@@ -11181,7 +11586,7 @@ var DrawingCanvas = memo30(function DrawingCanvas2({
11181
11586
  }
11182
11587
  setCurrentPath([]);
11183
11588
  }, [isDrawing, currentPath, onDrawingComplete]);
11184
- return /* @__PURE__ */ jsx31(
11589
+ return /* @__PURE__ */ jsx32(
11185
11590
  "svg",
11186
11591
  {
11187
11592
  ref: svgRef,
@@ -11201,7 +11606,7 @@ var DrawingCanvas = memo30(function DrawingCanvas2({
11201
11606
  onTouchStart: handleStart,
11202
11607
  onTouchMove: handleMove,
11203
11608
  onTouchEnd: handleEnd,
11204
- children: isDrawing && currentPath.length > 0 && /* @__PURE__ */ jsx31(
11609
+ children: isDrawing && currentPath.length > 0 && /* @__PURE__ */ jsx32(
11205
11610
  "path",
11206
11611
  {
11207
11612
  d: pointsToSvgPath(currentPath),
@@ -11219,9 +11624,9 @@ var DrawingCanvas = memo30(function DrawingCanvas2({
11219
11624
 
11220
11625
  // src/components/Annotations/ShapeRenderer.tsx
11221
11626
  init_utils();
11222
- import { memo as memo31, useCallback as useCallback36, useState as useState24, useRef as useRef22 } from "react";
11223
- import { jsx as jsx32, jsxs as jsxs27 } from "react/jsx-runtime";
11224
- var ShapeRenderer = memo31(function ShapeRenderer2({
11627
+ import { memo as memo32, useCallback as useCallback37, useState as useState25, useRef as useRef23 } from "react";
11628
+ import { jsx as jsx33, jsxs as jsxs28 } from "react/jsx-runtime";
11629
+ var ShapeRenderer = memo32(function ShapeRenderer2({
11225
11630
  shape,
11226
11631
  scale,
11227
11632
  isSelected,
@@ -11231,18 +11636,18 @@ var ShapeRenderer = memo31(function ShapeRenderer2({
11231
11636
  onDelete: _onDelete,
11232
11637
  className
11233
11638
  }) {
11234
- const [_isDragging, setIsDragging] = useState24(false);
11235
- const [_isResizing, setIsResizing] = useState24(false);
11236
- const [activeHandle, setActiveHandle] = useState24(null);
11237
- const startPosRef = useRef22({ x: 0, y: 0 });
11238
- const startShapeRef = useRef22({ x: 0, y: 0, width: 0, height: 0 });
11639
+ const [_isDragging, setIsDragging] = useState25(false);
11640
+ const [_isResizing, setIsResizing] = useState25(false);
11641
+ const [activeHandle, setActiveHandle] = useState25(null);
11642
+ const startPosRef = useRef23({ x: 0, y: 0 });
11643
+ const startShapeRef = useRef23({ x: 0, y: 0, width: 0, height: 0 });
11239
11644
  const { shapeType, x, y, width, height, color, strokeWidth, id: _id } = shape;
11240
11645
  const scaledX = x * scale;
11241
11646
  const scaledY = y * scale;
11242
11647
  const scaledWidth = width * scale;
11243
11648
  const scaledHeight = height * scale;
11244
11649
  const scaledStroke = strokeWidth * scale;
11245
- const getResizeHandles = useCallback36(() => {
11650
+ const getResizeHandles = useCallback37(() => {
11246
11651
  const handleSize = 8;
11247
11652
  const half = handleSize / 2;
11248
11653
  return [
@@ -11256,7 +11661,7 @@ var ShapeRenderer = memo31(function ShapeRenderer2({
11256
11661
  { position: "w", cursor: "ew-resize", x: scaledX - half, y: scaledY + scaledHeight / 2 - half }
11257
11662
  ];
11258
11663
  }, [scaledX, scaledY, scaledWidth, scaledHeight]);
11259
- const handleMouseDown = useCallback36((e, handle) => {
11664
+ const handleMouseDown = useCallback37((e, handle) => {
11260
11665
  e.stopPropagation();
11261
11666
  onSelect?.();
11262
11667
  if (!isEditing) return;
@@ -11332,7 +11737,7 @@ var ShapeRenderer = memo31(function ShapeRenderer2({
11332
11737
  document.addEventListener("mousemove", handleMouseMove);
11333
11738
  document.addEventListener("mouseup", handleMouseUp);
11334
11739
  }, [isEditing, x, y, width, height, scale, onSelect, onUpdate]);
11335
- const renderShape2 = useCallback36(() => {
11740
+ const renderShape2 = useCallback37(() => {
11336
11741
  const commonProps = {
11337
11742
  stroke: color,
11338
11743
  strokeWidth: scaledStroke,
@@ -11344,7 +11749,7 @@ var ShapeRenderer = memo31(function ShapeRenderer2({
11344
11749
  };
11345
11750
  switch (shapeType) {
11346
11751
  case "rect":
11347
- return /* @__PURE__ */ jsx32(
11752
+ return /* @__PURE__ */ jsx33(
11348
11753
  "rect",
11349
11754
  {
11350
11755
  x: scaledX,
@@ -11355,7 +11760,7 @@ var ShapeRenderer = memo31(function ShapeRenderer2({
11355
11760
  }
11356
11761
  );
11357
11762
  case "circle":
11358
- return /* @__PURE__ */ jsx32(
11763
+ return /* @__PURE__ */ jsx33(
11359
11764
  "ellipse",
11360
11765
  {
11361
11766
  cx: scaledX + scaledWidth / 2,
@@ -11366,7 +11771,7 @@ var ShapeRenderer = memo31(function ShapeRenderer2({
11366
11771
  }
11367
11772
  );
11368
11773
  case "line":
11369
- return /* @__PURE__ */ jsx32(
11774
+ return /* @__PURE__ */ jsx33(
11370
11775
  "line",
11371
11776
  {
11372
11777
  x1: scaledX,
@@ -11386,22 +11791,22 @@ var ShapeRenderer = memo31(function ShapeRenderer2({
11386
11791
  const arrow1Y = endY - arrowLength * Math.sin(angle - arrowAngle);
11387
11792
  const arrow2X = endX - arrowLength * Math.cos(angle + arrowAngle);
11388
11793
  const arrow2Y = endY - arrowLength * Math.sin(angle + arrowAngle);
11389
- return /* @__PURE__ */ jsxs27("g", { children: [
11390
- /* @__PURE__ */ jsx32("line", { x1: scaledX, y1: scaledY, x2: endX, y2: endY, ...commonProps }),
11391
- /* @__PURE__ */ jsx32("line", { x1: endX, y1: endY, x2: arrow1X, y2: arrow1Y, ...commonProps }),
11392
- /* @__PURE__ */ jsx32("line", { x1: endX, y1: endY, x2: arrow2X, y2: arrow2Y, ...commonProps })
11794
+ return /* @__PURE__ */ jsxs28("g", { children: [
11795
+ /* @__PURE__ */ jsx33("line", { x1: scaledX, y1: scaledY, x2: endX, y2: endY, ...commonProps }),
11796
+ /* @__PURE__ */ jsx33("line", { x1: endX, y1: endY, x2: arrow1X, y2: arrow1Y, ...commonProps }),
11797
+ /* @__PURE__ */ jsx33("line", { x1: endX, y1: endY, x2: arrow2X, y2: arrow2Y, ...commonProps })
11393
11798
  ] });
11394
11799
  default:
11395
11800
  return null;
11396
11801
  }
11397
11802
  }, [shapeType, scaledX, scaledY, scaledWidth, scaledHeight, color, scaledStroke, isSelected]);
11398
- return /* @__PURE__ */ jsxs27(
11803
+ return /* @__PURE__ */ jsxs28(
11399
11804
  "g",
11400
11805
  {
11401
11806
  className: cn("shape-renderer", className),
11402
11807
  onMouseDown: (e) => handleMouseDown(e),
11403
11808
  children: [
11404
- /* @__PURE__ */ jsx32(
11809
+ /* @__PURE__ */ jsx33(
11405
11810
  "rect",
11406
11811
  {
11407
11812
  x: scaledX - 5,
@@ -11414,7 +11819,7 @@ var ShapeRenderer = memo31(function ShapeRenderer2({
11414
11819
  }
11415
11820
  ),
11416
11821
  renderShape2(),
11417
- isSelected && /* @__PURE__ */ jsx32(
11822
+ isSelected && /* @__PURE__ */ jsx33(
11418
11823
  "rect",
11419
11824
  {
11420
11825
  x: scaledX - 2,
@@ -11427,7 +11832,7 @@ var ShapeRenderer = memo31(function ShapeRenderer2({
11427
11832
  strokeDasharray: "4 2"
11428
11833
  }
11429
11834
  ),
11430
- isSelected && isEditing && getResizeHandles().map((handle) => /* @__PURE__ */ jsx32(
11835
+ isSelected && isEditing && getResizeHandles().map((handle) => /* @__PURE__ */ jsx33(
11431
11836
  "rect",
11432
11837
  {
11433
11838
  x: handle.x,
@@ -11447,7 +11852,7 @@ var ShapeRenderer = memo31(function ShapeRenderer2({
11447
11852
  }
11448
11853
  );
11449
11854
  });
11450
- var ShapePreview = memo31(function ShapePreview2({
11855
+ var ShapePreview = memo32(function ShapePreview2({
11451
11856
  shapeType,
11452
11857
  startPoint,
11453
11858
  endPoint,
@@ -11468,9 +11873,9 @@ var ShapePreview = memo31(function ShapePreview2({
11468
11873
  };
11469
11874
  switch (shapeType) {
11470
11875
  case "rect":
11471
- return /* @__PURE__ */ jsx32("rect", { x, y, width, height, ...commonProps });
11876
+ return /* @__PURE__ */ jsx33("rect", { x, y, width, height, ...commonProps });
11472
11877
  case "circle":
11473
- return /* @__PURE__ */ jsx32(
11878
+ return /* @__PURE__ */ jsx33(
11474
11879
  "ellipse",
11475
11880
  {
11476
11881
  cx: x + width / 2,
@@ -11481,7 +11886,7 @@ var ShapePreview = memo31(function ShapePreview2({
11481
11886
  }
11482
11887
  );
11483
11888
  case "line":
11484
- return /* @__PURE__ */ jsx32(
11889
+ return /* @__PURE__ */ jsx33(
11485
11890
  "line",
11486
11891
  {
11487
11892
  x1: startPoint.x * scale,
@@ -11503,8 +11908,8 @@ var ShapePreview = memo31(function ShapePreview2({
11503
11908
  const arrow1Y = endY - arrowLength * Math.sin(angle - arrowAngle);
11504
11909
  const arrow2X = endX - arrowLength * Math.cos(angle + arrowAngle);
11505
11910
  const arrow2Y = endY - arrowLength * Math.sin(angle + arrowAngle);
11506
- return /* @__PURE__ */ jsxs27("g", { children: [
11507
- /* @__PURE__ */ jsx32(
11911
+ return /* @__PURE__ */ jsxs28("g", { children: [
11912
+ /* @__PURE__ */ jsx33(
11508
11913
  "line",
11509
11914
  {
11510
11915
  x1: startPoint.x * scale,
@@ -11514,8 +11919,8 @@ var ShapePreview = memo31(function ShapePreview2({
11514
11919
  ...commonProps
11515
11920
  }
11516
11921
  ),
11517
- /* @__PURE__ */ jsx32("line", { x1: endX, y1: endY, x2: arrow1X, y2: arrow1Y, ...commonProps }),
11518
- /* @__PURE__ */ jsx32("line", { x1: endX, y1: endY, x2: arrow2X, y2: arrow2Y, ...commonProps })
11922
+ /* @__PURE__ */ jsx33("line", { x1: endX, y1: endY, x2: arrow1X, y2: arrow1Y, ...commonProps }),
11923
+ /* @__PURE__ */ jsx33("line", { x1: endX, y1: endY, x2: arrow2X, y2: arrow2Y, ...commonProps })
11519
11924
  ] });
11520
11925
  default:
11521
11926
  return null;
@@ -11524,30 +11929,30 @@ var ShapePreview = memo31(function ShapePreview2({
11524
11929
 
11525
11930
  // src/components/Annotations/QuickNoteButton.tsx
11526
11931
  init_utils();
11527
- import { memo as memo32, useCallback as useCallback37, useState as useState25 } from "react";
11528
- import { jsx as jsx33 } from "react/jsx-runtime";
11529
- var QuickNoteButton = memo32(function QuickNoteButton2({
11932
+ import { memo as memo33, useCallback as useCallback38, useState as useState26 } from "react";
11933
+ import { jsx as jsx34 } from "react/jsx-runtime";
11934
+ var QuickNoteButton = memo33(function QuickNoteButton2({
11530
11935
  pageNumber,
11531
11936
  scale,
11532
- position = "top-right",
11937
+ position: position2 = "top-right",
11533
11938
  onClick,
11534
11939
  className,
11535
11940
  visible = true
11536
11941
  }) {
11537
- const [isHovered, setIsHovered] = useState25(false);
11538
- const handleClick = useCallback37(
11942
+ const [isHovered, setIsHovered] = useState26(false);
11943
+ const handleClick = useCallback38(
11539
11944
  (e) => {
11540
11945
  e.stopPropagation();
11541
- const x = position === "top-right" ? 80 : 80;
11542
- const y = position === "top-right" ? 20 : 80;
11946
+ const x = position2 === "top-right" ? 80 : 80;
11947
+ const y = position2 === "top-right" ? 20 : 80;
11543
11948
  onClick(pageNumber, x / scale, y / scale);
11544
11949
  },
11545
- [pageNumber, onClick, position, scale]
11950
+ [pageNumber, onClick, position2, scale]
11546
11951
  );
11547
11952
  if (!visible) {
11548
11953
  return null;
11549
11954
  }
11550
- return /* @__PURE__ */ jsx33(
11955
+ return /* @__PURE__ */ jsx34(
11551
11956
  "button",
11552
11957
  {
11553
11958
  onClick: handleClick,
@@ -11563,13 +11968,13 @@ var QuickNoteButton = memo32(function QuickNoteButton2({
11563
11968
  "transition-all duration-200",
11564
11969
  "focus:outline-none focus:ring-2 focus:ring-yellow-500 focus:ring-offset-2",
11565
11970
  isHovered && "scale-110",
11566
- position === "top-right" && "top-3 right-3",
11567
- position === "bottom-right" && "bottom-3 right-3",
11971
+ position2 === "top-right" && "top-3 right-3",
11972
+ position2 === "bottom-right" && "bottom-3 right-3",
11568
11973
  className
11569
11974
  ),
11570
11975
  title: "Add quick note",
11571
11976
  "aria-label": "Add quick note",
11572
- children: /* @__PURE__ */ jsx33(
11977
+ children: /* @__PURE__ */ jsx34(
11573
11978
  "svg",
11574
11979
  {
11575
11980
  className: "w-4 h-4 text-yellow-900",
@@ -11577,7 +11982,7 @@ var QuickNoteButton = memo32(function QuickNoteButton2({
11577
11982
  viewBox: "0 0 24 24",
11578
11983
  stroke: "currentColor",
11579
11984
  strokeWidth: 2,
11580
- children: /* @__PURE__ */ jsx33("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M12 4v16m8-8H4" })
11985
+ children: /* @__PURE__ */ jsx34("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M12 4v16m8-8H4" })
11581
11986
  }
11582
11987
  )
11583
11988
  }
@@ -11586,36 +11991,36 @@ var QuickNoteButton = memo32(function QuickNoteButton2({
11586
11991
 
11587
11992
  // src/components/Annotations/QuickNotePopover.tsx
11588
11993
  init_utils();
11589
- import { memo as memo33, useCallback as useCallback38, useState as useState26, useRef as useRef23, useEffect as useEffect24 } from "react";
11590
- import { jsx as jsx34, jsxs as jsxs28 } from "react/jsx-runtime";
11591
- var QuickNotePopover = memo33(function QuickNotePopover2({
11994
+ import { memo as memo34, useCallback as useCallback39, useState as useState27, useRef as useRef24, useEffect as useEffect25 } from "react";
11995
+ import { jsx as jsx35, jsxs as jsxs29 } from "react/jsx-runtime";
11996
+ var QuickNotePopover = memo34(function QuickNotePopover2({
11592
11997
  visible,
11593
- position,
11998
+ position: position2,
11594
11999
  initialContent = "",
11595
12000
  agentLastStatement,
11596
12001
  onSave,
11597
12002
  onCancel,
11598
12003
  className
11599
12004
  }) {
11600
- const [content, setContent] = useState26(initialContent);
11601
- const textareaRef = useRef23(null);
11602
- const popoverRef = useRef23(null);
11603
- const [adjustedPosition, setAdjustedPosition] = useState26(position);
11604
- useEffect24(() => {
12005
+ const [content, setContent] = useState27(initialContent);
12006
+ const textareaRef = useRef24(null);
12007
+ const popoverRef = useRef24(null);
12008
+ const [adjustedPosition, setAdjustedPosition] = useState27(position2);
12009
+ useEffect25(() => {
11605
12010
  if (visible && textareaRef.current) {
11606
12011
  textareaRef.current.focus();
11607
12012
  }
11608
12013
  }, [visible]);
11609
- useEffect24(() => {
12014
+ useEffect25(() => {
11610
12015
  if (visible) {
11611
12016
  setContent(initialContent);
11612
12017
  }
11613
12018
  }, [visible, initialContent]);
11614
- useEffect24(() => {
12019
+ useEffect25(() => {
11615
12020
  if (!visible || !popoverRef.current) return;
11616
12021
  const rect = popoverRef.current.getBoundingClientRect();
11617
12022
  const padding = 10;
11618
- let { x, y } = position;
12023
+ let { x, y } = position2;
11619
12024
  if (x + rect.width > window.innerWidth - padding) {
11620
12025
  x = window.innerWidth - rect.width - padding;
11621
12026
  }
@@ -11629,15 +12034,15 @@ var QuickNotePopover = memo33(function QuickNotePopover2({
11629
12034
  y = padding;
11630
12035
  }
11631
12036
  setAdjustedPosition({ x, y });
11632
- }, [position, visible]);
11633
- const handleSave = useCallback38(() => {
12037
+ }, [position2, visible]);
12038
+ const handleSave = useCallback39(() => {
11634
12039
  if (content.trim()) {
11635
12040
  onSave(content.trim());
11636
12041
  } else {
11637
12042
  onCancel();
11638
12043
  }
11639
12044
  }, [content, onSave, onCancel]);
11640
- const handleKeyDown = useCallback38(
12045
+ const handleKeyDown = useCallback39(
11641
12046
  (e) => {
11642
12047
  if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
11643
12048
  e.preventDefault();
@@ -11652,7 +12057,7 @@ var QuickNotePopover = memo33(function QuickNotePopover2({
11652
12057
  if (!visible) {
11653
12058
  return null;
11654
12059
  }
11655
- return /* @__PURE__ */ jsxs28(
12060
+ return /* @__PURE__ */ jsxs29(
11656
12061
  "div",
11657
12062
  {
11658
12063
  ref: popoverRef,
@@ -11671,15 +12076,15 @@ var QuickNotePopover = memo33(function QuickNotePopover2({
11671
12076
  top: adjustedPosition.y
11672
12077
  },
11673
12078
  children: [
11674
- agentLastStatement && /* @__PURE__ */ jsx34("div", { className: "mb-2 p-2 bg-blue-50 dark:bg-blue-900/50 rounded text-xs text-blue-600 dark:text-blue-300 border border-blue-100 dark:border-blue-800", children: /* @__PURE__ */ jsxs28("div", { className: "flex items-start gap-1", children: [
11675
- /* @__PURE__ */ jsx34("svg", { className: "w-3 h-3 mt-0.5 flex-shrink-0", fill: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx34("path", { d: "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" }) }),
11676
- /* @__PURE__ */ jsxs28("span", { className: "line-clamp-2", children: [
12079
+ agentLastStatement && /* @__PURE__ */ jsx35("div", { className: "mb-2 p-2 bg-blue-50 dark:bg-blue-900/50 rounded text-xs text-blue-600 dark:text-blue-300 border border-blue-100 dark:border-blue-800", children: /* @__PURE__ */ jsxs29("div", { className: "flex items-start gap-1", children: [
12080
+ /* @__PURE__ */ jsx35("svg", { className: "w-3 h-3 mt-0.5 flex-shrink-0", fill: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx35("path", { d: "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" }) }),
12081
+ /* @__PURE__ */ jsxs29("span", { className: "line-clamp-2", children: [
11677
12082
  "AI discussed: \u201C",
11678
12083
  agentLastStatement,
11679
12084
  "\u201D"
11680
12085
  ] })
11681
12086
  ] }) }),
11682
- /* @__PURE__ */ jsx34(
12087
+ /* @__PURE__ */ jsx35(
11683
12088
  "textarea",
11684
12089
  {
11685
12090
  ref: textareaRef,
@@ -11698,13 +12103,13 @@ var QuickNotePopover = memo33(function QuickNotePopover2({
11698
12103
  )
11699
12104
  }
11700
12105
  ),
11701
- /* @__PURE__ */ jsxs28("div", { className: "flex items-center justify-between mt-2", children: [
11702
- /* @__PURE__ */ jsxs28("span", { className: "text-xs text-gray-500 dark:text-gray-400", children: [
12106
+ /* @__PURE__ */ jsxs29("div", { className: "flex items-center justify-between mt-2", children: [
12107
+ /* @__PURE__ */ jsxs29("span", { className: "text-xs text-gray-500 dark:text-gray-400", children: [
11703
12108
  navigator.platform.includes("Mac") ? "\u2318" : "Ctrl",
11704
12109
  "+Enter to save"
11705
12110
  ] }),
11706
- /* @__PURE__ */ jsxs28("div", { className: "flex gap-2", children: [
11707
- /* @__PURE__ */ jsx34(
12111
+ /* @__PURE__ */ jsxs29("div", { className: "flex gap-2", children: [
12112
+ /* @__PURE__ */ jsx35(
11708
12113
  "button",
11709
12114
  {
11710
12115
  onClick: onCancel,
@@ -11717,7 +12122,7 @@ var QuickNotePopover = memo33(function QuickNotePopover2({
11717
12122
  children: "Cancel"
11718
12123
  }
11719
12124
  ),
11720
- /* @__PURE__ */ jsx34(
12125
+ /* @__PURE__ */ jsx35(
11721
12126
  "button",
11722
12127
  {
11723
12128
  onClick: handleSave,
@@ -11741,23 +12146,23 @@ var QuickNotePopover = memo33(function QuickNotePopover2({
11741
12146
 
11742
12147
  // src/components/AskAbout/AskAboutOverlay.tsx
11743
12148
  init_utils();
11744
- import { memo as memo34 } from "react";
11745
- import { jsx as jsx35, jsxs as jsxs29 } from "react/jsx-runtime";
11746
- var AskAboutOverlay = memo34(function AskAboutOverlay2({
12149
+ import { memo as memo35 } from "react";
12150
+ import { jsx as jsx36, jsxs as jsxs30 } from "react/jsx-runtime";
12151
+ var AskAboutOverlay = memo35(function AskAboutOverlay2({
11747
12152
  visible,
11748
12153
  progress,
11749
- position,
12154
+ position: position2,
11750
12155
  size = 60,
11751
12156
  className
11752
12157
  }) {
11753
- if (!visible || !position) {
12158
+ if (!visible || !position2) {
11754
12159
  return null;
11755
12160
  }
11756
12161
  const strokeWidth = 4;
11757
12162
  const radius = (size - strokeWidth) / 2;
11758
12163
  const circumference = 2 * Math.PI * radius;
11759
12164
  const strokeDashoffset = circumference * (1 - progress);
11760
- return /* @__PURE__ */ jsxs29(
12165
+ return /* @__PURE__ */ jsxs30(
11761
12166
  "div",
11762
12167
  {
11763
12168
  className: cn(
@@ -11766,11 +12171,11 @@ var AskAboutOverlay = memo34(function AskAboutOverlay2({
11766
12171
  className
11767
12172
  ),
11768
12173
  style: {
11769
- left: position.x - size / 2,
11770
- top: position.y - size / 2
12174
+ left: position2.x - size / 2,
12175
+ top: position2.y - size / 2
11771
12176
  },
11772
12177
  children: [
11773
- /* @__PURE__ */ jsxs29(
12178
+ /* @__PURE__ */ jsxs30(
11774
12179
  "svg",
11775
12180
  {
11776
12181
  width: size,
@@ -11778,7 +12183,7 @@ var AskAboutOverlay = memo34(function AskAboutOverlay2({
11778
12183
  viewBox: `0 0 ${size} ${size}`,
11779
12184
  className: "transform -rotate-90",
11780
12185
  children: [
11781
- /* @__PURE__ */ jsx35(
12186
+ /* @__PURE__ */ jsx36(
11782
12187
  "circle",
11783
12188
  {
11784
12189
  cx: size / 2,
@@ -11789,7 +12194,7 @@ var AskAboutOverlay = memo34(function AskAboutOverlay2({
11789
12194
  strokeWidth
11790
12195
  }
11791
12196
  ),
11792
- /* @__PURE__ */ jsx35(
12197
+ /* @__PURE__ */ jsx36(
11793
12198
  "circle",
11794
12199
  {
11795
12200
  cx: size / 2,
@@ -11807,12 +12212,12 @@ var AskAboutOverlay = memo34(function AskAboutOverlay2({
11807
12212
  ]
11808
12213
  }
11809
12214
  ),
11810
- /* @__PURE__ */ jsx35(
12215
+ /* @__PURE__ */ jsx36(
11811
12216
  "div",
11812
12217
  {
11813
12218
  className: "absolute inset-0 flex items-center justify-center",
11814
12219
  style: { color: progress >= 1 ? "#22c55e" : "white" },
11815
- children: progress >= 1 ? /* @__PURE__ */ jsx35(
12220
+ children: progress >= 1 ? /* @__PURE__ */ jsx36(
11816
12221
  "svg",
11817
12222
  {
11818
12223
  width: "24",
@@ -11823,9 +12228,9 @@ var AskAboutOverlay = memo34(function AskAboutOverlay2({
11823
12228
  strokeWidth: "2",
11824
12229
  strokeLinecap: "round",
11825
12230
  strokeLinejoin: "round",
11826
- children: /* @__PURE__ */ jsx35("polyline", { points: "20 6 9 17 4 12" })
12231
+ children: /* @__PURE__ */ jsx36("polyline", { points: "20 6 9 17 4 12" })
11827
12232
  }
11828
- ) : /* @__PURE__ */ jsxs29(
12233
+ ) : /* @__PURE__ */ jsxs30(
11829
12234
  "svg",
11830
12235
  {
11831
12236
  width: "20",
@@ -11837,9 +12242,9 @@ var AskAboutOverlay = memo34(function AskAboutOverlay2({
11837
12242
  strokeLinecap: "round",
11838
12243
  strokeLinejoin: "round",
11839
12244
  children: [
11840
- /* @__PURE__ */ jsx35("circle", { cx: "12", cy: "12", r: "10" }),
11841
- /* @__PURE__ */ jsx35("path", { d: "M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" }),
11842
- /* @__PURE__ */ jsx35("line", { x1: "12", y1: "17", x2: "12.01", y2: "17" })
12245
+ /* @__PURE__ */ jsx36("circle", { cx: "12", cy: "12", r: "10" }),
12246
+ /* @__PURE__ */ jsx36("path", { d: "M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" }),
12247
+ /* @__PURE__ */ jsx36("line", { x1: "12", y1: "17", x2: "12.01", y2: "17" })
11843
12248
  ]
11844
12249
  }
11845
12250
  )
@@ -11852,23 +12257,23 @@ var AskAboutOverlay = memo34(function AskAboutOverlay2({
11852
12257
 
11853
12258
  // src/components/AskAbout/AskAboutTrigger.tsx
11854
12259
  init_utils();
11855
- import { memo as memo35, useCallback as useCallback39, useState as useState27, useRef as useRef24, useEffect as useEffect25 } from "react";
11856
- import { jsx as jsx36, jsxs as jsxs30 } from "react/jsx-runtime";
11857
- var AskAboutTrigger = memo35(function AskAboutTrigger2({
11858
- position,
12260
+ import { memo as memo36, useCallback as useCallback40, useState as useState28, useRef as useRef25, useEffect as useEffect26 } from "react";
12261
+ import { jsx as jsx37, jsxs as jsxs31 } from "react/jsx-runtime";
12262
+ var AskAboutTrigger = memo36(function AskAboutTrigger2({
12263
+ position: position2,
11859
12264
  onConfirm,
11860
12265
  onCancel,
11861
12266
  visible,
11862
12267
  autoHideDelay = 5e3,
11863
12268
  className
11864
12269
  }) {
11865
- const [adjustedPosition, setAdjustedPosition] = useState27(position);
11866
- const triggerRef = useRef24(null);
11867
- useEffect25(() => {
12270
+ const [adjustedPosition, setAdjustedPosition] = useState28(position2);
12271
+ const triggerRef = useRef25(null);
12272
+ useEffect26(() => {
11868
12273
  if (!visible || !triggerRef.current) return;
11869
12274
  const rect = triggerRef.current.getBoundingClientRect();
11870
12275
  const padding = 10;
11871
- let { x, y } = position;
12276
+ let { x, y } = position2;
11872
12277
  if (x + rect.width / 2 > window.innerWidth - padding) {
11873
12278
  x = window.innerWidth - rect.width / 2 - padding;
11874
12279
  }
@@ -11876,23 +12281,23 @@ var AskAboutTrigger = memo35(function AskAboutTrigger2({
11876
12281
  x = rect.width / 2 + padding;
11877
12282
  }
11878
12283
  if (y + rect.height > window.innerHeight - padding) {
11879
- y = position.y - rect.height - 20;
12284
+ y = position2.y - rect.height - 20;
11880
12285
  }
11881
12286
  setAdjustedPosition({ x, y });
11882
- }, [position, visible]);
11883
- useEffect25(() => {
12287
+ }, [position2, visible]);
12288
+ useEffect26(() => {
11884
12289
  if (!visible || autoHideDelay === 0) return;
11885
12290
  const timer = setTimeout(onCancel, autoHideDelay);
11886
12291
  return () => clearTimeout(timer);
11887
12292
  }, [visible, autoHideDelay, onCancel]);
11888
- const handleConfirm = useCallback39(
12293
+ const handleConfirm = useCallback40(
11889
12294
  (e) => {
11890
12295
  e.stopPropagation();
11891
12296
  onConfirm();
11892
12297
  },
11893
12298
  [onConfirm]
11894
12299
  );
11895
- const handleCancel = useCallback39(
12300
+ const handleCancel = useCallback40(
11896
12301
  (e) => {
11897
12302
  e.stopPropagation();
11898
12303
  onCancel();
@@ -11902,7 +12307,7 @@ var AskAboutTrigger = memo35(function AskAboutTrigger2({
11902
12307
  if (!visible) {
11903
12308
  return null;
11904
12309
  }
11905
- return /* @__PURE__ */ jsxs30(
12310
+ return /* @__PURE__ */ jsxs31(
11906
12311
  "div",
11907
12312
  {
11908
12313
  ref: triggerRef,
@@ -11921,8 +12326,8 @@ var AskAboutTrigger = memo35(function AskAboutTrigger2({
11921
12326
  transform: "translate(-50%, 0)"
11922
12327
  },
11923
12328
  children: [
11924
- /* @__PURE__ */ jsx36("span", { className: "text-sm text-gray-600 dark:text-gray-300 px-2", children: "Ask about this?" }),
11925
- /* @__PURE__ */ jsx36(
12329
+ /* @__PURE__ */ jsx37("span", { className: "text-sm text-gray-600 dark:text-gray-300 px-2", children: "Ask about this?" }),
12330
+ /* @__PURE__ */ jsx37(
11926
12331
  "button",
11927
12332
  {
11928
12333
  onClick: handleConfirm,
@@ -11935,7 +12340,7 @@ var AskAboutTrigger = memo35(function AskAboutTrigger2({
11935
12340
  children: "Ask"
11936
12341
  }
11937
12342
  ),
11938
- /* @__PURE__ */ jsx36(
12343
+ /* @__PURE__ */ jsx37(
11939
12344
  "button",
11940
12345
  {
11941
12346
  onClick: handleCancel,
@@ -11957,9 +12362,9 @@ var AskAboutTrigger = memo35(function AskAboutTrigger2({
11957
12362
  // src/components/Minimap/Minimap.tsx
11958
12363
  init_hooks();
11959
12364
  init_utils();
11960
- import { memo as memo36, useMemo as useMemo14, useCallback as useCallback40 } from "react";
11961
- import { Fragment as Fragment3, jsx as jsx37, jsxs as jsxs31 } from "react/jsx-runtime";
11962
- var PageIndicator = memo36(function PageIndicator2({
12365
+ import { memo as memo37, useMemo as useMemo14, useCallback as useCallback41 } from "react";
12366
+ import { Fragment as Fragment3, jsx as jsx38, jsxs as jsxs32 } from "react/jsx-runtime";
12367
+ var PageIndicator = memo37(function PageIndicator2({
11963
12368
  pageNumber,
11964
12369
  status,
11965
12370
  isBookmarked,
@@ -11973,7 +12378,7 @@ var PageIndicator = memo36(function PageIndicator2({
11973
12378
  if (status === "visited") return "bg-green-400";
11974
12379
  return "bg-gray-200 dark:bg-gray-700";
11975
12380
  };
11976
- return /* @__PURE__ */ jsxs31(
12381
+ return /* @__PURE__ */ jsxs32(
11977
12382
  "button",
11978
12383
  {
11979
12384
  onClick,
@@ -11989,13 +12394,13 @@ var PageIndicator = memo36(function PageIndicator2({
11989
12394
  title: `Page ${pageNumber}${isBookmarked ? " (bookmarked)" : ""}`,
11990
12395
  "aria-label": `Go to page ${pageNumber}`,
11991
12396
  children: [
11992
- isBookmarked && !compact && /* @__PURE__ */ jsx37("div", { className: "absolute -top-1 -right-1 w-2 h-2 bg-yellow-500 rounded-full border border-white" }),
11993
- showNumber && !compact && /* @__PURE__ */ jsx37("span", { className: "absolute inset-0 flex items-center justify-center text-[8px] font-medium text-white", children: pageNumber })
12397
+ isBookmarked && !compact && /* @__PURE__ */ jsx38("div", { className: "absolute -top-1 -right-1 w-2 h-2 bg-yellow-500 rounded-full border border-white" }),
12398
+ showNumber && !compact && /* @__PURE__ */ jsx38("span", { className: "absolute inset-0 flex items-center justify-center text-[8px] font-medium text-white", children: pageNumber })
11994
12399
  ]
11995
12400
  }
11996
12401
  );
11997
12402
  });
11998
- var Minimap = memo36(function Minimap2({
12403
+ var Minimap = memo37(function Minimap2({
11999
12404
  variant = "sidebar",
12000
12405
  floatingPosition = "right",
12001
12406
  maxHeight = 300,
@@ -12012,14 +12417,14 @@ var Minimap = memo36(function Minimap2({
12012
12417
  return new Set(bookmarks.map((b) => b.pageNumber));
12013
12418
  }, [bookmarks]);
12014
12419
  const compact = numPages > 50;
12015
- const handlePageClick = useCallback40(
12420
+ const handlePageClick = useCallback41(
12016
12421
  (pageNumber) => {
12017
12422
  goToPage(pageNumber);
12018
12423
  onPageClick?.(pageNumber);
12019
12424
  },
12020
12425
  [goToPage, onPageClick]
12021
12426
  );
12022
- const getPageStatus = useCallback40(
12427
+ const getPageStatus = useCallback41(
12023
12428
  (pageNumber) => {
12024
12429
  if (pageNumber === currentPage) return "current";
12025
12430
  if (bookmarkedPages.has(pageNumber)) return "bookmarked";
@@ -12032,7 +12437,7 @@ var Minimap = memo36(function Minimap2({
12032
12437
  const pages = [];
12033
12438
  for (let i = 1; i <= numPages; i++) {
12034
12439
  pages.push(
12035
- /* @__PURE__ */ jsx37(
12440
+ /* @__PURE__ */ jsx38(
12036
12441
  PageIndicator,
12037
12442
  {
12038
12443
  pageNumber: i,
@@ -12053,16 +12458,16 @@ var Minimap = memo36(function Minimap2({
12053
12458
  if (numPages === 0) {
12054
12459
  return null;
12055
12460
  }
12056
- const content = /* @__PURE__ */ jsxs31(Fragment3, { children: [
12057
- /* @__PURE__ */ jsxs31("div", { className: "mb-3", children: [
12058
- /* @__PURE__ */ jsxs31("div", { className: "flex items-center justify-between text-xs text-gray-500 dark:text-gray-400 mb-1", children: [
12059
- /* @__PURE__ */ jsx37("span", { children: "Progress" }),
12060
- /* @__PURE__ */ jsxs31("span", { children: [
12461
+ const content = /* @__PURE__ */ jsxs32(Fragment3, { children: [
12462
+ /* @__PURE__ */ jsxs32("div", { className: "mb-3", children: [
12463
+ /* @__PURE__ */ jsxs32("div", { className: "flex items-center justify-between text-xs text-gray-500 dark:text-gray-400 mb-1", children: [
12464
+ /* @__PURE__ */ jsx38("span", { children: "Progress" }),
12465
+ /* @__PURE__ */ jsxs32("span", { children: [
12061
12466
  progressPercentage,
12062
12467
  "%"
12063
12468
  ] })
12064
12469
  ] }),
12065
- /* @__PURE__ */ jsx37("div", { className: "h-1.5 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden", children: /* @__PURE__ */ jsx37(
12470
+ /* @__PURE__ */ jsx38("div", { className: "h-1.5 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden", children: /* @__PURE__ */ jsx38(
12066
12471
  "div",
12067
12472
  {
12068
12473
  className: "h-full bg-green-500 rounded-full transition-all duration-300",
@@ -12070,7 +12475,7 @@ var Minimap = memo36(function Minimap2({
12070
12475
  }
12071
12476
  ) })
12072
12477
  ] }),
12073
- /* @__PURE__ */ jsx37(
12478
+ /* @__PURE__ */ jsx38(
12074
12479
  "div",
12075
12480
  {
12076
12481
  className: cn(
@@ -12081,21 +12486,21 @@ var Minimap = memo36(function Minimap2({
12081
12486
  children: pageIndicators
12082
12487
  }
12083
12488
  ),
12084
- /* @__PURE__ */ jsx37("div", { className: "mt-3 pt-2 border-t border-gray-200 dark:border-gray-700", children: /* @__PURE__ */ jsxs31("div", { className: "flex flex-wrap gap-3 text-xs text-gray-500 dark:text-gray-400", children: [
12085
- /* @__PURE__ */ jsxs31("div", { className: "flex items-center gap-1", children: [
12086
- /* @__PURE__ */ jsx37("div", { className: "w-2 h-2 rounded-sm bg-blue-500" }),
12087
- /* @__PURE__ */ jsx37("span", { children: "Current" })
12489
+ /* @__PURE__ */ jsx38("div", { className: "mt-3 pt-2 border-t border-gray-200 dark:border-gray-700", children: /* @__PURE__ */ jsxs32("div", { className: "flex flex-wrap gap-3 text-xs text-gray-500 dark:text-gray-400", children: [
12490
+ /* @__PURE__ */ jsxs32("div", { className: "flex items-center gap-1", children: [
12491
+ /* @__PURE__ */ jsx38("div", { className: "w-2 h-2 rounded-sm bg-blue-500" }),
12492
+ /* @__PURE__ */ jsx38("span", { children: "Current" })
12088
12493
  ] }),
12089
- /* @__PURE__ */ jsxs31("div", { className: "flex items-center gap-1", children: [
12090
- /* @__PURE__ */ jsx37("div", { className: "w-2 h-2 rounded-sm bg-green-400" }),
12091
- /* @__PURE__ */ jsx37("span", { children: "Visited" })
12494
+ /* @__PURE__ */ jsxs32("div", { className: "flex items-center gap-1", children: [
12495
+ /* @__PURE__ */ jsx38("div", { className: "w-2 h-2 rounded-sm bg-green-400" }),
12496
+ /* @__PURE__ */ jsx38("span", { children: "Visited" })
12092
12497
  ] }),
12093
- /* @__PURE__ */ jsxs31("div", { className: "flex items-center gap-1", children: [
12094
- /* @__PURE__ */ jsx37("div", { className: "w-2 h-2 rounded-sm bg-yellow-400" }),
12095
- /* @__PURE__ */ jsx37("span", { children: "Bookmarked" })
12498
+ /* @__PURE__ */ jsxs32("div", { className: "flex items-center gap-1", children: [
12499
+ /* @__PURE__ */ jsx38("div", { className: "w-2 h-2 rounded-sm bg-yellow-400" }),
12500
+ /* @__PURE__ */ jsx38("span", { children: "Bookmarked" })
12096
12501
  ] })
12097
12502
  ] }) }),
12098
- /* @__PURE__ */ jsxs31("div", { className: "mt-2 text-xs text-gray-500 dark:text-gray-400", children: [
12503
+ /* @__PURE__ */ jsxs32("div", { className: "mt-2 text-xs text-gray-500 dark:text-gray-400", children: [
12099
12504
  visitedCount,
12100
12505
  " of ",
12101
12506
  numPages,
@@ -12103,7 +12508,7 @@ var Minimap = memo36(function Minimap2({
12103
12508
  ] })
12104
12509
  ] });
12105
12510
  if (variant === "floating") {
12106
- return /* @__PURE__ */ jsxs31(
12511
+ return /* @__PURE__ */ jsxs32(
12107
12512
  "div",
12108
12513
  {
12109
12514
  className: cn(
@@ -12119,13 +12524,13 @@ var Minimap = memo36(function Minimap2({
12119
12524
  ),
12120
12525
  style: { maxHeight },
12121
12526
  children: [
12122
- /* @__PURE__ */ jsx37("h3", { className: "text-sm font-semibold text-gray-700 dark:text-gray-200 mb-2", children: "Reading Progress" }),
12527
+ /* @__PURE__ */ jsx38("h3", { className: "text-sm font-semibold text-gray-700 dark:text-gray-200 mb-2", children: "Reading Progress" }),
12123
12528
  content
12124
12529
  ]
12125
12530
  }
12126
12531
  );
12127
12532
  }
12128
- return /* @__PURE__ */ jsx37(
12533
+ return /* @__PURE__ */ jsx38(
12129
12534
  "div",
12130
12535
  {
12131
12536
  className: cn(
@@ -12145,11 +12550,11 @@ init_FloatingZoomControls2();
12145
12550
  // src/components/PDFThumbnailNav/PDFThumbnailNav.tsx
12146
12551
  init_hooks();
12147
12552
  init_utils();
12148
- import { memo as memo37, useEffect as useEffect26, useState as useState28, useRef as useRef25, useCallback as useCallback41 } from "react";
12149
- import { jsx as jsx38, jsxs as jsxs32 } from "react/jsx-runtime";
12553
+ import { memo as memo38, useEffect as useEffect27, useState as useState29, useRef as useRef26, useCallback as useCallback42 } from "react";
12554
+ import { jsx as jsx39, jsxs as jsxs33 } from "react/jsx-runtime";
12150
12555
  var DEFAULT_WIDTH = 612;
12151
12556
  var DEFAULT_HEIGHT = 792;
12152
- var PDFThumbnailNav = memo37(function PDFThumbnailNav2({
12557
+ var PDFThumbnailNav = memo38(function PDFThumbnailNav2({
12153
12558
  thumbnailScale = 0.15,
12154
12559
  orientation = "vertical",
12155
12560
  maxVisible = 10,
@@ -12160,14 +12565,14 @@ var PDFThumbnailNav = memo37(function PDFThumbnailNav2({
12160
12565
  }) {
12161
12566
  const { document: document2, numPages, currentPage } = usePDFViewer();
12162
12567
  const { viewerStore } = usePDFViewerStores();
12163
- const containerRef = useRef25(null);
12164
- const [thumbnails, setThumbnails] = useState28(/* @__PURE__ */ new Map());
12165
- const [visibleRange, setVisibleRange] = useState28({ start: 1, end: maxVisible });
12166
- const renderQueueRef = useRef25(/* @__PURE__ */ new Set());
12167
- const pageCache = useRef25(/* @__PURE__ */ new Map());
12568
+ const containerRef = useRef26(null);
12569
+ const [thumbnails, setThumbnails] = useState29(/* @__PURE__ */ new Map());
12570
+ const [visibleRange, setVisibleRange] = useState29({ start: 1, end: maxVisible });
12571
+ const renderQueueRef = useRef26(/* @__PURE__ */ new Set());
12572
+ const pageCache = useRef26(/* @__PURE__ */ new Map());
12168
12573
  const thumbnailWidth = Math.floor(DEFAULT_WIDTH * thumbnailScale);
12169
12574
  const thumbnailHeight = Math.floor(DEFAULT_HEIGHT * thumbnailScale);
12170
- const updateVisibleRange = useCallback41(() => {
12575
+ const updateVisibleRange = useCallback42(() => {
12171
12576
  if (!containerRef.current || numPages === 0) return;
12172
12577
  const container = containerRef.current;
12173
12578
  const isHorizontal2 = orientation === "horizontal";
@@ -12179,7 +12584,7 @@ var PDFThumbnailNav = memo37(function PDFThumbnailNav2({
12179
12584
  const lastVisible = Math.min(numPages, firstVisible + visibleCount);
12180
12585
  setVisibleRange({ start: firstVisible, end: lastVisible });
12181
12586
  }, [numPages, orientation, thumbnailWidth, thumbnailHeight, gap]);
12182
- useEffect26(() => {
12587
+ useEffect27(() => {
12183
12588
  const container = containerRef.current;
12184
12589
  if (!container) return;
12185
12590
  const handleScroll = () => {
@@ -12189,7 +12594,7 @@ var PDFThumbnailNav = memo37(function PDFThumbnailNav2({
12189
12594
  updateVisibleRange();
12190
12595
  return () => container.removeEventListener("scroll", handleScroll);
12191
12596
  }, [updateVisibleRange]);
12192
- useEffect26(() => {
12597
+ useEffect27(() => {
12193
12598
  if (!document2) {
12194
12599
  setThumbnails(/* @__PURE__ */ new Map());
12195
12600
  pageCache.current.clear();
@@ -12244,7 +12649,7 @@ var PDFThumbnailNav = memo37(function PDFThumbnailNav2({
12244
12649
  };
12245
12650
  renderThumbnails();
12246
12651
  }, [document2, visibleRange, thumbnailScale, thumbnails]);
12247
- useEffect26(() => {
12652
+ useEffect27(() => {
12248
12653
  if (!containerRef.current || numPages === 0) return;
12249
12654
  const container = containerRef.current;
12250
12655
  const isHorizontal2 = orientation === "horizontal";
@@ -12260,12 +12665,12 @@ var PDFThumbnailNav = memo37(function PDFThumbnailNav2({
12260
12665
  });
12261
12666
  }
12262
12667
  }, [currentPage, numPages, orientation, thumbnailWidth, thumbnailHeight, gap]);
12263
- const handleThumbnailClick = useCallback41((pageNum) => {
12668
+ const handleThumbnailClick = useCallback42((pageNum) => {
12264
12669
  onThumbnailClick?.(pageNum);
12265
12670
  viewerStore.getState().requestScrollToPage(pageNum, "smooth");
12266
12671
  }, [onThumbnailClick, viewerStore]);
12267
12672
  if (!document2 || numPages === 0) {
12268
- return /* @__PURE__ */ jsx38(
12673
+ return /* @__PURE__ */ jsx39(
12269
12674
  "div",
12270
12675
  {
12271
12676
  className: cn(
@@ -12286,7 +12691,7 @@ var PDFThumbnailNav = memo37(function PDFThumbnailNav2({
12286
12691
  }
12287
12692
  const isHorizontal = orientation === "horizontal";
12288
12693
  const totalSize = numPages * ((isHorizontal ? thumbnailWidth : thumbnailHeight) + gap) - gap;
12289
- return /* @__PURE__ */ jsx38(
12694
+ return /* @__PURE__ */ jsx39(
12290
12695
  "div",
12291
12696
  {
12292
12697
  ref: containerRef,
@@ -12300,7 +12705,7 @@ var PDFThumbnailNav = memo37(function PDFThumbnailNav2({
12300
12705
  style: {
12301
12706
  ...isHorizontal ? { overflowX: "auto", overflowY: "hidden" } : { overflowX: "hidden", overflowY: "auto" }
12302
12707
  },
12303
- children: /* @__PURE__ */ jsx38(
12708
+ children: /* @__PURE__ */ jsx39(
12304
12709
  "div",
12305
12710
  {
12306
12711
  className: cn(
@@ -12317,7 +12722,7 @@ var PDFThumbnailNav = memo37(function PDFThumbnailNav2({
12317
12722
  const thumbnail = thumbnails.get(pageNum);
12318
12723
  const isActive = pageNum === currentPage;
12319
12724
  const isVisible = pageNum >= visibleRange.start && pageNum <= visibleRange.end;
12320
- return /* @__PURE__ */ jsxs32(
12725
+ return /* @__PURE__ */ jsxs33(
12321
12726
  "div",
12322
12727
  {
12323
12728
  className: cn(
@@ -12342,7 +12747,7 @@ var PDFThumbnailNav = memo37(function PDFThumbnailNav2({
12342
12747
  }
12343
12748
  },
12344
12749
  children: [
12345
- /* @__PURE__ */ jsx38(
12750
+ /* @__PURE__ */ jsx39(
12346
12751
  "div",
12347
12752
  {
12348
12753
  className: "relative bg-white dark:bg-gray-700",
@@ -12350,7 +12755,7 @@ var PDFThumbnailNav = memo37(function PDFThumbnailNav2({
12350
12755
  width: thumbnailWidth,
12351
12756
  height: thumbnailHeight
12352
12757
  },
12353
- children: isVisible && thumbnail?.canvas ? /* @__PURE__ */ jsx38(
12758
+ children: isVisible && thumbnail?.canvas ? /* @__PURE__ */ jsx39(
12354
12759
  "img",
12355
12760
  {
12356
12761
  src: thumbnail.canvas.toDataURL(),
@@ -12358,10 +12763,10 @@ var PDFThumbnailNav = memo37(function PDFThumbnailNav2({
12358
12763
  className: "w-full h-full object-contain",
12359
12764
  loading: "lazy"
12360
12765
  }
12361
- ) : /* @__PURE__ */ jsx38("div", { className: "absolute inset-0 flex items-center justify-center text-gray-400 dark:text-gray-500 text-xs", children: pageNum })
12766
+ ) : /* @__PURE__ */ jsx39("div", { className: "absolute inset-0 flex items-center justify-center text-gray-400 dark:text-gray-500 text-xs", children: pageNum })
12362
12767
  }
12363
12768
  ),
12364
- showPageNumbers && /* @__PURE__ */ jsx38(
12769
+ showPageNumbers && /* @__PURE__ */ jsx39(
12365
12770
  "div",
12366
12771
  {
12367
12772
  className: cn(
@@ -12386,7 +12791,7 @@ var PDFThumbnailNav = memo37(function PDFThumbnailNav2({
12386
12791
  // src/components/ErrorBoundary/PDFErrorBoundary.tsx
12387
12792
  init_utils();
12388
12793
  import { Component } from "react";
12389
- import { jsx as jsx39, jsxs as jsxs33 } from "react/jsx-runtime";
12794
+ import { jsx as jsx40, jsxs as jsxs34 } from "react/jsx-runtime";
12390
12795
  var PDFErrorBoundary = class extends Component {
12391
12796
  constructor(props) {
12392
12797
  super(props);
@@ -12414,7 +12819,7 @@ var PDFErrorBoundary = class extends Component {
12414
12819
  return fallback;
12415
12820
  }
12416
12821
  if (showDefaultUI) {
12417
- return /* @__PURE__ */ jsx39(
12822
+ return /* @__PURE__ */ jsx40(
12418
12823
  DefaultErrorUI,
12419
12824
  {
12420
12825
  error,
@@ -12433,7 +12838,7 @@ function DefaultErrorUI({ error, onReset, className }) {
12433
12838
  const isNetworkError = error.message.includes("fetch") || error.message.includes("network") || error.message.includes("Failed to load");
12434
12839
  let title = "Something went wrong";
12435
12840
  let description = error.message;
12436
- let icon = /* @__PURE__ */ jsx39("svg", { className: "w-12 h-12 text-red-500", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ jsx39(
12841
+ let icon = /* @__PURE__ */ jsx40("svg", { className: "w-12 h-12 text-red-500", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ jsx40(
12437
12842
  "path",
12438
12843
  {
12439
12844
  strokeLinecap: "round",
@@ -12445,7 +12850,7 @@ function DefaultErrorUI({ error, onReset, className }) {
12445
12850
  if (isPDFError) {
12446
12851
  title = "Unable to load PDF";
12447
12852
  description = "The PDF file could not be loaded. It may be corrupted or in an unsupported format.";
12448
- icon = /* @__PURE__ */ jsx39("svg", { className: "w-12 h-12 text-red-500", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ jsx39(
12853
+ icon = /* @__PURE__ */ jsx40("svg", { className: "w-12 h-12 text-red-500", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ jsx40(
12449
12854
  "path",
12450
12855
  {
12451
12856
  strokeLinecap: "round",
@@ -12457,7 +12862,7 @@ function DefaultErrorUI({ error, onReset, className }) {
12457
12862
  } else if (isNetworkError) {
12458
12863
  title = "Network error";
12459
12864
  description = "Unable to fetch the PDF file. Please check your internet connection and try again.";
12460
- icon = /* @__PURE__ */ jsx39("svg", { className: "w-12 h-12 text-red-500", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ jsx39(
12865
+ icon = /* @__PURE__ */ jsx40("svg", { className: "w-12 h-12 text-red-500", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ jsx40(
12461
12866
  "path",
12462
12867
  {
12463
12868
  strokeLinecap: "round",
@@ -12467,7 +12872,7 @@ function DefaultErrorUI({ error, onReset, className }) {
12467
12872
  }
12468
12873
  ) });
12469
12874
  }
12470
- return /* @__PURE__ */ jsxs33(
12875
+ return /* @__PURE__ */ jsxs34(
12471
12876
  "div",
12472
12877
  {
12473
12878
  className: cn(
@@ -12480,14 +12885,14 @@ function DefaultErrorUI({ error, onReset, className }) {
12480
12885
  ),
12481
12886
  children: [
12482
12887
  icon,
12483
- /* @__PURE__ */ jsx39("h2", { className: "mt-4 text-xl font-semibold text-gray-900 dark:text-gray-100", children: title }),
12484
- /* @__PURE__ */ jsx39("p", { className: "mt-2 text-sm text-gray-600 dark:text-gray-400 max-w-md", children: description }),
12485
- /* @__PURE__ */ jsxs33("details", { className: "mt-4 text-left max-w-md w-full", children: [
12486
- /* @__PURE__ */ jsx39("summary", { className: "cursor-pointer text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200", children: "Technical details" }),
12487
- /* @__PURE__ */ jsx39("pre", { className: "mt-2 p-2 bg-gray-100 dark:bg-gray-800 rounded text-xs overflow-auto", children: error.stack || error.message })
12888
+ /* @__PURE__ */ jsx40("h2", { className: "mt-4 text-xl font-semibold text-gray-900 dark:text-gray-100", children: title }),
12889
+ /* @__PURE__ */ jsx40("p", { className: "mt-2 text-sm text-gray-600 dark:text-gray-400 max-w-md", children: description }),
12890
+ /* @__PURE__ */ jsxs34("details", { className: "mt-4 text-left max-w-md w-full", children: [
12891
+ /* @__PURE__ */ jsx40("summary", { className: "cursor-pointer text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200", children: "Technical details" }),
12892
+ /* @__PURE__ */ jsx40("pre", { className: "mt-2 p-2 bg-gray-100 dark:bg-gray-800 rounded text-xs overflow-auto", children: error.stack || error.message })
12488
12893
  ] }),
12489
- /* @__PURE__ */ jsxs33("div", { className: "mt-6 flex gap-3", children: [
12490
- /* @__PURE__ */ jsx39(
12894
+ /* @__PURE__ */ jsxs34("div", { className: "mt-6 flex gap-3", children: [
12895
+ /* @__PURE__ */ jsx40(
12491
12896
  "button",
12492
12897
  {
12493
12898
  onClick: onReset,
@@ -12501,7 +12906,7 @@ function DefaultErrorUI({ error, onReset, className }) {
12501
12906
  children: "Try again"
12502
12907
  }
12503
12908
  ),
12504
- /* @__PURE__ */ jsx39(
12909
+ /* @__PURE__ */ jsx40(
12505
12910
  "button",
12506
12911
  {
12507
12912
  onClick: () => window.location.reload(),
@@ -12521,12 +12926,2173 @@ function DefaultErrorUI({ error, onReset, className }) {
12521
12926
  );
12522
12927
  }
12523
12928
  function withErrorBoundary({ component, ...props }) {
12524
- return /* @__PURE__ */ jsx39(PDFErrorBoundary, { ...props, children: component });
12929
+ return /* @__PURE__ */ jsx40(PDFErrorBoundary, { ...props, children: component });
12525
12930
  }
12526
12931
 
12527
12932
  // src/components/index.ts
12528
12933
  init_PDFLoadingScreen2();
12529
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
+
12530
15096
  // src/index.ts
12531
15097
  init_hooks();
12532
15098
  init_store();
@@ -12537,18 +15103,26 @@ init_PluginManager();
12537
15103
  // src/index.ts
12538
15104
  init_utils();
12539
15105
  export {
15106
+ AnimatedHighlight,
15107
+ AnimatedUnderline,
12540
15108
  AnnotationLayer,
12541
15109
  AnnotationToolbar,
12542
15110
  AskAboutOverlay,
12543
15111
  AskAboutTrigger,
15112
+ BookModeContainer,
12544
15113
  BookmarksPanel,
15114
+ BoxOverlay,
15115
+ CalloutArrow,
15116
+ CameraView,
12545
15117
  CanvasLayer,
15118
+ CinemaLayer,
12546
15119
  ContinuousScrollContainer,
12547
15120
  DocumentContainer,
12548
15121
  DrawingCanvas,
12549
15122
  DualPageContainer,
12550
15123
  FloatingZoomControls,
12551
15124
  FocusRegionLayer,
15125
+ GhostReference,
12552
15126
  HighlightLayer,
12553
15127
  HighlightPopover,
12554
15128
  HighlightsPanel,
@@ -12565,32 +15139,46 @@ export {
12565
15139
  PDFViewerContext,
12566
15140
  PDFViewerProvider,
12567
15141
  PluginManager,
15142
+ PulseOverlay,
12568
15143
  QuickNoteButton,
12569
15144
  QuickNotePopover,
15145
+ SYSTEM_PROMPT,
12570
15146
  SearchPanel,
12571
15147
  SelectionToolbar,
12572
15148
  ShapePreview,
12573
15149
  ShapeRenderer,
12574
15150
  Sidebar,
15151
+ SpotlightMask,
15152
+ StickyLabel,
12575
15153
  StickyNote,
15154
+ StoryboardActionSchema,
15155
+ StoryboardEngine,
15156
+ StoryboardSchema,
15157
+ SubtitleBar,
12576
15158
  TakeawaysPanel,
12577
15159
  TextLayer,
12578
15160
  ThumbnailPanel,
12579
15161
  Toolbar,
15162
+ TutorModeContainer,
12580
15163
  VirtualizedDocumentContainer,
12581
15164
  applyRotation,
15165
+ buildBBoxIndex,
15166
+ buildUserPrompt,
12582
15167
  clearHighlights,
12583
15168
  clearStudentData,
12584
15169
  cn,
15170
+ cosineSimilarity,
12585
15171
  countTextOnPage,
12586
15172
  createAgentAPI,
12587
15173
  createAgentStore,
12588
15174
  createAnnotationStore,
15175
+ createNarrationStore,
12589
15176
  createPDFViewer,
12590
15177
  createPluginManager,
12591
15178
  createSearchStore,
12592
15179
  createStudentStore,
12593
15180
  createViewerStore,
15181
+ directStoryboard,
12594
15182
  doRectsIntersect,
12595
15183
  downloadAnnotationsAsJSON,
12596
15184
  downloadAnnotationsAsMarkdown,
@@ -12605,6 +15193,7 @@ export {
12605
15193
  generateDocumentId,
12606
15194
  getAllDocumentIds,
12607
15195
  getAllStudentDataDocumentIds,
15196
+ getLocalMiniLM,
12608
15197
  getMetadata,
12609
15198
  getOutline,
12610
15199
  getPage,
@@ -12622,17 +15211,23 @@ export {
12622
15211
  loadDocumentWithCallbacks,
12623
15212
  loadHighlights,
12624
15213
  loadStudentData,
15214
+ makeOverlayId,
15215
+ matchChunkToBlock,
12625
15216
  mergeAdjacentRects,
12626
15217
  pdfToPercent,
12627
15218
  pdfToViewport,
12628
15219
  pdfjsLib,
12629
15220
  percentToPDF,
12630
15221
  percentToViewport,
15222
+ playPageTurnSound,
12631
15223
  quickViewer,
12632
15224
  removeRotation,
12633
15225
  saveHighlights,
12634
15226
  saveStudentData,
12635
15227
  scaleRect,
15228
+ storyboardFromMatch,
15229
+ storyboardJsonSchema,
15230
+ truncate,
12636
15231
  useAgentContext,
12637
15232
  useAgentStore,
12638
15233
  useAnnotationStore,