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