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.cjs
CHANGED
|
@@ -140,8 +140,8 @@ async function loadDocument(options) {
|
|
|
140
140
|
signal.addEventListener("abort", abortHandler);
|
|
141
141
|
}
|
|
142
142
|
if (onProgress) {
|
|
143
|
-
loadingTask.onProgress = ({ loaded, total }) => {
|
|
144
|
-
onProgress({ loaded, total });
|
|
143
|
+
loadingTask.onProgress = ({ loaded: loaded2, total }) => {
|
|
144
|
+
onProgress({ loaded: loaded2, total });
|
|
145
145
|
};
|
|
146
146
|
}
|
|
147
147
|
let document2;
|
|
@@ -271,9 +271,9 @@ function loadDocumentWithCallbacks(options) {
|
|
|
271
271
|
};
|
|
272
272
|
abortController.signal.addEventListener("abort", abortHandler);
|
|
273
273
|
if (onProgress) {
|
|
274
|
-
loadingTask.onProgress = ({ loaded, total }) => {
|
|
274
|
+
loadingTask.onProgress = ({ loaded: loaded2, total }) => {
|
|
275
275
|
if (!abortController.signal.aborted) {
|
|
276
|
-
onProgress({ loaded, total });
|
|
276
|
+
onProgress({ loaded: loaded2, total });
|
|
277
277
|
}
|
|
278
278
|
};
|
|
279
279
|
}
|
|
@@ -627,13 +627,13 @@ function createViewerStore(initialOverrides = {}) {
|
|
|
627
627
|
},
|
|
628
628
|
zoomIn: () => {
|
|
629
629
|
const { scale } = get();
|
|
630
|
-
const currentIndex = ZOOM_LEVELS.findIndex((
|
|
630
|
+
const currentIndex = ZOOM_LEVELS.findIndex((z2) => z2 >= scale);
|
|
631
631
|
const nextIndex = Math.min(currentIndex + 1, ZOOM_LEVELS.length - 1);
|
|
632
632
|
set({ scale: ZOOM_LEVELS[nextIndex] ?? MAX_SCALE });
|
|
633
633
|
},
|
|
634
634
|
zoomOut: () => {
|
|
635
635
|
const { scale } = get();
|
|
636
|
-
const currentIndex = ZOOM_LEVELS.findIndex((
|
|
636
|
+
const currentIndex = ZOOM_LEVELS.findIndex((z2) => z2 >= scale);
|
|
637
637
|
const prevIndex = Math.max(currentIndex - 1, 0);
|
|
638
638
|
set({ scale: ZOOM_LEVELS[prevIndex] ?? MIN_SCALE });
|
|
639
639
|
},
|
|
@@ -1863,6 +1863,34 @@ var init_page_turn_sound = __esm({
|
|
|
1863
1863
|
}
|
|
1864
1864
|
});
|
|
1865
1865
|
|
|
1866
|
+
// src/utils/camera-math.ts
|
|
1867
|
+
function fitPageScale(page, viewport) {
|
|
1868
|
+
const sx = viewport.width / page.width;
|
|
1869
|
+
const sy = viewport.height / page.height;
|
|
1870
|
+
return Math.min(sx, sy);
|
|
1871
|
+
}
|
|
1872
|
+
function computeCameraForBlock(bbox, page, viewport, opts = {}) {
|
|
1873
|
+
const targetScale = opts.targetScale ?? 1.5;
|
|
1874
|
+
const paddingPdf = opts.paddingPdf ?? 80;
|
|
1875
|
+
const [x1, y1, x2, y2] = bbox;
|
|
1876
|
+
const blockW = Math.max(1, x2 - x1 + paddingPdf * 2);
|
|
1877
|
+
const blockH = Math.max(1, y2 - y1 + paddingPdf * 2);
|
|
1878
|
+
const blockCX = (x1 + x2) / 2;
|
|
1879
|
+
const blockCY = (y1 + y2) / 2;
|
|
1880
|
+
const fitBlock = Math.min(viewport.width / blockW, viewport.height / blockH);
|
|
1881
|
+
const scale = fitBlock * targetScale;
|
|
1882
|
+
const pageCX = page.width / 2;
|
|
1883
|
+
const pageCY = page.height / 2;
|
|
1884
|
+
const x = (pageCX - blockCX) * scale;
|
|
1885
|
+
const y = (pageCY - blockCY) * scale;
|
|
1886
|
+
return { scale, x, y };
|
|
1887
|
+
}
|
|
1888
|
+
var init_camera_math = __esm({
|
|
1889
|
+
"src/utils/camera-math.ts"() {
|
|
1890
|
+
"use strict";
|
|
1891
|
+
}
|
|
1892
|
+
});
|
|
1893
|
+
|
|
1866
1894
|
// src/utils/index.ts
|
|
1867
1895
|
var init_utils = __esm({
|
|
1868
1896
|
"src/utils/index.ts"() {
|
|
@@ -2281,6 +2309,78 @@ var init_student_store = __esm({
|
|
|
2281
2309
|
}
|
|
2282
2310
|
});
|
|
2283
2311
|
|
|
2312
|
+
// src/store/narration-store.ts
|
|
2313
|
+
function createNarrationStore(overrides = {}) {
|
|
2314
|
+
return (0, import_vanilla6.createStore)()((set) => ({
|
|
2315
|
+
...initialState6,
|
|
2316
|
+
...overrides,
|
|
2317
|
+
setCurrentChunk: (chunk) => set({ currentChunk: chunk }),
|
|
2318
|
+
setCurrentPage: (page) => set({ currentPage: page }),
|
|
2319
|
+
pushChunkHistory: (entry) => set((state) => ({
|
|
2320
|
+
chunkHistory: [
|
|
2321
|
+
...state.chunkHistory.slice(-(MAX_HISTORY - 1)),
|
|
2322
|
+
entry
|
|
2323
|
+
]
|
|
2324
|
+
})),
|
|
2325
|
+
setCamera: (camera) => set((state) => ({ camera: { ...state.camera, ...camera } })),
|
|
2326
|
+
addOverlay: (overlay) => set((state) => ({ activeOverlays: [...state.activeOverlays, overlay] })),
|
|
2327
|
+
removeOverlay: (id) => set((state) => ({
|
|
2328
|
+
activeOverlays: state.activeOverlays.filter((o) => o.id !== id)
|
|
2329
|
+
})),
|
|
2330
|
+
clearOverlays: (predicate) => set((state) => ({
|
|
2331
|
+
activeOverlays: predicate ? state.activeOverlays.filter((o) => !predicate(o)) : []
|
|
2332
|
+
})),
|
|
2333
|
+
setEngineStatus: (s) => set({ engineStatus: s }),
|
|
2334
|
+
setLlmStatus: (s, error = null) => set({ llmStatus: s, lastError: error }),
|
|
2335
|
+
setLastStoryboard: (sb) => set({ lastStoryboard: sb }),
|
|
2336
|
+
setPaused: (paused) => set({ isPaused: paused }),
|
|
2337
|
+
appendDebugEvent: (event) => set((state) => {
|
|
2338
|
+
debugEventCounter += 1;
|
|
2339
|
+
const next = {
|
|
2340
|
+
...event,
|
|
2341
|
+
id: `dbg-${debugEventCounter}`,
|
|
2342
|
+
timestamp: Date.now()
|
|
2343
|
+
};
|
|
2344
|
+
return {
|
|
2345
|
+
debugEvents: [
|
|
2346
|
+
...state.debugEvents.slice(-(MAX_DEBUG_EVENTS - 1)),
|
|
2347
|
+
next
|
|
2348
|
+
]
|
|
2349
|
+
};
|
|
2350
|
+
}),
|
|
2351
|
+
clearDebugEvents: () => set({ debugEvents: [] }),
|
|
2352
|
+
reset: () => set(initialState6)
|
|
2353
|
+
}));
|
|
2354
|
+
}
|
|
2355
|
+
function makeOverlayId(action) {
|
|
2356
|
+
overlayIdCounter += 1;
|
|
2357
|
+
return `ov-${action.type}-${overlayIdCounter}-${Date.now()}`;
|
|
2358
|
+
}
|
|
2359
|
+
var import_vanilla6, MAX_HISTORY, initialState6, MAX_DEBUG_EVENTS, debugEventCounter, overlayIdCounter;
|
|
2360
|
+
var init_narration_store = __esm({
|
|
2361
|
+
"src/store/narration-store.ts"() {
|
|
2362
|
+
"use strict";
|
|
2363
|
+
import_vanilla6 = require("zustand/vanilla");
|
|
2364
|
+
MAX_HISTORY = 5;
|
|
2365
|
+
initialState6 = {
|
|
2366
|
+
currentChunk: null,
|
|
2367
|
+
currentPage: 1,
|
|
2368
|
+
chunkHistory: [],
|
|
2369
|
+
camera: { scale: 1, x: 0, y: 0, easing: "ease-in-out" },
|
|
2370
|
+
activeOverlays: [],
|
|
2371
|
+
engineStatus: "idle",
|
|
2372
|
+
llmStatus: "idle",
|
|
2373
|
+
lastStoryboard: null,
|
|
2374
|
+
lastError: null,
|
|
2375
|
+
isPaused: false,
|
|
2376
|
+
debugEvents: []
|
|
2377
|
+
};
|
|
2378
|
+
MAX_DEBUG_EVENTS = 50;
|
|
2379
|
+
debugEventCounter = 0;
|
|
2380
|
+
overlayIdCounter = 0;
|
|
2381
|
+
}
|
|
2382
|
+
});
|
|
2383
|
+
|
|
2284
2384
|
// src/store/index.ts
|
|
2285
2385
|
var init_store = __esm({
|
|
2286
2386
|
"src/store/index.ts"() {
|
|
@@ -2290,13 +2390,14 @@ var init_store = __esm({
|
|
|
2290
2390
|
init_search_store();
|
|
2291
2391
|
init_agent_store();
|
|
2292
2392
|
init_student_store();
|
|
2393
|
+
init_narration_store();
|
|
2293
2394
|
}
|
|
2294
2395
|
});
|
|
2295
2396
|
|
|
2296
2397
|
// src/hooks/PDFViewerContext.tsx
|
|
2297
2398
|
function PDFViewerProvider({
|
|
2298
2399
|
children,
|
|
2299
|
-
initialState:
|
|
2400
|
+
initialState: initialState7,
|
|
2300
2401
|
theme = "light",
|
|
2301
2402
|
defaultSidebarPanel = "thumbnails",
|
|
2302
2403
|
studentMode: _studentMode = false
|
|
@@ -2308,22 +2409,22 @@ function PDFViewerProvider({
|
|
|
2308
2409
|
const studentStoreRef = (0, import_react2.useRef)(null);
|
|
2309
2410
|
if (!viewerStoreRef.current) {
|
|
2310
2411
|
viewerStoreRef.current = createViewerStore({
|
|
2311
|
-
...
|
|
2412
|
+
...initialState7?.viewer,
|
|
2312
2413
|
theme,
|
|
2313
2414
|
sidebarPanel: defaultSidebarPanel
|
|
2314
2415
|
});
|
|
2315
2416
|
}
|
|
2316
2417
|
if (!annotationStoreRef.current) {
|
|
2317
|
-
annotationStoreRef.current = createAnnotationStore(
|
|
2418
|
+
annotationStoreRef.current = createAnnotationStore(initialState7?.annotation);
|
|
2318
2419
|
}
|
|
2319
2420
|
if (!searchStoreRef.current) {
|
|
2320
|
-
searchStoreRef.current = createSearchStore(
|
|
2421
|
+
searchStoreRef.current = createSearchStore(initialState7?.search);
|
|
2321
2422
|
}
|
|
2322
2423
|
if (!agentStoreRef.current) {
|
|
2323
|
-
agentStoreRef.current = createAgentStore(
|
|
2424
|
+
agentStoreRef.current = createAgentStore(initialState7?.agent);
|
|
2324
2425
|
}
|
|
2325
2426
|
if (!studentStoreRef.current) {
|
|
2326
|
-
studentStoreRef.current = createStudentStore(
|
|
2427
|
+
studentStoreRef.current = createStudentStore(initialState7?.student);
|
|
2327
2428
|
}
|
|
2328
2429
|
(0, import_react2.useEffect)(() => {
|
|
2329
2430
|
return () => {
|
|
@@ -3647,8 +3748,8 @@ var init_PluginManager = __esm({
|
|
|
3647
3748
|
/**
|
|
3648
3749
|
* Get toolbar items by position
|
|
3649
3750
|
*/
|
|
3650
|
-
getToolbarItemsByPosition(
|
|
3651
|
-
return this.getToolbarItems().filter((item) => item.position ===
|
|
3751
|
+
getToolbarItemsByPosition(position2) {
|
|
3752
|
+
return this.getToolbarItems().filter((item) => item.position === position2);
|
|
3652
3753
|
}
|
|
3653
3754
|
/**
|
|
3654
3755
|
* Get all sidebar panels from all plugins
|
|
@@ -4761,7 +4862,7 @@ var init_MobileToolbar = __esm({
|
|
|
4761
4862
|
sidebarOpen,
|
|
4762
4863
|
theme,
|
|
4763
4864
|
onThemeChange,
|
|
4764
|
-
position = "bottom",
|
|
4865
|
+
position: position2 = "bottom",
|
|
4765
4866
|
className
|
|
4766
4867
|
}) {
|
|
4767
4868
|
const [showMoreMenu, setShowMoreMenu] = (0, import_react17.useState)(false);
|
|
@@ -4795,8 +4896,8 @@ var init_MobileToolbar = __esm({
|
|
|
4795
4896
|
"bg-white dark:bg-gray-800",
|
|
4796
4897
|
"border-gray-200 dark:border-gray-700",
|
|
4797
4898
|
"px-2 py-1 safe-area-inset",
|
|
4798
|
-
|
|
4799
|
-
|
|
4899
|
+
position2 === "top" && "top-0 border-b",
|
|
4900
|
+
position2 === "bottom" && "bottom-0 border-t",
|
|
4800
4901
|
className
|
|
4801
4902
|
),
|
|
4802
4903
|
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex items-center justify-between gap-1", children: [
|
|
@@ -4922,7 +5023,7 @@ var init_MobileToolbar = __esm({
|
|
|
4922
5023
|
"bg-white dark:bg-gray-800",
|
|
4923
5024
|
"rounded-lg shadow-lg",
|
|
4924
5025
|
"border border-gray-200 dark:border-gray-700",
|
|
4925
|
-
|
|
5026
|
+
position2 === "bottom" ? "bottom-full mb-2" : "top-full mt-2"
|
|
4926
5027
|
),
|
|
4927
5028
|
children: [
|
|
4928
5029
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "px-2 py-1 text-xs text-gray-500 dark:text-gray-400 font-medium", children: "Theme" }),
|
|
@@ -6748,7 +6849,7 @@ var init_AnnotationToolbar = __esm({
|
|
|
6748
6849
|
onShapeTypeChange: onShapeTypeChangeProp,
|
|
6749
6850
|
onColorChange: onColorChangeProp,
|
|
6750
6851
|
onStrokeWidthChange: onStrokeWidthChangeProp,
|
|
6751
|
-
position = "top",
|
|
6852
|
+
position: position2 = "top",
|
|
6752
6853
|
className
|
|
6753
6854
|
}) {
|
|
6754
6855
|
const storeActiveTool = useAnnotationStore((s) => s.activeAnnotationTool);
|
|
@@ -6798,9 +6899,9 @@ var init_AnnotationToolbar = __esm({
|
|
|
6798
6899
|
{
|
|
6799
6900
|
className: cn(
|
|
6800
6901
|
"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",
|
|
6801
|
-
|
|
6802
|
-
|
|
6803
|
-
|
|
6902
|
+
position2 === "floating" && "fixed bottom-20 left-1/2 -translate-x-1/2 z-50",
|
|
6903
|
+
position2 === "top" && "sticky top-0 z-40",
|
|
6904
|
+
position2 === "bottom" && "sticky bottom-0 z-40",
|
|
6804
6905
|
!isActive && "opacity-90",
|
|
6805
6906
|
className
|
|
6806
6907
|
),
|
|
@@ -8449,7 +8550,7 @@ var init_SelectionToolbar = __esm({
|
|
|
8449
8550
|
activeColor = "yellow",
|
|
8450
8551
|
className
|
|
8451
8552
|
}) {
|
|
8452
|
-
const [
|
|
8553
|
+
const [position2, setPosition] = (0, import_react35.useState)({ top: 0, left: 0, visible: false });
|
|
8453
8554
|
const toolbarRef = (0, import_react35.useRef)(null);
|
|
8454
8555
|
(0, import_react35.useEffect)(() => {
|
|
8455
8556
|
if (selection && selection.text && selection.rects.length > 0) {
|
|
@@ -8488,7 +8589,7 @@ var init_SelectionToolbar = __esm({
|
|
|
8488
8589
|
const handleCopy = (0, import_react35.useCallback)(() => {
|
|
8489
8590
|
onCopy?.();
|
|
8490
8591
|
}, [onCopy]);
|
|
8491
|
-
if (!
|
|
8592
|
+
if (!position2.visible || !selection?.text) {
|
|
8492
8593
|
return null;
|
|
8493
8594
|
}
|
|
8494
8595
|
return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
|
|
@@ -8506,8 +8607,8 @@ var init_SelectionToolbar = __esm({
|
|
|
8506
8607
|
className
|
|
8507
8608
|
),
|
|
8508
8609
|
style: {
|
|
8509
|
-
top:
|
|
8510
|
-
left:
|
|
8610
|
+
top: position2.top,
|
|
8611
|
+
left: position2.left,
|
|
8511
8612
|
transform: "translateX(-50%)"
|
|
8512
8613
|
},
|
|
8513
8614
|
onMouseDown: (e) => {
|
|
@@ -8624,7 +8725,7 @@ var init_HighlightPopover = __esm({
|
|
|
8624
8725
|
}) {
|
|
8625
8726
|
const [isEditingComment, setIsEditingComment] = (0, import_react36.useState)(false);
|
|
8626
8727
|
const [comment, setComment] = (0, import_react36.useState)(highlight?.comment ?? "");
|
|
8627
|
-
const [
|
|
8728
|
+
const [position2, setPosition] = (0, import_react36.useState)({ top: 0, left: 0, visible: false });
|
|
8628
8729
|
const popoverRef = (0, import_react36.useRef)(null);
|
|
8629
8730
|
const textareaRef = (0, import_react36.useRef)(null);
|
|
8630
8731
|
(0, import_react36.useEffect)(() => {
|
|
@@ -8672,11 +8773,11 @@ var init_HighlightPopover = __esm({
|
|
|
8672
8773
|
onClose();
|
|
8673
8774
|
}
|
|
8674
8775
|
}
|
|
8675
|
-
if (
|
|
8776
|
+
if (position2.visible) {
|
|
8676
8777
|
document.addEventListener("mousedown", handleClickOutside);
|
|
8677
8778
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
8678
8779
|
}
|
|
8679
|
-
}, [
|
|
8780
|
+
}, [position2.visible, onClose]);
|
|
8680
8781
|
(0, import_react36.useEffect)(() => {
|
|
8681
8782
|
function handleKeyDown(event) {
|
|
8682
8783
|
if (event.key === "Escape") {
|
|
@@ -8688,11 +8789,11 @@ var init_HighlightPopover = __esm({
|
|
|
8688
8789
|
}
|
|
8689
8790
|
}
|
|
8690
8791
|
}
|
|
8691
|
-
if (
|
|
8792
|
+
if (position2.visible) {
|
|
8692
8793
|
document.addEventListener("keydown", handleKeyDown);
|
|
8693
8794
|
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
8694
8795
|
}
|
|
8695
|
-
}, [
|
|
8796
|
+
}, [position2.visible, isEditingComment, highlight?.comment, onClose]);
|
|
8696
8797
|
const handleColorClick = (0, import_react36.useCallback)(
|
|
8697
8798
|
(color) => {
|
|
8698
8799
|
if (highlight) {
|
|
@@ -8719,7 +8820,7 @@ var init_HighlightPopover = __esm({
|
|
|
8719
8820
|
onCopy?.(highlight.text);
|
|
8720
8821
|
}
|
|
8721
8822
|
}, [highlight, onCopy]);
|
|
8722
|
-
if (!highlight || !
|
|
8823
|
+
if (!highlight || !position2.visible) {
|
|
8723
8824
|
return null;
|
|
8724
8825
|
}
|
|
8725
8826
|
return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
|
|
@@ -8736,8 +8837,8 @@ var init_HighlightPopover = __esm({
|
|
|
8736
8837
|
className
|
|
8737
8838
|
),
|
|
8738
8839
|
style: {
|
|
8739
|
-
top:
|
|
8740
|
-
left:
|
|
8840
|
+
top: position2.top,
|
|
8841
|
+
left: position2.left,
|
|
8741
8842
|
transform: "translate(-50%, -100%)",
|
|
8742
8843
|
width: 280
|
|
8743
8844
|
},
|
|
@@ -9964,10 +10065,23 @@ var init_BookModeContainer = __esm({
|
|
|
9964
10065
|
const scrollToPageRequest = useViewerStore((s) => s.scrollToPageRequest);
|
|
9965
10066
|
const { viewerStore } = usePDFViewerStores();
|
|
9966
10067
|
const [pages, setPages] = (0, import_react41.useState)([]);
|
|
9967
|
-
const [
|
|
10068
|
+
const [rawPageDims, setRawPageDims] = (0, import_react41.useState)({ width: 612, height: 792 });
|
|
9968
10069
|
const [isLoadingPages, setIsLoadingPages] = (0, import_react41.useState)(false);
|
|
10070
|
+
const containerRef = (0, import_react41.useRef)(null);
|
|
10071
|
+
const [containerSize, setContainerSize] = (0, import_react41.useState)({ width: 0, height: 0 });
|
|
9969
10072
|
const flipBookRef = (0, import_react41.useRef)(null);
|
|
9970
10073
|
const isSyncingRef = (0, import_react41.useRef)(false);
|
|
10074
|
+
(0, import_react41.useEffect)(() => {
|
|
10075
|
+
const el = containerRef.current;
|
|
10076
|
+
if (!el) return;
|
|
10077
|
+
const measure = () => {
|
|
10078
|
+
setContainerSize({ width: el.clientWidth, height: el.clientHeight });
|
|
10079
|
+
};
|
|
10080
|
+
measure();
|
|
10081
|
+
const ro = new ResizeObserver(measure);
|
|
10082
|
+
ro.observe(el);
|
|
10083
|
+
return () => ro.disconnect();
|
|
10084
|
+
}, []);
|
|
9971
10085
|
(0, import_react41.useEffect)(() => {
|
|
9972
10086
|
if (!document2) {
|
|
9973
10087
|
setPages([]);
|
|
@@ -9983,12 +10097,12 @@ var init_BookModeContainer = __esm({
|
|
|
9983
10097
|
}
|
|
9984
10098
|
const results = await Promise.allSettled(pagePromises);
|
|
9985
10099
|
if (!cancelled) {
|
|
9986
|
-
const
|
|
9987
|
-
setPages(
|
|
9988
|
-
const firstPage =
|
|
10100
|
+
const loaded2 = results.map((r) => r.status === "fulfilled" ? r.value : null);
|
|
10101
|
+
setPages(loaded2);
|
|
10102
|
+
const firstPage = loaded2[0];
|
|
9989
10103
|
if (firstPage) {
|
|
9990
|
-
const vp = firstPage.getViewport({ scale, rotation });
|
|
9991
|
-
|
|
10104
|
+
const vp = firstPage.getViewport({ scale: 1, rotation });
|
|
10105
|
+
setRawPageDims({ width: vp.width, height: vp.height });
|
|
9992
10106
|
}
|
|
9993
10107
|
}
|
|
9994
10108
|
} catch {
|
|
@@ -10000,13 +10114,27 @@ var init_BookModeContainer = __esm({
|
|
|
10000
10114
|
return () => {
|
|
10001
10115
|
cancelled = true;
|
|
10002
10116
|
};
|
|
10003
|
-
}, [document2, numPages,
|
|
10117
|
+
}, [document2, numPages, rotation]);
|
|
10004
10118
|
(0, import_react41.useEffect)(() => {
|
|
10005
10119
|
if (pages[0]) {
|
|
10006
|
-
const vp = pages[0].getViewport({ scale, rotation });
|
|
10007
|
-
|
|
10008
|
-
}
|
|
10009
|
-
}, [pages,
|
|
10120
|
+
const vp = pages[0].getViewport({ scale: 1, rotation });
|
|
10121
|
+
setRawPageDims({ width: vp.width, height: vp.height });
|
|
10122
|
+
}
|
|
10123
|
+
}, [pages, rotation]);
|
|
10124
|
+
const padding = 8;
|
|
10125
|
+
const fitWidth = Math.max(containerSize.width - padding * 2, 200);
|
|
10126
|
+
const fitHeight = Math.max(containerSize.height - padding * 2, 300);
|
|
10127
|
+
const pageAspect = rawPageDims.width / rawPageDims.height;
|
|
10128
|
+
let displayWidth;
|
|
10129
|
+
let displayHeight;
|
|
10130
|
+
if (fitWidth / fitHeight > pageAspect) {
|
|
10131
|
+
displayHeight = fitHeight;
|
|
10132
|
+
displayWidth = Math.floor(fitHeight * pageAspect);
|
|
10133
|
+
} else {
|
|
10134
|
+
displayWidth = fitWidth;
|
|
10135
|
+
displayHeight = Math.floor(fitWidth / pageAspect);
|
|
10136
|
+
}
|
|
10137
|
+
const renderScale = displayWidth / rawPageDims.width;
|
|
10010
10138
|
(0, import_react41.useEffect)(() => {
|
|
10011
10139
|
const pageFlip = flipBookRef.current?.pageFlip();
|
|
10012
10140
|
if (!pageFlip) return;
|
|
@@ -10040,18 +10168,15 @@ var init_BookModeContainer = __esm({
|
|
|
10040
10168
|
sepia: "bg-amber-50"
|
|
10041
10169
|
};
|
|
10042
10170
|
const themeClass = theme === "dark" ? "dark" : theme === "sepia" ? "sepia" : "";
|
|
10043
|
-
|
|
10044
|
-
|
|
10045
|
-
}
|
|
10046
|
-
if (isLoadingPages || pages.length === 0) {
|
|
10047
|
-
return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("div", { className: cn("document-container", "flex-1", themeStyles[theme], className), children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(PDFLoadingScreen, { phase: "rendering" }) });
|
|
10048
|
-
}
|
|
10171
|
+
const ready = !!document2 && !isLoadingPages && pages.length > 0;
|
|
10172
|
+
const hasContainer = containerSize.width > 0 && containerSize.height > 0;
|
|
10049
10173
|
return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(
|
|
10050
10174
|
"div",
|
|
10051
10175
|
{
|
|
10176
|
+
ref: containerRef,
|
|
10052
10177
|
className: cn(
|
|
10053
10178
|
"book-mode-container",
|
|
10054
|
-
"flex-1 overflow-hidden",
|
|
10179
|
+
"flex-1 h-full w-full overflow-hidden",
|
|
10055
10180
|
"flex items-center justify-center",
|
|
10056
10181
|
themeStyles[theme],
|
|
10057
10182
|
themeClass,
|
|
@@ -10059,24 +10184,30 @@ var init_BookModeContainer = __esm({
|
|
|
10059
10184
|
),
|
|
10060
10185
|
style: { userSelect: "none", WebkitUserSelect: "none" },
|
|
10061
10186
|
children: [
|
|
10062
|
-
/* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
|
|
10187
|
+
!ready && /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
|
|
10188
|
+
PDFLoadingScreen,
|
|
10189
|
+
{
|
|
10190
|
+
phase: !document2 ? isLoading ? "fetching" : "initializing" : "rendering"
|
|
10191
|
+
}
|
|
10192
|
+
),
|
|
10193
|
+
ready && hasContainer && /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
|
|
10063
10194
|
import_react_pageflip.default,
|
|
10064
10195
|
{
|
|
10065
10196
|
ref: flipBookRef,
|
|
10066
|
-
width:
|
|
10067
|
-
height:
|
|
10068
|
-
size: "
|
|
10069
|
-
minWidth:
|
|
10070
|
-
maxWidth:
|
|
10071
|
-
minHeight:
|
|
10072
|
-
maxHeight:
|
|
10197
|
+
width: displayWidth,
|
|
10198
|
+
height: displayHeight,
|
|
10199
|
+
size: "fixed",
|
|
10200
|
+
minWidth: displayWidth,
|
|
10201
|
+
maxWidth: displayWidth,
|
|
10202
|
+
minHeight: displayHeight,
|
|
10203
|
+
maxHeight: displayHeight,
|
|
10073
10204
|
drawShadow,
|
|
10074
10205
|
maxShadowOpacity,
|
|
10075
10206
|
flippingTime,
|
|
10076
10207
|
usePortrait: true,
|
|
10077
10208
|
startPage: currentPage - 1,
|
|
10078
10209
|
showCover: false,
|
|
10079
|
-
mobileScrollSupport:
|
|
10210
|
+
mobileScrollSupport: true,
|
|
10080
10211
|
swipeDistance: 30,
|
|
10081
10212
|
showPageCorners: true,
|
|
10082
10213
|
useMouseEvents: true,
|
|
@@ -10085,7 +10216,7 @@ var init_BookModeContainer = __esm({
|
|
|
10085
10216
|
className: "book-flipbook",
|
|
10086
10217
|
style: {},
|
|
10087
10218
|
startZIndex: 0,
|
|
10088
|
-
autoSize:
|
|
10219
|
+
autoSize: false,
|
|
10089
10220
|
renderOnlyPageLengthChange: false,
|
|
10090
10221
|
disableFlipByClick: false,
|
|
10091
10222
|
children: pages.map((page, index) => /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
|
|
@@ -10093,20 +10224,15 @@ var init_BookModeContainer = __esm({
|
|
|
10093
10224
|
{
|
|
10094
10225
|
pageNumber: index + 1,
|
|
10095
10226
|
page,
|
|
10096
|
-
scale,
|
|
10227
|
+
scale: renderScale,
|
|
10097
10228
|
rotation,
|
|
10098
|
-
width:
|
|
10099
|
-
height:
|
|
10229
|
+
width: displayWidth,
|
|
10230
|
+
height: displayHeight
|
|
10100
10231
|
},
|
|
10101
10232
|
index
|
|
10102
10233
|
))
|
|
10103
10234
|
}
|
|
10104
|
-
)
|
|
10105
|
-
/* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("div", { className: "book-page-indicator", children: [
|
|
10106
|
-
currentPage,
|
|
10107
|
-
" / ",
|
|
10108
|
-
numPages
|
|
10109
|
-
] })
|
|
10235
|
+
)
|
|
10110
10236
|
]
|
|
10111
10237
|
}
|
|
10112
10238
|
);
|
|
@@ -10124,7 +10250,7 @@ var init_FloatingZoomControls = __esm({
|
|
|
10124
10250
|
init_utils();
|
|
10125
10251
|
import_jsx_runtime28 = require("react/jsx-runtime");
|
|
10126
10252
|
FloatingZoomControls = (0, import_react42.memo)(function FloatingZoomControls2({
|
|
10127
|
-
position = "bottom-right",
|
|
10253
|
+
position: position2 = "bottom-right",
|
|
10128
10254
|
className,
|
|
10129
10255
|
showFitToWidth = true,
|
|
10130
10256
|
showFitToPage = false,
|
|
@@ -10165,7 +10291,7 @@ var init_FloatingZoomControls = __esm({
|
|
|
10165
10291
|
"bg-white dark:bg-gray-800 rounded-lg shadow-lg",
|
|
10166
10292
|
"border border-gray-200 dark:border-gray-700",
|
|
10167
10293
|
"p-1",
|
|
10168
|
-
positionClasses[
|
|
10294
|
+
positionClasses[position2],
|
|
10169
10295
|
className
|
|
10170
10296
|
),
|
|
10171
10297
|
children: [
|
|
@@ -10871,12 +10997,12 @@ var init_PDFViewerClient = __esm({
|
|
|
10871
10997
|
src,
|
|
10872
10998
|
workerSrc,
|
|
10873
10999
|
signal: abortController.signal,
|
|
10874
|
-
onProgress: ({ loaded, total }) => {
|
|
11000
|
+
onProgress: ({ loaded: loaded2, total }) => {
|
|
10875
11001
|
if (!mountedRef.current || srcIdRef.current !== loadId || abortController.signal.aborted) {
|
|
10876
11002
|
return;
|
|
10877
11003
|
}
|
|
10878
11004
|
const now = Date.now();
|
|
10879
|
-
const percent = total > 0 ? Math.round(
|
|
11005
|
+
const percent = total > 0 ? Math.round(loaded2 / total * 100) : 0;
|
|
10880
11006
|
const timePassed = now - lastProgressUpdate >= PROGRESS_THROTTLE_MS;
|
|
10881
11007
|
const percentChanged = Math.abs(percent - lastPercent) >= PROGRESS_MIN_CHANGE;
|
|
10882
11008
|
const isComplete = percent >= 100;
|
|
@@ -10887,10 +11013,10 @@ var init_PDFViewerClient = __esm({
|
|
|
10887
11013
|
loadingProgress: {
|
|
10888
11014
|
phase: "fetching",
|
|
10889
11015
|
percent,
|
|
10890
|
-
bytesLoaded:
|
|
11016
|
+
bytesLoaded: loaded2,
|
|
10891
11017
|
totalBytes: total
|
|
10892
11018
|
},
|
|
10893
|
-
streamingProgress: { loaded, total },
|
|
11019
|
+
streamingProgress: { loaded: loaded2, total },
|
|
10894
11020
|
documentLoadingState: "loading"
|
|
10895
11021
|
});
|
|
10896
11022
|
}
|
|
@@ -11169,19 +11295,26 @@ var init_PDFViewer2 = __esm({
|
|
|
11169
11295
|
// src/index.ts
|
|
11170
11296
|
var index_exports = {};
|
|
11171
11297
|
__export(index_exports, {
|
|
11298
|
+
AnimatedHighlight: () => AnimatedHighlight,
|
|
11299
|
+
AnimatedUnderline: () => AnimatedUnderline,
|
|
11172
11300
|
AnnotationLayer: () => AnnotationLayer,
|
|
11173
11301
|
AnnotationToolbar: () => AnnotationToolbar,
|
|
11174
11302
|
AskAboutOverlay: () => AskAboutOverlay,
|
|
11175
11303
|
AskAboutTrigger: () => AskAboutTrigger,
|
|
11176
11304
|
BookModeContainer: () => BookModeContainer,
|
|
11177
11305
|
BookmarksPanel: () => BookmarksPanel,
|
|
11306
|
+
BoxOverlay: () => BoxOverlay,
|
|
11307
|
+
CalloutArrow: () => CalloutArrow,
|
|
11308
|
+
CameraView: () => CameraView,
|
|
11178
11309
|
CanvasLayer: () => CanvasLayer,
|
|
11310
|
+
CinemaLayer: () => CinemaLayer,
|
|
11179
11311
|
ContinuousScrollContainer: () => ContinuousScrollContainer,
|
|
11180
11312
|
DocumentContainer: () => DocumentContainer,
|
|
11181
11313
|
DrawingCanvas: () => DrawingCanvas,
|
|
11182
11314
|
DualPageContainer: () => DualPageContainer,
|
|
11183
11315
|
FloatingZoomControls: () => FloatingZoomControls,
|
|
11184
11316
|
FocusRegionLayer: () => FocusRegionLayer,
|
|
11317
|
+
GhostReference: () => GhostReference,
|
|
11185
11318
|
HighlightLayer: () => HighlightLayer,
|
|
11186
11319
|
HighlightPopover: () => HighlightPopover,
|
|
11187
11320
|
HighlightsPanel: () => HighlightsPanel,
|
|
@@ -11198,32 +11331,46 @@ __export(index_exports, {
|
|
|
11198
11331
|
PDFViewerContext: () => PDFViewerContext,
|
|
11199
11332
|
PDFViewerProvider: () => PDFViewerProvider,
|
|
11200
11333
|
PluginManager: () => PluginManager,
|
|
11334
|
+
PulseOverlay: () => PulseOverlay,
|
|
11201
11335
|
QuickNoteButton: () => QuickNoteButton,
|
|
11202
11336
|
QuickNotePopover: () => QuickNotePopover,
|
|
11337
|
+
SYSTEM_PROMPT: () => SYSTEM_PROMPT,
|
|
11203
11338
|
SearchPanel: () => SearchPanel,
|
|
11204
11339
|
SelectionToolbar: () => SelectionToolbar,
|
|
11205
11340
|
ShapePreview: () => ShapePreview,
|
|
11206
11341
|
ShapeRenderer: () => ShapeRenderer,
|
|
11207
11342
|
Sidebar: () => Sidebar,
|
|
11343
|
+
SpotlightMask: () => SpotlightMask,
|
|
11344
|
+
StickyLabel: () => StickyLabel,
|
|
11208
11345
|
StickyNote: () => StickyNote,
|
|
11346
|
+
StoryboardActionSchema: () => StoryboardActionSchema,
|
|
11347
|
+
StoryboardEngine: () => StoryboardEngine,
|
|
11348
|
+
StoryboardSchema: () => StoryboardSchema,
|
|
11349
|
+
SubtitleBar: () => SubtitleBar,
|
|
11209
11350
|
TakeawaysPanel: () => TakeawaysPanel,
|
|
11210
11351
|
TextLayer: () => TextLayer,
|
|
11211
11352
|
ThumbnailPanel: () => ThumbnailPanel,
|
|
11212
11353
|
Toolbar: () => Toolbar,
|
|
11354
|
+
TutorModeContainer: () => TutorModeContainer,
|
|
11213
11355
|
VirtualizedDocumentContainer: () => VirtualizedDocumentContainer,
|
|
11214
11356
|
applyRotation: () => applyRotation,
|
|
11357
|
+
buildBBoxIndex: () => buildBBoxIndex,
|
|
11358
|
+
buildUserPrompt: () => buildUserPrompt,
|
|
11215
11359
|
clearHighlights: () => clearHighlights,
|
|
11216
11360
|
clearStudentData: () => clearStudentData,
|
|
11217
11361
|
cn: () => cn,
|
|
11362
|
+
cosineSimilarity: () => cosineSimilarity,
|
|
11218
11363
|
countTextOnPage: () => countTextOnPage,
|
|
11219
11364
|
createAgentAPI: () => createAgentAPI,
|
|
11220
11365
|
createAgentStore: () => createAgentStore,
|
|
11221
11366
|
createAnnotationStore: () => createAnnotationStore,
|
|
11367
|
+
createNarrationStore: () => createNarrationStore,
|
|
11222
11368
|
createPDFViewer: () => createPDFViewer,
|
|
11223
11369
|
createPluginManager: () => createPluginManager,
|
|
11224
11370
|
createSearchStore: () => createSearchStore,
|
|
11225
11371
|
createStudentStore: () => createStudentStore,
|
|
11226
11372
|
createViewerStore: () => createViewerStore,
|
|
11373
|
+
directStoryboard: () => directStoryboard,
|
|
11227
11374
|
doRectsIntersect: () => doRectsIntersect,
|
|
11228
11375
|
downloadAnnotationsAsJSON: () => downloadAnnotationsAsJSON,
|
|
11229
11376
|
downloadAnnotationsAsMarkdown: () => downloadAnnotationsAsMarkdown,
|
|
@@ -11238,6 +11385,7 @@ __export(index_exports, {
|
|
|
11238
11385
|
generateDocumentId: () => generateDocumentId,
|
|
11239
11386
|
getAllDocumentIds: () => getAllDocumentIds,
|
|
11240
11387
|
getAllStudentDataDocumentIds: () => getAllStudentDataDocumentIds,
|
|
11388
|
+
getLocalMiniLM: () => getLocalMiniLM,
|
|
11241
11389
|
getMetadata: () => getMetadata,
|
|
11242
11390
|
getOutline: () => getOutline,
|
|
11243
11391
|
getPage: () => getPage,
|
|
@@ -11255,6 +11403,8 @@ __export(index_exports, {
|
|
|
11255
11403
|
loadDocumentWithCallbacks: () => loadDocumentWithCallbacks,
|
|
11256
11404
|
loadHighlights: () => loadHighlights,
|
|
11257
11405
|
loadStudentData: () => loadStudentData,
|
|
11406
|
+
makeOverlayId: () => makeOverlayId,
|
|
11407
|
+
matchChunkToBlock: () => matchChunkToBlock,
|
|
11258
11408
|
mergeAdjacentRects: () => mergeAdjacentRects,
|
|
11259
11409
|
pdfToPercent: () => pdfToPercent,
|
|
11260
11410
|
pdfToViewport: () => pdfToViewport,
|
|
@@ -11267,6 +11417,9 @@ __export(index_exports, {
|
|
|
11267
11417
|
saveHighlights: () => saveHighlights,
|
|
11268
11418
|
saveStudentData: () => saveStudentData,
|
|
11269
11419
|
scaleRect: () => scaleRect,
|
|
11420
|
+
storyboardFromMatch: () => storyboardFromMatch,
|
|
11421
|
+
storyboardJsonSchema: () => storyboardJsonSchema,
|
|
11422
|
+
truncate: () => truncate,
|
|
11270
11423
|
useAgentContext: () => useAgentContext,
|
|
11271
11424
|
useAgentStore: () => useAgentStore,
|
|
11272
11425
|
useAnnotationStore: () => useAnnotationStore,
|
|
@@ -11966,7 +12119,7 @@ var import_jsx_runtime34 = require("react/jsx-runtime");
|
|
|
11966
12119
|
var QuickNoteButton = (0, import_react48.memo)(function QuickNoteButton2({
|
|
11967
12120
|
pageNumber,
|
|
11968
12121
|
scale,
|
|
11969
|
-
position = "top-right",
|
|
12122
|
+
position: position2 = "top-right",
|
|
11970
12123
|
onClick,
|
|
11971
12124
|
className,
|
|
11972
12125
|
visible = true
|
|
@@ -11975,11 +12128,11 @@ var QuickNoteButton = (0, import_react48.memo)(function QuickNoteButton2({
|
|
|
11975
12128
|
const handleClick = (0, import_react48.useCallback)(
|
|
11976
12129
|
(e) => {
|
|
11977
12130
|
e.stopPropagation();
|
|
11978
|
-
const x =
|
|
11979
|
-
const y =
|
|
12131
|
+
const x = position2 === "top-right" ? 80 : 80;
|
|
12132
|
+
const y = position2 === "top-right" ? 20 : 80;
|
|
11980
12133
|
onClick(pageNumber, x / scale, y / scale);
|
|
11981
12134
|
},
|
|
11982
|
-
[pageNumber, onClick,
|
|
12135
|
+
[pageNumber, onClick, position2, scale]
|
|
11983
12136
|
);
|
|
11984
12137
|
if (!visible) {
|
|
11985
12138
|
return null;
|
|
@@ -12000,8 +12153,8 @@ var QuickNoteButton = (0, import_react48.memo)(function QuickNoteButton2({
|
|
|
12000
12153
|
"transition-all duration-200",
|
|
12001
12154
|
"focus:outline-none focus:ring-2 focus:ring-yellow-500 focus:ring-offset-2",
|
|
12002
12155
|
isHovered && "scale-110",
|
|
12003
|
-
|
|
12004
|
-
|
|
12156
|
+
position2 === "top-right" && "top-3 right-3",
|
|
12157
|
+
position2 === "bottom-right" && "bottom-3 right-3",
|
|
12005
12158
|
className
|
|
12006
12159
|
),
|
|
12007
12160
|
title: "Add quick note",
|
|
@@ -12027,7 +12180,7 @@ init_utils();
|
|
|
12027
12180
|
var import_jsx_runtime35 = require("react/jsx-runtime");
|
|
12028
12181
|
var QuickNotePopover = (0, import_react49.memo)(function QuickNotePopover2({
|
|
12029
12182
|
visible,
|
|
12030
|
-
position,
|
|
12183
|
+
position: position2,
|
|
12031
12184
|
initialContent = "",
|
|
12032
12185
|
agentLastStatement,
|
|
12033
12186
|
onSave,
|
|
@@ -12037,7 +12190,7 @@ var QuickNotePopover = (0, import_react49.memo)(function QuickNotePopover2({
|
|
|
12037
12190
|
const [content, setContent] = (0, import_react49.useState)(initialContent);
|
|
12038
12191
|
const textareaRef = (0, import_react49.useRef)(null);
|
|
12039
12192
|
const popoverRef = (0, import_react49.useRef)(null);
|
|
12040
|
-
const [adjustedPosition, setAdjustedPosition] = (0, import_react49.useState)(
|
|
12193
|
+
const [adjustedPosition, setAdjustedPosition] = (0, import_react49.useState)(position2);
|
|
12041
12194
|
(0, import_react49.useEffect)(() => {
|
|
12042
12195
|
if (visible && textareaRef.current) {
|
|
12043
12196
|
textareaRef.current.focus();
|
|
@@ -12052,7 +12205,7 @@ var QuickNotePopover = (0, import_react49.memo)(function QuickNotePopover2({
|
|
|
12052
12205
|
if (!visible || !popoverRef.current) return;
|
|
12053
12206
|
const rect = popoverRef.current.getBoundingClientRect();
|
|
12054
12207
|
const padding = 10;
|
|
12055
|
-
let { x, y } =
|
|
12208
|
+
let { x, y } = position2;
|
|
12056
12209
|
if (x + rect.width > window.innerWidth - padding) {
|
|
12057
12210
|
x = window.innerWidth - rect.width - padding;
|
|
12058
12211
|
}
|
|
@@ -12066,7 +12219,7 @@ var QuickNotePopover = (0, import_react49.memo)(function QuickNotePopover2({
|
|
|
12066
12219
|
y = padding;
|
|
12067
12220
|
}
|
|
12068
12221
|
setAdjustedPosition({ x, y });
|
|
12069
|
-
}, [
|
|
12222
|
+
}, [position2, visible]);
|
|
12070
12223
|
const handleSave = (0, import_react49.useCallback)(() => {
|
|
12071
12224
|
if (content.trim()) {
|
|
12072
12225
|
onSave(content.trim());
|
|
@@ -12183,11 +12336,11 @@ var import_jsx_runtime36 = require("react/jsx-runtime");
|
|
|
12183
12336
|
var AskAboutOverlay = (0, import_react50.memo)(function AskAboutOverlay2({
|
|
12184
12337
|
visible,
|
|
12185
12338
|
progress,
|
|
12186
|
-
position,
|
|
12339
|
+
position: position2,
|
|
12187
12340
|
size = 60,
|
|
12188
12341
|
className
|
|
12189
12342
|
}) {
|
|
12190
|
-
if (!visible || !
|
|
12343
|
+
if (!visible || !position2) {
|
|
12191
12344
|
return null;
|
|
12192
12345
|
}
|
|
12193
12346
|
const strokeWidth = 4;
|
|
@@ -12203,8 +12356,8 @@ var AskAboutOverlay = (0, import_react50.memo)(function AskAboutOverlay2({
|
|
|
12203
12356
|
className
|
|
12204
12357
|
),
|
|
12205
12358
|
style: {
|
|
12206
|
-
left:
|
|
12207
|
-
top:
|
|
12359
|
+
left: position2.x - size / 2,
|
|
12360
|
+
top: position2.y - size / 2
|
|
12208
12361
|
},
|
|
12209
12362
|
children: [
|
|
12210
12363
|
/* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(
|
|
@@ -12292,20 +12445,20 @@ var import_react51 = require("react");
|
|
|
12292
12445
|
init_utils();
|
|
12293
12446
|
var import_jsx_runtime37 = require("react/jsx-runtime");
|
|
12294
12447
|
var AskAboutTrigger = (0, import_react51.memo)(function AskAboutTrigger2({
|
|
12295
|
-
position,
|
|
12448
|
+
position: position2,
|
|
12296
12449
|
onConfirm,
|
|
12297
12450
|
onCancel,
|
|
12298
12451
|
visible,
|
|
12299
12452
|
autoHideDelay = 5e3,
|
|
12300
12453
|
className
|
|
12301
12454
|
}) {
|
|
12302
|
-
const [adjustedPosition, setAdjustedPosition] = (0, import_react51.useState)(
|
|
12455
|
+
const [adjustedPosition, setAdjustedPosition] = (0, import_react51.useState)(position2);
|
|
12303
12456
|
const triggerRef = (0, import_react51.useRef)(null);
|
|
12304
12457
|
(0, import_react51.useEffect)(() => {
|
|
12305
12458
|
if (!visible || !triggerRef.current) return;
|
|
12306
12459
|
const rect = triggerRef.current.getBoundingClientRect();
|
|
12307
12460
|
const padding = 10;
|
|
12308
|
-
let { x, y } =
|
|
12461
|
+
let { x, y } = position2;
|
|
12309
12462
|
if (x + rect.width / 2 > window.innerWidth - padding) {
|
|
12310
12463
|
x = window.innerWidth - rect.width / 2 - padding;
|
|
12311
12464
|
}
|
|
@@ -12313,10 +12466,10 @@ var AskAboutTrigger = (0, import_react51.memo)(function AskAboutTrigger2({
|
|
|
12313
12466
|
x = rect.width / 2 + padding;
|
|
12314
12467
|
}
|
|
12315
12468
|
if (y + rect.height > window.innerHeight - padding) {
|
|
12316
|
-
y =
|
|
12469
|
+
y = position2.y - rect.height - 20;
|
|
12317
12470
|
}
|
|
12318
12471
|
setAdjustedPosition({ x, y });
|
|
12319
|
-
}, [
|
|
12472
|
+
}, [position2, visible]);
|
|
12320
12473
|
(0, import_react51.useEffect)(() => {
|
|
12321
12474
|
if (!visible || autoHideDelay === 0) return;
|
|
12322
12475
|
const timer = setTimeout(onCancel, autoHideDelay);
|
|
@@ -12964,6 +13117,2258 @@ function withErrorBoundary({ component, ...props }) {
|
|
|
12964
13117
|
// src/components/index.ts
|
|
12965
13118
|
init_PDFLoadingScreen2();
|
|
12966
13119
|
|
|
13120
|
+
// src/components/TutorMode/TutorModeContainer.tsx
|
|
13121
|
+
var import_react56 = require("react");
|
|
13122
|
+
var import_zustand2 = require("zustand");
|
|
13123
|
+
init_PDFPage2();
|
|
13124
|
+
init_hooks();
|
|
13125
|
+
|
|
13126
|
+
// src/components/TutorMode/CameraView.tsx
|
|
13127
|
+
var import_framer_motion = require("framer-motion");
|
|
13128
|
+
var import_jsx_runtime41 = require("react/jsx-runtime");
|
|
13129
|
+
function CameraView({
|
|
13130
|
+
camera,
|
|
13131
|
+
children,
|
|
13132
|
+
durationMs = 700,
|
|
13133
|
+
className
|
|
13134
|
+
}) {
|
|
13135
|
+
return /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
|
|
13136
|
+
import_framer_motion.motion.div,
|
|
13137
|
+
{
|
|
13138
|
+
className,
|
|
13139
|
+
style: {
|
|
13140
|
+
transformOrigin: "50% 50%",
|
|
13141
|
+
willChange: "transform",
|
|
13142
|
+
width: "100%",
|
|
13143
|
+
height: "100%",
|
|
13144
|
+
position: "relative"
|
|
13145
|
+
},
|
|
13146
|
+
animate: {
|
|
13147
|
+
scale: camera.scale,
|
|
13148
|
+
x: camera.x,
|
|
13149
|
+
y: camera.y
|
|
13150
|
+
},
|
|
13151
|
+
transition: {
|
|
13152
|
+
duration: durationMs / 1e3,
|
|
13153
|
+
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]
|
|
13154
|
+
},
|
|
13155
|
+
children
|
|
13156
|
+
}
|
|
13157
|
+
);
|
|
13158
|
+
}
|
|
13159
|
+
|
|
13160
|
+
// src/components/TutorMode/CinemaLayer.tsx
|
|
13161
|
+
var import_framer_motion10 = require("framer-motion");
|
|
13162
|
+
|
|
13163
|
+
// src/components/TutorMode/SpotlightMask.tsx
|
|
13164
|
+
var import_react55 = require("react");
|
|
13165
|
+
var import_framer_motion2 = require("framer-motion");
|
|
13166
|
+
var import_jsx_runtime42 = require("react/jsx-runtime");
|
|
13167
|
+
function SpotlightMask({
|
|
13168
|
+
page,
|
|
13169
|
+
bbox,
|
|
13170
|
+
action,
|
|
13171
|
+
durationMs = 400
|
|
13172
|
+
}) {
|
|
13173
|
+
const maskId = (0, import_react55.useId)();
|
|
13174
|
+
const filterId = `${maskId}-blur`;
|
|
13175
|
+
const [x1, y1, x2, y2] = bbox;
|
|
13176
|
+
const w = Math.max(0, x2 - x1);
|
|
13177
|
+
const h = Math.max(0, y2 - y1);
|
|
13178
|
+
const rx = action.shape === "rounded" ? 12 : action.shape === "ellipse" ? w / 2 : 0;
|
|
13179
|
+
const ry = action.shape === "rounded" ? 12 : action.shape === "ellipse" ? h / 2 : 0;
|
|
13180
|
+
const feather = action.feather_px;
|
|
13181
|
+
return /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(
|
|
13182
|
+
"svg",
|
|
13183
|
+
{
|
|
13184
|
+
viewBox: `0 0 ${page.width} ${page.height}`,
|
|
13185
|
+
width: page.width,
|
|
13186
|
+
height: page.height,
|
|
13187
|
+
preserveAspectRatio: "none",
|
|
13188
|
+
style: {
|
|
13189
|
+
position: "absolute",
|
|
13190
|
+
inset: 0,
|
|
13191
|
+
pointerEvents: "none",
|
|
13192
|
+
width: page.width,
|
|
13193
|
+
height: page.height
|
|
13194
|
+
},
|
|
13195
|
+
"data-role": "spotlight-mask",
|
|
13196
|
+
children: [
|
|
13197
|
+
/* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("defs", { children: [
|
|
13198
|
+
/* @__PURE__ */ (0, import_jsx_runtime42.jsx)("filter", { id: filterId, x: "-20%", y: "-20%", width: "140%", height: "140%", children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("feGaussianBlur", { in: "SourceGraphic", stdDeviation: feather / 4 }) }),
|
|
13199
|
+
/* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("mask", { id: maskId, children: [
|
|
13200
|
+
/* @__PURE__ */ (0, import_jsx_runtime42.jsx)("rect", { x: 0, y: 0, width: page.width, height: page.height, fill: "white" }),
|
|
13201
|
+
action.shape === "ellipse" ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
|
|
13202
|
+
"ellipse",
|
|
13203
|
+
{
|
|
13204
|
+
cx: (x1 + x2) / 2,
|
|
13205
|
+
cy: (y1 + y2) / 2,
|
|
13206
|
+
rx: w / 2,
|
|
13207
|
+
ry: h / 2,
|
|
13208
|
+
fill: "black",
|
|
13209
|
+
filter: `url(#${filterId})`
|
|
13210
|
+
}
|
|
13211
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
|
|
13212
|
+
"rect",
|
|
13213
|
+
{
|
|
13214
|
+
x: x1,
|
|
13215
|
+
y: y1,
|
|
13216
|
+
width: w,
|
|
13217
|
+
height: h,
|
|
13218
|
+
rx,
|
|
13219
|
+
ry,
|
|
13220
|
+
fill: "black",
|
|
13221
|
+
filter: `url(#${filterId})`
|
|
13222
|
+
}
|
|
13223
|
+
)
|
|
13224
|
+
] })
|
|
13225
|
+
] }),
|
|
13226
|
+
/* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
|
|
13227
|
+
import_framer_motion2.motion.rect,
|
|
13228
|
+
{
|
|
13229
|
+
x: 0,
|
|
13230
|
+
y: 0,
|
|
13231
|
+
width: page.width,
|
|
13232
|
+
height: page.height,
|
|
13233
|
+
fill: "black",
|
|
13234
|
+
mask: `url(#${maskId})`,
|
|
13235
|
+
initial: { fillOpacity: 0 },
|
|
13236
|
+
animate: { fillOpacity: action.dim_opacity },
|
|
13237
|
+
exit: { fillOpacity: 0 },
|
|
13238
|
+
transition: { duration: durationMs / 1e3, ease: "easeOut" }
|
|
13239
|
+
}
|
|
13240
|
+
)
|
|
13241
|
+
]
|
|
13242
|
+
}
|
|
13243
|
+
);
|
|
13244
|
+
}
|
|
13245
|
+
|
|
13246
|
+
// src/components/TutorMode/AnimatedUnderline.tsx
|
|
13247
|
+
var import_framer_motion3 = require("framer-motion");
|
|
13248
|
+
var import_jsx_runtime43 = require("react/jsx-runtime");
|
|
13249
|
+
function pathForStyle(x1, x2, y, style) {
|
|
13250
|
+
if (style === "straight") return `M ${x1} ${y} L ${x2} ${y}`;
|
|
13251
|
+
if (style === "double")
|
|
13252
|
+
return `M ${x1} ${y - 3} L ${x2} ${y - 3} M ${x1} ${y + 3} L ${x2} ${y + 3}`;
|
|
13253
|
+
if (style === "wavy") {
|
|
13254
|
+
const steps = Math.max(8, Math.floor((x2 - x1) / 18));
|
|
13255
|
+
let d2 = `M ${x1} ${y}`;
|
|
13256
|
+
for (let i = 1; i <= steps; i++) {
|
|
13257
|
+
const px = x1 + (x2 - x1) * i / steps;
|
|
13258
|
+
const dy = i % 2 === 0 ? 4 : -4;
|
|
13259
|
+
d2 += ` Q ${px - (x2 - x1) / (2 * steps)} ${y + dy} ${px} ${y}`;
|
|
13260
|
+
}
|
|
13261
|
+
return d2;
|
|
13262
|
+
}
|
|
13263
|
+
const segs = 6;
|
|
13264
|
+
let d = `M ${x1} ${y}`;
|
|
13265
|
+
for (let i = 1; i <= segs; i++) {
|
|
13266
|
+
const px = x1 + (x2 - x1) * i / segs;
|
|
13267
|
+
const jitter = (Math.random() - 0.5) * 4;
|
|
13268
|
+
d += ` L ${px} ${y + jitter}`;
|
|
13269
|
+
}
|
|
13270
|
+
return d;
|
|
13271
|
+
}
|
|
13272
|
+
function AnimatedUnderline({ bbox, action }) {
|
|
13273
|
+
const [x1, , x2, y2] = bbox;
|
|
13274
|
+
const y = y2 + 6;
|
|
13275
|
+
const d = pathForStyle(x1, x2, y, action.style);
|
|
13276
|
+
const duration = action.draw_duration_ms / 1e3;
|
|
13277
|
+
return /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
|
|
13278
|
+
"svg",
|
|
13279
|
+
{
|
|
13280
|
+
style: {
|
|
13281
|
+
position: "absolute",
|
|
13282
|
+
inset: 0,
|
|
13283
|
+
pointerEvents: "none",
|
|
13284
|
+
overflow: "visible"
|
|
13285
|
+
},
|
|
13286
|
+
"data-role": "underline",
|
|
13287
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
|
|
13288
|
+
import_framer_motion3.motion.path,
|
|
13289
|
+
{
|
|
13290
|
+
d,
|
|
13291
|
+
fill: "none",
|
|
13292
|
+
stroke: action.color,
|
|
13293
|
+
strokeWidth: 4,
|
|
13294
|
+
strokeLinecap: "round",
|
|
13295
|
+
initial: { pathLength: 0, opacity: 0 },
|
|
13296
|
+
animate: { pathLength: 1, opacity: 1 },
|
|
13297
|
+
exit: { opacity: 0 },
|
|
13298
|
+
transition: { duration, ease: "easeOut" }
|
|
13299
|
+
}
|
|
13300
|
+
)
|
|
13301
|
+
}
|
|
13302
|
+
);
|
|
13303
|
+
}
|
|
13304
|
+
|
|
13305
|
+
// src/components/TutorMode/AnimatedHighlight.tsx
|
|
13306
|
+
var import_framer_motion4 = require("framer-motion");
|
|
13307
|
+
var import_jsx_runtime44 = require("react/jsx-runtime");
|
|
13308
|
+
function AnimatedHighlight({ bbox, action }) {
|
|
13309
|
+
const [x1, y1, x2, y2] = bbox;
|
|
13310
|
+
const w = x2 - x1;
|
|
13311
|
+
const h = y2 - y1;
|
|
13312
|
+
return /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
|
|
13313
|
+
import_framer_motion4.motion.div,
|
|
13314
|
+
{
|
|
13315
|
+
style: {
|
|
13316
|
+
position: "absolute",
|
|
13317
|
+
left: x1,
|
|
13318
|
+
top: y1,
|
|
13319
|
+
height: h,
|
|
13320
|
+
background: action.color,
|
|
13321
|
+
borderRadius: 4,
|
|
13322
|
+
mixBlendMode: "multiply",
|
|
13323
|
+
transformOrigin: "0% 50%",
|
|
13324
|
+
pointerEvents: "none"
|
|
13325
|
+
},
|
|
13326
|
+
initial: { width: 0, opacity: 0.9 },
|
|
13327
|
+
animate: { width: w, opacity: 0.9 },
|
|
13328
|
+
exit: { opacity: 0 },
|
|
13329
|
+
transition: { duration: action.draw_duration_ms / 1e3, ease: "easeOut" },
|
|
13330
|
+
"data-role": "highlight"
|
|
13331
|
+
}
|
|
13332
|
+
);
|
|
13333
|
+
}
|
|
13334
|
+
|
|
13335
|
+
// src/components/TutorMode/PulseOverlay.tsx
|
|
13336
|
+
var import_framer_motion5 = require("framer-motion");
|
|
13337
|
+
var import_jsx_runtime45 = require("react/jsx-runtime");
|
|
13338
|
+
var INTENSITY = {
|
|
13339
|
+
subtle: { scale: 1.02, border: "2px solid rgba(59,130,246,0.6)" },
|
|
13340
|
+
normal: { scale: 1.05, border: "3px solid rgba(59,130,246,0.8)" },
|
|
13341
|
+
strong: { scale: 1.1, border: "4px solid rgba(59,130,246,1.0)" }
|
|
13342
|
+
};
|
|
13343
|
+
function PulseOverlay({ bbox, action }) {
|
|
13344
|
+
const [x1, y1, x2, y2] = bbox;
|
|
13345
|
+
const { scale, border } = INTENSITY[action.intensity];
|
|
13346
|
+
const repeat = action.count === 1 ? 0 : action.count - 1;
|
|
13347
|
+
return /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
|
|
13348
|
+
import_framer_motion5.motion.div,
|
|
13349
|
+
{
|
|
13350
|
+
style: {
|
|
13351
|
+
position: "absolute",
|
|
13352
|
+
left: x1,
|
|
13353
|
+
top: y1,
|
|
13354
|
+
width: x2 - x1,
|
|
13355
|
+
height: y2 - y1,
|
|
13356
|
+
border,
|
|
13357
|
+
borderRadius: 8,
|
|
13358
|
+
pointerEvents: "none",
|
|
13359
|
+
boxSizing: "border-box"
|
|
13360
|
+
},
|
|
13361
|
+
animate: { scale: [1, scale, 1] },
|
|
13362
|
+
transition: {
|
|
13363
|
+
duration: 1.2,
|
|
13364
|
+
times: [0, 0.5, 1],
|
|
13365
|
+
ease: "easeInOut",
|
|
13366
|
+
repeat,
|
|
13367
|
+
repeatType: "loop"
|
|
13368
|
+
},
|
|
13369
|
+
exit: { opacity: 0 },
|
|
13370
|
+
"data-role": "pulse"
|
|
13371
|
+
}
|
|
13372
|
+
);
|
|
13373
|
+
}
|
|
13374
|
+
|
|
13375
|
+
// src/components/TutorMode/CalloutArrow.tsx
|
|
13376
|
+
var import_framer_motion6 = require("framer-motion");
|
|
13377
|
+
var import_jsx_runtime46 = require("react/jsx-runtime");
|
|
13378
|
+
function centerOf(b) {
|
|
13379
|
+
return { x: (b[0] + b[2]) / 2, y: (b[1] + b[3]) / 2 };
|
|
13380
|
+
}
|
|
13381
|
+
function arrowPath(fromBbox, toBbox, curve) {
|
|
13382
|
+
const a = centerOf(fromBbox);
|
|
13383
|
+
const b = centerOf(toBbox);
|
|
13384
|
+
if (curve === "straight") return `M ${a.x} ${a.y} L ${b.x} ${b.y}`;
|
|
13385
|
+
if (curve === "zigzag") {
|
|
13386
|
+
const mx = (a.x + b.x) / 2;
|
|
13387
|
+
return `M ${a.x} ${a.y} L ${mx} ${a.y} L ${mx} ${b.y} L ${b.x} ${b.y}`;
|
|
13388
|
+
}
|
|
13389
|
+
const dx = b.x - a.x;
|
|
13390
|
+
const dy = b.y - a.y;
|
|
13391
|
+
const cx = (a.x + b.x) / 2 - dy * 0.25;
|
|
13392
|
+
const cy = (a.y + b.y) / 2 + dx * 0.25;
|
|
13393
|
+
return `M ${a.x} ${a.y} Q ${cx} ${cy} ${b.x} ${b.y}`;
|
|
13394
|
+
}
|
|
13395
|
+
function CalloutArrow({ fromBbox, toBbox, action }) {
|
|
13396
|
+
const d = arrowPath(fromBbox, toBbox, action.curve);
|
|
13397
|
+
const label = action.label;
|
|
13398
|
+
const target = centerOf(toBbox);
|
|
13399
|
+
return /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)(
|
|
13400
|
+
"svg",
|
|
13401
|
+
{
|
|
13402
|
+
style: {
|
|
13403
|
+
position: "absolute",
|
|
13404
|
+
inset: 0,
|
|
13405
|
+
pointerEvents: "none",
|
|
13406
|
+
overflow: "visible"
|
|
13407
|
+
},
|
|
13408
|
+
"data-role": "callout",
|
|
13409
|
+
children: [
|
|
13410
|
+
/* @__PURE__ */ (0, import_jsx_runtime46.jsx)("defs", { children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
|
|
13411
|
+
"marker",
|
|
13412
|
+
{
|
|
13413
|
+
id: "arrowhead",
|
|
13414
|
+
viewBox: "0 0 10 10",
|
|
13415
|
+
refX: "8",
|
|
13416
|
+
refY: "5",
|
|
13417
|
+
markerWidth: "8",
|
|
13418
|
+
markerHeight: "8",
|
|
13419
|
+
orient: "auto",
|
|
13420
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("path", { d: "M 0 0 L 10 5 L 0 10 z", fill: "#3B82F6" })
|
|
13421
|
+
}
|
|
13422
|
+
) }),
|
|
13423
|
+
/* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
|
|
13424
|
+
import_framer_motion6.motion.path,
|
|
13425
|
+
{
|
|
13426
|
+
d,
|
|
13427
|
+
fill: "none",
|
|
13428
|
+
stroke: "#3B82F6",
|
|
13429
|
+
strokeWidth: 3,
|
|
13430
|
+
strokeLinecap: "round",
|
|
13431
|
+
markerEnd: "url(#arrowhead)",
|
|
13432
|
+
initial: { pathLength: 0, opacity: 0 },
|
|
13433
|
+
animate: { pathLength: 1, opacity: 1 },
|
|
13434
|
+
exit: { opacity: 0 },
|
|
13435
|
+
transition: { duration: 0.6, ease: "easeOut" }
|
|
13436
|
+
}
|
|
13437
|
+
),
|
|
13438
|
+
label ? /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)(
|
|
13439
|
+
import_framer_motion6.motion.g,
|
|
13440
|
+
{
|
|
13441
|
+
initial: { opacity: 0 },
|
|
13442
|
+
animate: { opacity: 1 },
|
|
13443
|
+
exit: { opacity: 0 },
|
|
13444
|
+
transition: { delay: 0.3, duration: 0.3 },
|
|
13445
|
+
children: [
|
|
13446
|
+
/* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
|
|
13447
|
+
"rect",
|
|
13448
|
+
{
|
|
13449
|
+
x: target.x - 4,
|
|
13450
|
+
y: target.y - 28,
|
|
13451
|
+
width: label.length * 9 + 12,
|
|
13452
|
+
height: 22,
|
|
13453
|
+
rx: 4,
|
|
13454
|
+
fill: "#1F2937"
|
|
13455
|
+
}
|
|
13456
|
+
),
|
|
13457
|
+
/* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
|
|
13458
|
+
"text",
|
|
13459
|
+
{
|
|
13460
|
+
x: target.x + 2,
|
|
13461
|
+
y: target.y - 12,
|
|
13462
|
+
fill: "white",
|
|
13463
|
+
fontSize: 14,
|
|
13464
|
+
fontFamily: "system-ui, sans-serif",
|
|
13465
|
+
children: label
|
|
13466
|
+
}
|
|
13467
|
+
)
|
|
13468
|
+
]
|
|
13469
|
+
}
|
|
13470
|
+
) : null
|
|
13471
|
+
]
|
|
13472
|
+
}
|
|
13473
|
+
);
|
|
13474
|
+
}
|
|
13475
|
+
|
|
13476
|
+
// src/components/TutorMode/GhostReference.tsx
|
|
13477
|
+
var import_framer_motion7 = require("framer-motion");
|
|
13478
|
+
var import_jsx_runtime47 = require("react/jsx-runtime");
|
|
13479
|
+
var POSITIONS = {
|
|
13480
|
+
"top-right": { top: 40, right: 40 },
|
|
13481
|
+
"top-left": { top: 40, left: 40 },
|
|
13482
|
+
"bottom-right": { bottom: 40, right: 40 },
|
|
13483
|
+
"bottom-left": { bottom: 40, left: 40 }
|
|
13484
|
+
};
|
|
13485
|
+
function GhostReference({
|
|
13486
|
+
page,
|
|
13487
|
+
sourceBbox,
|
|
13488
|
+
sourceBlockText,
|
|
13489
|
+
sourcePageNumber,
|
|
13490
|
+
action
|
|
13491
|
+
}) {
|
|
13492
|
+
const width = 360;
|
|
13493
|
+
const [x1, y1, x2, y2] = sourceBbox;
|
|
13494
|
+
return /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)(
|
|
13495
|
+
import_framer_motion7.motion.div,
|
|
13496
|
+
{
|
|
13497
|
+
initial: { opacity: 0, y: 20, scale: 0.95 },
|
|
13498
|
+
animate: { opacity: 1, y: 0, scale: 1 },
|
|
13499
|
+
exit: { opacity: 0, y: 20, scale: 0.95 },
|
|
13500
|
+
transition: { duration: 0.4, ease: "easeOut" },
|
|
13501
|
+
style: {
|
|
13502
|
+
position: "absolute",
|
|
13503
|
+
width,
|
|
13504
|
+
background: "#111",
|
|
13505
|
+
color: "white",
|
|
13506
|
+
borderRadius: 12,
|
|
13507
|
+
padding: 12,
|
|
13508
|
+
boxShadow: "0 10px 40px rgba(0,0,0,0.5)",
|
|
13509
|
+
pointerEvents: "none",
|
|
13510
|
+
fontFamily: "system-ui, sans-serif",
|
|
13511
|
+
fontSize: 13,
|
|
13512
|
+
...POSITIONS[action.position]
|
|
13513
|
+
},
|
|
13514
|
+
"data-role": "ghost-reference",
|
|
13515
|
+
children: [
|
|
13516
|
+
/* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("div", { style: { opacity: 0.7, fontSize: 11, marginBottom: 6 }, children: [
|
|
13517
|
+
"Page ",
|
|
13518
|
+
sourcePageNumber,
|
|
13519
|
+
" \u2014 ",
|
|
13520
|
+
action.target_block
|
|
13521
|
+
] }),
|
|
13522
|
+
/* @__PURE__ */ (0, import_jsx_runtime47.jsxs)(
|
|
13523
|
+
"svg",
|
|
13524
|
+
{
|
|
13525
|
+
width: width - 24,
|
|
13526
|
+
height: 160,
|
|
13527
|
+
viewBox: `0 0 ${page.width} ${page.height}`,
|
|
13528
|
+
style: { background: "#1F2937", borderRadius: 6, display: "block" },
|
|
13529
|
+
preserveAspectRatio: "xMidYMid meet",
|
|
13530
|
+
children: [
|
|
13531
|
+
/* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
|
|
13532
|
+
"rect",
|
|
13533
|
+
{
|
|
13534
|
+
x: 0,
|
|
13535
|
+
y: 0,
|
|
13536
|
+
width: page.width,
|
|
13537
|
+
height: page.height,
|
|
13538
|
+
fill: "#1F2937"
|
|
13539
|
+
}
|
|
13540
|
+
),
|
|
13541
|
+
/* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
|
|
13542
|
+
"rect",
|
|
13543
|
+
{
|
|
13544
|
+
x: x1,
|
|
13545
|
+
y: y1,
|
|
13546
|
+
width: x2 - x1,
|
|
13547
|
+
height: y2 - y1,
|
|
13548
|
+
fill: "rgba(250,204,21,0.45)",
|
|
13549
|
+
stroke: "#FBBF24",
|
|
13550
|
+
strokeWidth: 8
|
|
13551
|
+
}
|
|
13552
|
+
)
|
|
13553
|
+
]
|
|
13554
|
+
}
|
|
13555
|
+
),
|
|
13556
|
+
/* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
|
|
13557
|
+
"div",
|
|
13558
|
+
{
|
|
13559
|
+
style: {
|
|
13560
|
+
marginTop: 8,
|
|
13561
|
+
fontSize: 12,
|
|
13562
|
+
lineHeight: 1.4,
|
|
13563
|
+
opacity: 0.9
|
|
13564
|
+
},
|
|
13565
|
+
children: sourceBlockText ?? "(figure)"
|
|
13566
|
+
}
|
|
13567
|
+
)
|
|
13568
|
+
]
|
|
13569
|
+
}
|
|
13570
|
+
);
|
|
13571
|
+
}
|
|
13572
|
+
|
|
13573
|
+
// src/components/TutorMode/BoxOverlay.tsx
|
|
13574
|
+
var import_framer_motion8 = require("framer-motion");
|
|
13575
|
+
var import_jsx_runtime48 = require("react/jsx-runtime");
|
|
13576
|
+
function BoxOverlay({ bbox, action }) {
|
|
13577
|
+
const [x1, y1, x2, y2] = bbox;
|
|
13578
|
+
return /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
|
|
13579
|
+
import_framer_motion8.motion.div,
|
|
13580
|
+
{
|
|
13581
|
+
initial: { opacity: 0, scale: 0.97 },
|
|
13582
|
+
animate: { opacity: 1, scale: 1 },
|
|
13583
|
+
exit: { opacity: 0 },
|
|
13584
|
+
transition: { duration: 0.35, ease: "easeOut" },
|
|
13585
|
+
style: {
|
|
13586
|
+
position: "absolute",
|
|
13587
|
+
left: x1,
|
|
13588
|
+
top: y1,
|
|
13589
|
+
width: x2 - x1,
|
|
13590
|
+
height: y2 - y1,
|
|
13591
|
+
border: `${action.style === "dashed" ? "3px dashed" : "3px solid"} ${action.color}`,
|
|
13592
|
+
borderRadius: 6,
|
|
13593
|
+
pointerEvents: "none",
|
|
13594
|
+
boxSizing: "border-box"
|
|
13595
|
+
},
|
|
13596
|
+
"data-role": "box"
|
|
13597
|
+
}
|
|
13598
|
+
);
|
|
13599
|
+
}
|
|
13600
|
+
|
|
13601
|
+
// src/components/TutorMode/StickyLabel.tsx
|
|
13602
|
+
var import_framer_motion9 = require("framer-motion");
|
|
13603
|
+
var import_jsx_runtime49 = require("react/jsx-runtime");
|
|
13604
|
+
function position(bbox, where) {
|
|
13605
|
+
const [x1, y1, x2, y2] = bbox;
|
|
13606
|
+
const cx = (x1 + x2) / 2;
|
|
13607
|
+
const cy = (y1 + y2) / 2;
|
|
13608
|
+
const PAD = 16;
|
|
13609
|
+
switch (where) {
|
|
13610
|
+
case "top":
|
|
13611
|
+
return { left: cx, top: y1 - PAD, transform: "translate(-50%, -100%)" };
|
|
13612
|
+
case "bottom":
|
|
13613
|
+
return { left: cx, top: y2 + PAD, transform: "translate(-50%, 0)" };
|
|
13614
|
+
case "left":
|
|
13615
|
+
return { left: x1 - PAD, top: cy, transform: "translate(-100%, -50%)" };
|
|
13616
|
+
case "right":
|
|
13617
|
+
return { left: x2 + PAD, top: cy, transform: "translate(0, -50%)" };
|
|
13618
|
+
default:
|
|
13619
|
+
return { left: cx, top: y1, transform: "translate(-50%, -100%)" };
|
|
13620
|
+
}
|
|
13621
|
+
}
|
|
13622
|
+
function StickyLabel({ bbox, action }) {
|
|
13623
|
+
return /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
|
|
13624
|
+
import_framer_motion9.motion.div,
|
|
13625
|
+
{
|
|
13626
|
+
initial: { opacity: 0, scale: 0.9 },
|
|
13627
|
+
animate: { opacity: 1, scale: 1 },
|
|
13628
|
+
exit: { opacity: 0 },
|
|
13629
|
+
transition: { duration: 0.35, ease: "easeOut" },
|
|
13630
|
+
style: {
|
|
13631
|
+
position: "absolute",
|
|
13632
|
+
padding: "6px 10px",
|
|
13633
|
+
background: "#FEF3C7",
|
|
13634
|
+
color: "#78350F",
|
|
13635
|
+
borderRadius: 6,
|
|
13636
|
+
boxShadow: "0 3px 10px rgba(0,0,0,0.2)",
|
|
13637
|
+
fontSize: 14,
|
|
13638
|
+
fontFamily: "system-ui, sans-serif",
|
|
13639
|
+
maxWidth: 280,
|
|
13640
|
+
pointerEvents: "none",
|
|
13641
|
+
...position(bbox, action.position)
|
|
13642
|
+
},
|
|
13643
|
+
"data-role": "label",
|
|
13644
|
+
children: action.text
|
|
13645
|
+
}
|
|
13646
|
+
);
|
|
13647
|
+
}
|
|
13648
|
+
|
|
13649
|
+
// src/components/TutorMode/CinemaLayer.tsx
|
|
13650
|
+
var import_jsx_runtime50 = require("react/jsx-runtime");
|
|
13651
|
+
function blockBbox(index, block_id) {
|
|
13652
|
+
return index.blockById.get(block_id)?.block.bbox;
|
|
13653
|
+
}
|
|
13654
|
+
function CinemaLayer({
|
|
13655
|
+
page,
|
|
13656
|
+
index,
|
|
13657
|
+
overlays,
|
|
13658
|
+
scale
|
|
13659
|
+
}) {
|
|
13660
|
+
return /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
|
|
13661
|
+
"div",
|
|
13662
|
+
{
|
|
13663
|
+
"data-role": "cinema-layer",
|
|
13664
|
+
style: {
|
|
13665
|
+
position: "absolute",
|
|
13666
|
+
inset: 0,
|
|
13667
|
+
transformOrigin: "0 0",
|
|
13668
|
+
transform: `scale(${scale})`,
|
|
13669
|
+
width: page.page_dimensions.width,
|
|
13670
|
+
height: page.page_dimensions.height,
|
|
13671
|
+
pointerEvents: "none",
|
|
13672
|
+
// PDFPage renders internal layers at z-index 10/20/40/45/50
|
|
13673
|
+
// (canvas / text / highlight / focus / annotation). Without an
|
|
13674
|
+
// explicit z-index here, every tutor overlay stacks UNDER the
|
|
13675
|
+
// AnnotationLayer and becomes invisible. 100 puts us above all of
|
|
13676
|
+
// them while still letting the Exit button (z-index 60) remain
|
|
13677
|
+
// reachable because it sits OUTSIDE this stacking context.
|
|
13678
|
+
zIndex: 100
|
|
13679
|
+
},
|
|
13680
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(import_framer_motion10.AnimatePresence, { children: overlays.map((overlay) => {
|
|
13681
|
+
switch (overlay.kind) {
|
|
13682
|
+
case "spotlight": {
|
|
13683
|
+
const a = overlay.action;
|
|
13684
|
+
const b = blockBbox(index, a.target_block);
|
|
13685
|
+
if (!b) return null;
|
|
13686
|
+
return /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
|
|
13687
|
+
SpotlightMask,
|
|
13688
|
+
{
|
|
13689
|
+
page: page.page_dimensions,
|
|
13690
|
+
bbox: b,
|
|
13691
|
+
action: a
|
|
13692
|
+
},
|
|
13693
|
+
overlay.id
|
|
13694
|
+
);
|
|
13695
|
+
}
|
|
13696
|
+
case "underline": {
|
|
13697
|
+
const a = overlay.action;
|
|
13698
|
+
const b = blockBbox(index, a.target_block);
|
|
13699
|
+
if (!b) return null;
|
|
13700
|
+
return /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(AnimatedUnderline, { bbox: b, action: a }, overlay.id);
|
|
13701
|
+
}
|
|
13702
|
+
case "highlight": {
|
|
13703
|
+
const a = overlay.action;
|
|
13704
|
+
const b = blockBbox(index, a.target_block);
|
|
13705
|
+
if (!b) return null;
|
|
13706
|
+
return /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(AnimatedHighlight, { bbox: b, action: a }, overlay.id);
|
|
13707
|
+
}
|
|
13708
|
+
case "pulse": {
|
|
13709
|
+
const a = overlay.action;
|
|
13710
|
+
const b = blockBbox(index, a.target_block);
|
|
13711
|
+
if (!b) return null;
|
|
13712
|
+
return /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(PulseOverlay, { bbox: b, action: a }, overlay.id);
|
|
13713
|
+
}
|
|
13714
|
+
case "callout": {
|
|
13715
|
+
const a = overlay.action;
|
|
13716
|
+
const from = blockBbox(index, a.from_block);
|
|
13717
|
+
const to = blockBbox(index, a.to_block);
|
|
13718
|
+
if (!from || !to) return null;
|
|
13719
|
+
return /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
|
|
13720
|
+
CalloutArrow,
|
|
13721
|
+
{
|
|
13722
|
+
fromBbox: from,
|
|
13723
|
+
toBbox: to,
|
|
13724
|
+
action: a
|
|
13725
|
+
},
|
|
13726
|
+
overlay.id
|
|
13727
|
+
);
|
|
13728
|
+
}
|
|
13729
|
+
case "ghost_reference": {
|
|
13730
|
+
const a = overlay.action;
|
|
13731
|
+
const hit = index.blockById.get(a.target_block);
|
|
13732
|
+
if (!hit) return null;
|
|
13733
|
+
const targetPage = index.byPage.get(a.target_page);
|
|
13734
|
+
if (!targetPage) return null;
|
|
13735
|
+
return /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
|
|
13736
|
+
GhostReference,
|
|
13737
|
+
{
|
|
13738
|
+
page: targetPage.page_dimensions,
|
|
13739
|
+
sourceBbox: hit.block.bbox,
|
|
13740
|
+
sourceBlockText: hit.block.text,
|
|
13741
|
+
sourcePageNumber: hit.pageNumber,
|
|
13742
|
+
action: a
|
|
13743
|
+
},
|
|
13744
|
+
overlay.id
|
|
13745
|
+
);
|
|
13746
|
+
}
|
|
13747
|
+
case "box": {
|
|
13748
|
+
const a = overlay.action;
|
|
13749
|
+
const b = blockBbox(index, a.target_block);
|
|
13750
|
+
if (!b) return null;
|
|
13751
|
+
return /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(BoxOverlay, { bbox: b, action: a }, overlay.id);
|
|
13752
|
+
}
|
|
13753
|
+
case "label": {
|
|
13754
|
+
const a = overlay.action;
|
|
13755
|
+
const b = blockBbox(index, a.target_block);
|
|
13756
|
+
if (!b) return null;
|
|
13757
|
+
return /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(StickyLabel, { bbox: b, action: a }, overlay.id);
|
|
13758
|
+
}
|
|
13759
|
+
case "clear":
|
|
13760
|
+
case "camera":
|
|
13761
|
+
return null;
|
|
13762
|
+
}
|
|
13763
|
+
}) })
|
|
13764
|
+
}
|
|
13765
|
+
);
|
|
13766
|
+
}
|
|
13767
|
+
|
|
13768
|
+
// src/components/TutorMode/SubtitleBar.tsx
|
|
13769
|
+
var import_framer_motion11 = require("framer-motion");
|
|
13770
|
+
var import_jsx_runtime51 = require("react/jsx-runtime");
|
|
13771
|
+
function SubtitleBar({ text }) {
|
|
13772
|
+
return /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(import_framer_motion11.AnimatePresence, { children: text ? /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(
|
|
13773
|
+
import_framer_motion11.motion.div,
|
|
13774
|
+
{
|
|
13775
|
+
initial: { opacity: 0, y: 20 },
|
|
13776
|
+
animate: { opacity: 1, y: 0 },
|
|
13777
|
+
exit: { opacity: 0, y: 20 },
|
|
13778
|
+
transition: { duration: 0.3 },
|
|
13779
|
+
style: {
|
|
13780
|
+
position: "absolute",
|
|
13781
|
+
left: "50%",
|
|
13782
|
+
bottom: 32,
|
|
13783
|
+
transform: "translateX(-50%)",
|
|
13784
|
+
background: "rgba(0,0,0,0.75)",
|
|
13785
|
+
color: "white",
|
|
13786
|
+
padding: "10px 18px",
|
|
13787
|
+
borderRadius: 8,
|
|
13788
|
+
maxWidth: "80%",
|
|
13789
|
+
fontSize: 16,
|
|
13790
|
+
lineHeight: 1.4,
|
|
13791
|
+
fontFamily: "system-ui, sans-serif",
|
|
13792
|
+
pointerEvents: "none",
|
|
13793
|
+
zIndex: 50,
|
|
13794
|
+
textAlign: "center"
|
|
13795
|
+
},
|
|
13796
|
+
"data-role": "subtitle-bar",
|
|
13797
|
+
children: text
|
|
13798
|
+
},
|
|
13799
|
+
text
|
|
13800
|
+
) : null });
|
|
13801
|
+
}
|
|
13802
|
+
|
|
13803
|
+
// src/director/storyboard-engine.ts
|
|
13804
|
+
init_narration_store();
|
|
13805
|
+
init_camera_math();
|
|
13806
|
+
var DEFAULT_MIN_OVERLAY_MS = 3500;
|
|
13807
|
+
var StoryboardEngine = class {
|
|
13808
|
+
constructor(deps) {
|
|
13809
|
+
/**
|
|
13810
|
+
* Timers that schedule the START of a step (via `setTimeout(runStep, at_ms)`).
|
|
13811
|
+
* These are storyboard-scoped: when a new storyboard arrives, anything still
|
|
13812
|
+
* pending should be abandoned.
|
|
13813
|
+
*/
|
|
13814
|
+
this.pendingStepTimers = /* @__PURE__ */ new Set();
|
|
13815
|
+
/**
|
|
13816
|
+
* Timers that auto-REMOVE an already-placed overlay after its visible
|
|
13817
|
+
* duration. Keyed by overlay id so we can cancel one specifically. These are
|
|
13818
|
+
* NOT cancelled when a new storyboard starts — otherwise the still-visible
|
|
13819
|
+
* overlay from the previous beat would get stranded in the store forever
|
|
13820
|
+
* (the "stuck spotlight" bug).
|
|
13821
|
+
*/
|
|
13822
|
+
this.overlayRemovalTimers = /* @__PURE__ */ new Map();
|
|
13823
|
+
this.currentStoryboardId = 0;
|
|
13824
|
+
this.deps = deps;
|
|
13825
|
+
}
|
|
13826
|
+
/**
|
|
13827
|
+
* Execute a new storyboard. Cancels in-flight steps from the previous storyboard
|
|
13828
|
+
* and smoothly transitions the camera/overlays from the current state.
|
|
13829
|
+
*/
|
|
13830
|
+
execute(storyboard) {
|
|
13831
|
+
this.cancelPending();
|
|
13832
|
+
this.currentStoryboardId += 1;
|
|
13833
|
+
const storyboardId = this.currentStoryboardId;
|
|
13834
|
+
const { narrationStore } = this.deps;
|
|
13835
|
+
narrationStore.getState().setEngineStatus("transitioning");
|
|
13836
|
+
narrationStore.getState().setLastStoryboard(storyboard);
|
|
13837
|
+
let steps = [...storyboard.steps].sort((a, b) => a.at_ms - b.at_ms);
|
|
13838
|
+
const hasCamera = steps.some((s) => s.action.type === "camera");
|
|
13839
|
+
if (!hasCamera) {
|
|
13840
|
+
const focus = steps.find(
|
|
13841
|
+
(s) => s.action.type !== "clear" && "target_block" in s.action && s.action.target_block
|
|
13842
|
+
);
|
|
13843
|
+
if (focus && focus.action.type !== "clear" && "target_block" in focus.action) {
|
|
13844
|
+
steps = [
|
|
13845
|
+
{
|
|
13846
|
+
at_ms: 0,
|
|
13847
|
+
duration_ms: 700,
|
|
13848
|
+
action: {
|
|
13849
|
+
type: "camera",
|
|
13850
|
+
target_block: focus.action.target_block,
|
|
13851
|
+
scale: 1,
|
|
13852
|
+
padding: 60,
|
|
13853
|
+
easing: "ease-out"
|
|
13854
|
+
}
|
|
13855
|
+
},
|
|
13856
|
+
...steps
|
|
13857
|
+
];
|
|
13858
|
+
}
|
|
13859
|
+
}
|
|
13860
|
+
for (const step of steps) {
|
|
13861
|
+
const timer = setTimeout(() => {
|
|
13862
|
+
if (storyboardId !== this.currentStoryboardId) return;
|
|
13863
|
+
this.runStep(step);
|
|
13864
|
+
}, step.at_ms);
|
|
13865
|
+
this.pendingStepTimers.add(timer);
|
|
13866
|
+
}
|
|
13867
|
+
const markExecuting = setTimeout(() => {
|
|
13868
|
+
if (storyboardId !== this.currentStoryboardId) return;
|
|
13869
|
+
narrationStore.getState().setEngineStatus("executing");
|
|
13870
|
+
}, 0);
|
|
13871
|
+
this.pendingStepTimers.add(markExecuting);
|
|
13872
|
+
const last = steps[steps.length - 1];
|
|
13873
|
+
if (last) {
|
|
13874
|
+
const totalMs = last.at_ms + last.duration_ms;
|
|
13875
|
+
const markIdle = setTimeout(() => {
|
|
13876
|
+
if (storyboardId !== this.currentStoryboardId) return;
|
|
13877
|
+
narrationStore.getState().setEngineStatus("idle");
|
|
13878
|
+
}, totalMs + 50);
|
|
13879
|
+
this.pendingStepTimers.add(markIdle);
|
|
13880
|
+
}
|
|
13881
|
+
}
|
|
13882
|
+
/**
|
|
13883
|
+
* Abort pending STEP dispatches from the current storyboard. Overlay
|
|
13884
|
+
* removal timers are left alone so already-visible overlays still auto-
|
|
13885
|
+
* expire on their own schedule. To force-clear every overlay, call
|
|
13886
|
+
* `resetVisuals()` instead.
|
|
13887
|
+
*/
|
|
13888
|
+
cancelPending() {
|
|
13889
|
+
for (const t of this.pendingStepTimers) clearTimeout(t);
|
|
13890
|
+
this.pendingStepTimers.clear();
|
|
13891
|
+
this.deps.narrationStore.getState().setEngineStatus("idle");
|
|
13892
|
+
}
|
|
13893
|
+
/** Cancel every removal timer (used by resetVisuals only). */
|
|
13894
|
+
cancelAllRemovalTimers() {
|
|
13895
|
+
for (const t of this.overlayRemovalTimers.values()) clearTimeout(t);
|
|
13896
|
+
this.overlayRemovalTimers.clear();
|
|
13897
|
+
}
|
|
13898
|
+
/** Reset visuals: clear overlays, cancel every removal timer, fit camera. */
|
|
13899
|
+
resetVisuals() {
|
|
13900
|
+
this.cancelPending();
|
|
13901
|
+
this.cancelAllRemovalTimers();
|
|
13902
|
+
const { narrationStore, bboxIndex, getViewport } = this.deps;
|
|
13903
|
+
narrationStore.getState().clearOverlays();
|
|
13904
|
+
const viewport = getViewport();
|
|
13905
|
+
const currentPage = narrationStore.getState().currentPage;
|
|
13906
|
+
const pageDims = bboxIndex.byPage.get(currentPage);
|
|
13907
|
+
const fit = pageDims && viewport.width > 0 && viewport.height > 0 ? fitPageScale(pageDims.page_dimensions, viewport) * 0.95 : 1;
|
|
13908
|
+
narrationStore.getState().setCamera({ scale: fit, x: 0, y: 0, easing: "ease-in-out" });
|
|
13909
|
+
}
|
|
13910
|
+
/** Execute one step — dispatch to narrationStore. Returns true if applied. */
|
|
13911
|
+
runStep(step) {
|
|
13912
|
+
const action = step.action;
|
|
13913
|
+
const { narrationStore, bboxIndex } = this.deps;
|
|
13914
|
+
if ("target_block" in action && action.target_block) {
|
|
13915
|
+
if (!bboxIndex.blockById.has(action.target_block)) {
|
|
13916
|
+
narrationStore.getState().appendDebugEvent({
|
|
13917
|
+
kind: "llm-error",
|
|
13918
|
+
summary: `dropped ${action.type} step \u2192 unknown target_block "${action.target_block}"`,
|
|
13919
|
+
payload: { action, validIds: [...bboxIndex.blockById.keys()] }
|
|
13920
|
+
});
|
|
13921
|
+
return false;
|
|
13922
|
+
}
|
|
13923
|
+
}
|
|
13924
|
+
if ("from_block" in action && action.from_block) {
|
|
13925
|
+
if (!bboxIndex.blockById.has(action.from_block)) {
|
|
13926
|
+
narrationStore.getState().appendDebugEvent({
|
|
13927
|
+
kind: "llm-error",
|
|
13928
|
+
summary: `dropped ${action.type} step \u2192 unknown from_block "${action.from_block}"`,
|
|
13929
|
+
payload: { action }
|
|
13930
|
+
});
|
|
13931
|
+
return false;
|
|
13932
|
+
}
|
|
13933
|
+
}
|
|
13934
|
+
if ("to_block" in action && action.to_block) {
|
|
13935
|
+
if (!bboxIndex.blockById.has(action.to_block)) {
|
|
13936
|
+
narrationStore.getState().appendDebugEvent({
|
|
13937
|
+
kind: "llm-error",
|
|
13938
|
+
summary: `dropped ${action.type} step \u2192 unknown to_block "${action.to_block}"`,
|
|
13939
|
+
payload: { action }
|
|
13940
|
+
});
|
|
13941
|
+
return false;
|
|
13942
|
+
}
|
|
13943
|
+
}
|
|
13944
|
+
if (action.type === "camera") {
|
|
13945
|
+
this.applyCamera(action, step.duration_ms);
|
|
13946
|
+
return true;
|
|
13947
|
+
}
|
|
13948
|
+
if (action.type === "clear") {
|
|
13949
|
+
const targets = action.targets;
|
|
13950
|
+
if (targets === "all" || targets === "overlays") {
|
|
13951
|
+
narrationStore.getState().clearOverlays();
|
|
13952
|
+
} else if (targets === "spotlights") {
|
|
13953
|
+
narrationStore.getState().clearOverlays((o) => o.kind === "spotlight");
|
|
13954
|
+
} else if (Array.isArray(targets)) {
|
|
13955
|
+
const ids = new Set(targets);
|
|
13956
|
+
narrationStore.getState().clearOverlays((o) => ids.has(o.id));
|
|
13957
|
+
}
|
|
13958
|
+
return true;
|
|
13959
|
+
}
|
|
13960
|
+
const minMs = this.deps.minOverlayDurationMs ?? DEFAULT_MIN_OVERLAY_MS;
|
|
13961
|
+
const visibleMs = Math.max(step.duration_ms, minMs);
|
|
13962
|
+
const overlay = {
|
|
13963
|
+
id: makeOverlayId(action),
|
|
13964
|
+
kind: action.type,
|
|
13965
|
+
action,
|
|
13966
|
+
createdAt: Date.now(),
|
|
13967
|
+
expiresAt: Date.now() + visibleMs
|
|
13968
|
+
};
|
|
13969
|
+
narrationStore.getState().addOverlay(overlay);
|
|
13970
|
+
const timer = setTimeout(() => {
|
|
13971
|
+
narrationStore.getState().removeOverlay(overlay.id);
|
|
13972
|
+
this.overlayRemovalTimers.delete(overlay.id);
|
|
13973
|
+
}, visibleMs);
|
|
13974
|
+
this.overlayRemovalTimers.set(overlay.id, timer);
|
|
13975
|
+
return true;
|
|
13976
|
+
}
|
|
13977
|
+
applyCamera(action, durationMs) {
|
|
13978
|
+
const { narrationStore, bboxIndex, getViewport } = this.deps;
|
|
13979
|
+
const viewport = getViewport();
|
|
13980
|
+
let bbox = action.target_bbox;
|
|
13981
|
+
let pageDims = void 0;
|
|
13982
|
+
if (!bbox && action.target_block) {
|
|
13983
|
+
const hit = bboxIndex.blockById.get(action.target_block);
|
|
13984
|
+
if (!hit) return;
|
|
13985
|
+
bbox = hit.block.bbox;
|
|
13986
|
+
pageDims = bboxIndex.byPage.get(hit.pageNumber);
|
|
13987
|
+
} else if (bbox) {
|
|
13988
|
+
pageDims = bboxIndex.byPage.get(narrationStore.getState().currentPage);
|
|
13989
|
+
}
|
|
13990
|
+
if (!bbox || !pageDims) return;
|
|
13991
|
+
const fit = fitPageScale(pageDims.page_dimensions, viewport);
|
|
13992
|
+
const requested = Math.max(0.5, Math.min(3, action.scale ?? 1));
|
|
13993
|
+
const finalScale = fit * requested;
|
|
13994
|
+
const [x1, y1, x2, y2] = bbox;
|
|
13995
|
+
const blockCX = (x1 + x2) / 2;
|
|
13996
|
+
const blockCY = (y1 + y2) / 2;
|
|
13997
|
+
const pageCX = pageDims.page_dimensions.width / 2;
|
|
13998
|
+
const pageCY = pageDims.page_dimensions.height / 2;
|
|
13999
|
+
const x = (pageCX - blockCX) * finalScale;
|
|
14000
|
+
const y = (pageCY - blockCY) * finalScale;
|
|
14001
|
+
const camera = {
|
|
14002
|
+
scale: finalScale,
|
|
14003
|
+
x,
|
|
14004
|
+
y,
|
|
14005
|
+
easing: action.easing
|
|
14006
|
+
};
|
|
14007
|
+
narrationStore.getState().setCamera(camera);
|
|
14008
|
+
void durationMs;
|
|
14009
|
+
void computeCameraForBlock;
|
|
14010
|
+
}
|
|
14011
|
+
};
|
|
14012
|
+
|
|
14013
|
+
// src/director/storyboard-schema.ts
|
|
14014
|
+
var import_zod = require("zod");
|
|
14015
|
+
var BBoxCoordsSchema = import_zod.z.tuple([import_zod.z.number(), import_zod.z.number(), import_zod.z.number(), import_zod.z.number()]);
|
|
14016
|
+
var CameraSchema = import_zod.z.object({
|
|
14017
|
+
type: import_zod.z.literal("camera"),
|
|
14018
|
+
target_block: import_zod.z.string().optional(),
|
|
14019
|
+
target_bbox: BBoxCoordsSchema.optional(),
|
|
14020
|
+
scale: import_zod.z.number().min(0.5).max(4).default(1),
|
|
14021
|
+
padding: import_zod.z.number().min(0).max(400).default(80),
|
|
14022
|
+
easing: import_zod.z.enum(["linear", "ease-in", "ease-out", "ease-in-out"]).default("ease-in-out")
|
|
14023
|
+
}).refine((a) => !!a.target_block || !!a.target_bbox, {
|
|
14024
|
+
message: "camera requires target_block or target_bbox"
|
|
14025
|
+
});
|
|
14026
|
+
var SpotlightSchema = import_zod.z.object({
|
|
14027
|
+
type: import_zod.z.literal("spotlight"),
|
|
14028
|
+
target_block: import_zod.z.string(),
|
|
14029
|
+
dim_opacity: import_zod.z.number().min(0).max(1).default(0.65),
|
|
14030
|
+
feather_px: import_zod.z.number().min(0).max(200).default(40),
|
|
14031
|
+
shape: import_zod.z.enum(["rect", "rounded", "ellipse"]).default("rounded")
|
|
14032
|
+
});
|
|
14033
|
+
var UnderlineSchema = import_zod.z.object({
|
|
14034
|
+
type: import_zod.z.literal("underline"),
|
|
14035
|
+
target_block: import_zod.z.string(),
|
|
14036
|
+
color: import_zod.z.string().default("#FBBF24"),
|
|
14037
|
+
style: import_zod.z.enum(["straight", "sketch", "double", "wavy"]).default("sketch"),
|
|
14038
|
+
draw_duration_ms: import_zod.z.number().min(100).max(3e3).default(600)
|
|
14039
|
+
});
|
|
14040
|
+
var HighlightSchema = import_zod.z.object({
|
|
14041
|
+
type: import_zod.z.literal("highlight"),
|
|
14042
|
+
target_block: import_zod.z.string(),
|
|
14043
|
+
color: import_zod.z.string().default("rgba(250, 204, 21, 0.35)"),
|
|
14044
|
+
draw_duration_ms: import_zod.z.number().min(100).max(3e3).default(500)
|
|
14045
|
+
});
|
|
14046
|
+
var PulseSchema = import_zod.z.object({
|
|
14047
|
+
type: import_zod.z.literal("pulse"),
|
|
14048
|
+
target_block: import_zod.z.string(),
|
|
14049
|
+
count: import_zod.z.number().int().min(1).max(5).default(2),
|
|
14050
|
+
intensity: import_zod.z.enum(["subtle", "normal", "strong"]).default("normal")
|
|
14051
|
+
});
|
|
14052
|
+
var CalloutSchema = import_zod.z.object({
|
|
14053
|
+
type: import_zod.z.literal("callout"),
|
|
14054
|
+
from_block: import_zod.z.string(),
|
|
14055
|
+
to_block: import_zod.z.string(),
|
|
14056
|
+
label: import_zod.z.string().max(120).optional(),
|
|
14057
|
+
curve: import_zod.z.enum(["straight", "curved", "zigzag"]).default("curved")
|
|
14058
|
+
});
|
|
14059
|
+
var GhostReferenceSchema = import_zod.z.object({
|
|
14060
|
+
type: import_zod.z.literal("ghost_reference"),
|
|
14061
|
+
target_page: import_zod.z.number().int().min(1),
|
|
14062
|
+
target_block: import_zod.z.string(),
|
|
14063
|
+
position: import_zod.z.enum(["top-right", "top-left", "bottom-right", "bottom-left"]).default("top-right")
|
|
14064
|
+
});
|
|
14065
|
+
var BoxSchema = import_zod.z.object({
|
|
14066
|
+
type: import_zod.z.literal("box"),
|
|
14067
|
+
target_block: import_zod.z.string(),
|
|
14068
|
+
color: import_zod.z.string().default("#3B82F6"),
|
|
14069
|
+
style: import_zod.z.enum(["solid", "dashed"]).default("solid")
|
|
14070
|
+
});
|
|
14071
|
+
var LabelSchema = import_zod.z.object({
|
|
14072
|
+
type: import_zod.z.literal("label"),
|
|
14073
|
+
target_block: import_zod.z.string(),
|
|
14074
|
+
text: import_zod.z.string().min(1).max(120),
|
|
14075
|
+
position: import_zod.z.enum(["top", "bottom", "left", "right"]).default("top")
|
|
14076
|
+
});
|
|
14077
|
+
var ClearSchema = import_zod.z.object({
|
|
14078
|
+
type: import_zod.z.literal("clear"),
|
|
14079
|
+
targets: import_zod.z.union([import_zod.z.enum(["all", "spotlights", "overlays"]), import_zod.z.array(import_zod.z.string())]).default("overlays")
|
|
14080
|
+
});
|
|
14081
|
+
var StoryboardActionSchema = import_zod.z.union([
|
|
14082
|
+
CameraSchema,
|
|
14083
|
+
SpotlightSchema,
|
|
14084
|
+
UnderlineSchema,
|
|
14085
|
+
HighlightSchema,
|
|
14086
|
+
PulseSchema,
|
|
14087
|
+
CalloutSchema,
|
|
14088
|
+
GhostReferenceSchema,
|
|
14089
|
+
BoxSchema,
|
|
14090
|
+
LabelSchema,
|
|
14091
|
+
ClearSchema
|
|
14092
|
+
]);
|
|
14093
|
+
var StoryboardStepSchema = import_zod.z.object({
|
|
14094
|
+
at_ms: import_zod.z.number().min(0).max(5e3).default(0),
|
|
14095
|
+
duration_ms: import_zod.z.number().min(100).max(5e3).default(800),
|
|
14096
|
+
action: StoryboardActionSchema
|
|
14097
|
+
});
|
|
14098
|
+
var StoryboardSchema = import_zod.z.object({
|
|
14099
|
+
version: import_zod.z.literal(1),
|
|
14100
|
+
reasoning: import_zod.z.string().max(500).default(""),
|
|
14101
|
+
steps: import_zod.z.array(StoryboardStepSchema).min(1).max(4)
|
|
14102
|
+
});
|
|
14103
|
+
function storyboardJsonSchema(opts = {}) {
|
|
14104
|
+
const { validBlockIds, validCrossPageBlockIds } = opts;
|
|
14105
|
+
const blockIdSchema = validBlockIds && validBlockIds.length > 0 ? { type: ["string", "null"], enum: [...validBlockIds, null] } : { type: ["string", "null"] };
|
|
14106
|
+
const crossPageBlockIdSchema = validCrossPageBlockIds && validCrossPageBlockIds.length > 0 ? {
|
|
14107
|
+
type: ["string", "null"],
|
|
14108
|
+
enum: [...validCrossPageBlockIds, ...validBlockIds ?? [], null]
|
|
14109
|
+
} : blockIdSchema;
|
|
14110
|
+
const actionSchema = {
|
|
14111
|
+
type: "object",
|
|
14112
|
+
additionalProperties: false,
|
|
14113
|
+
required: [
|
|
14114
|
+
"type",
|
|
14115
|
+
"target_block",
|
|
14116
|
+
"target_bbox",
|
|
14117
|
+
"scale",
|
|
14118
|
+
"padding",
|
|
14119
|
+
"easing",
|
|
14120
|
+
"dim_opacity",
|
|
14121
|
+
"feather_px",
|
|
14122
|
+
"shape",
|
|
14123
|
+
"color",
|
|
14124
|
+
"style",
|
|
14125
|
+
"draw_duration_ms",
|
|
14126
|
+
"count",
|
|
14127
|
+
"intensity",
|
|
14128
|
+
"from_block",
|
|
14129
|
+
"to_block",
|
|
14130
|
+
"label",
|
|
14131
|
+
"curve",
|
|
14132
|
+
"target_page",
|
|
14133
|
+
"position",
|
|
14134
|
+
"text",
|
|
14135
|
+
"targets"
|
|
14136
|
+
],
|
|
14137
|
+
properties: {
|
|
14138
|
+
type: {
|
|
14139
|
+
type: "string",
|
|
14140
|
+
enum: [
|
|
14141
|
+
"camera",
|
|
14142
|
+
"spotlight",
|
|
14143
|
+
"underline",
|
|
14144
|
+
"highlight",
|
|
14145
|
+
"pulse",
|
|
14146
|
+
"callout",
|
|
14147
|
+
"ghost_reference",
|
|
14148
|
+
"box",
|
|
14149
|
+
"label",
|
|
14150
|
+
"clear"
|
|
14151
|
+
]
|
|
14152
|
+
},
|
|
14153
|
+
target_block: blockIdSchema,
|
|
14154
|
+
target_bbox: {
|
|
14155
|
+
type: ["array", "null"],
|
|
14156
|
+
items: { type: "number" },
|
|
14157
|
+
minItems: 4,
|
|
14158
|
+
maxItems: 4
|
|
14159
|
+
},
|
|
14160
|
+
scale: { type: ["number", "null"] },
|
|
14161
|
+
padding: { type: ["number", "null"] },
|
|
14162
|
+
easing: {
|
|
14163
|
+
type: ["string", "null"],
|
|
14164
|
+
enum: ["linear", "ease-in", "ease-out", "ease-in-out", null]
|
|
14165
|
+
},
|
|
14166
|
+
dim_opacity: { type: ["number", "null"] },
|
|
14167
|
+
feather_px: { type: ["number", "null"] },
|
|
14168
|
+
shape: {
|
|
14169
|
+
type: ["string", "null"],
|
|
14170
|
+
enum: ["rect", "rounded", "ellipse", null]
|
|
14171
|
+
},
|
|
14172
|
+
color: { type: ["string", "null"] },
|
|
14173
|
+
style: {
|
|
14174
|
+
type: ["string", "null"],
|
|
14175
|
+
enum: ["straight", "sketch", "double", "wavy", "solid", "dashed", null]
|
|
14176
|
+
},
|
|
14177
|
+
draw_duration_ms: { type: ["number", "null"] },
|
|
14178
|
+
count: { type: ["integer", "null"] },
|
|
14179
|
+
intensity: {
|
|
14180
|
+
type: ["string", "null"],
|
|
14181
|
+
enum: ["subtle", "normal", "strong", null]
|
|
14182
|
+
},
|
|
14183
|
+
from_block: blockIdSchema,
|
|
14184
|
+
to_block: crossPageBlockIdSchema,
|
|
14185
|
+
label: { type: ["string", "null"] },
|
|
14186
|
+
curve: {
|
|
14187
|
+
type: ["string", "null"],
|
|
14188
|
+
enum: ["straight", "curved", "zigzag", null]
|
|
14189
|
+
},
|
|
14190
|
+
target_page: { type: ["integer", "null"] },
|
|
14191
|
+
position: {
|
|
14192
|
+
type: ["string", "null"],
|
|
14193
|
+
enum: [
|
|
14194
|
+
"top",
|
|
14195
|
+
"bottom",
|
|
14196
|
+
"left",
|
|
14197
|
+
"right",
|
|
14198
|
+
"top-right",
|
|
14199
|
+
"top-left",
|
|
14200
|
+
"bottom-right",
|
|
14201
|
+
"bottom-left",
|
|
14202
|
+
null
|
|
14203
|
+
]
|
|
14204
|
+
},
|
|
14205
|
+
text: { type: ["string", "null"] },
|
|
14206
|
+
targets: {
|
|
14207
|
+
type: ["string", "null"],
|
|
14208
|
+
enum: ["all", "spotlights", "overlays", null]
|
|
14209
|
+
}
|
|
14210
|
+
}
|
|
14211
|
+
};
|
|
14212
|
+
return {
|
|
14213
|
+
type: "object",
|
|
14214
|
+
additionalProperties: false,
|
|
14215
|
+
required: ["version", "reasoning", "steps"],
|
|
14216
|
+
properties: {
|
|
14217
|
+
version: { type: "integer", enum: [1] },
|
|
14218
|
+
reasoning: { type: "string" },
|
|
14219
|
+
steps: {
|
|
14220
|
+
type: "array",
|
|
14221
|
+
minItems: 1,
|
|
14222
|
+
maxItems: 4,
|
|
14223
|
+
items: {
|
|
14224
|
+
type: "object",
|
|
14225
|
+
additionalProperties: false,
|
|
14226
|
+
required: ["at_ms", "duration_ms", "action"],
|
|
14227
|
+
properties: {
|
|
14228
|
+
at_ms: { type: "number" },
|
|
14229
|
+
duration_ms: { type: "number" },
|
|
14230
|
+
action: actionSchema
|
|
14231
|
+
}
|
|
14232
|
+
}
|
|
14233
|
+
}
|
|
14234
|
+
}
|
|
14235
|
+
};
|
|
14236
|
+
}
|
|
14237
|
+
|
|
14238
|
+
// src/director/prompts.ts
|
|
14239
|
+
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.
|
|
14240
|
+
|
|
14241
|
+
# Your primary task
|
|
14242
|
+
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.
|
|
14243
|
+
|
|
14244
|
+
Anchoring rules:
|
|
14245
|
+
- 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.
|
|
14246
|
+
- 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.
|
|
14247
|
+
- 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.
|
|
14248
|
+
- If multiple blocks match, pick the most specific one, or use a \`callout\` from one to the other.
|
|
14249
|
+
|
|
14250
|
+
# Output shape
|
|
14251
|
+
Output ONLY this JSON, nothing else:
|
|
14252
|
+
{
|
|
14253
|
+
"version": 1,
|
|
14254
|
+
"reasoning": "<which block(s) you picked, which intent you used, and why \u2014 name the block_id>",
|
|
14255
|
+
"steps": [ { "at_ms": <int>, "duration_ms": <int>, "action": <action> }, ... ]
|
|
14256
|
+
}
|
|
14257
|
+
|
|
14258
|
+
# Action shapes \u2014 ALL fields shown are REQUIRED per action type
|
|
14259
|
+
- camera: { "type":"camera", "target_block":"<id>", "scale":1.1, "padding":80, "easing":"ease-out" }
|
|
14260
|
+
- spotlight: { "type":"spotlight", "target_block":"<id>", "dim_opacity":0.65, "feather_px":40, "shape":"rounded" }
|
|
14261
|
+
- underline: { "type":"underline", "target_block":"<id>", "color":"#FBBF24", "style":"sketch", "draw_duration_ms":600 }
|
|
14262
|
+
- highlight: { "type":"highlight", "target_block":"<id>", "color":"rgba(250,204,21,0.35)", "draw_duration_ms":500 }
|
|
14263
|
+
- pulse: { "type":"pulse", "target_block":"<id>", "count":2, "intensity":"normal" }
|
|
14264
|
+
- callout: { "type":"callout", "from_block":"<id>", "to_block":"<id>", "label":"<text>", "curve":"curved" }
|
|
14265
|
+
- ghost_reference: { "type":"ghost_reference", "target_page":<int>, "target_block":"<id>", "position":"top-right" }
|
|
14266
|
+
- box: { "type":"box", "target_block":"<id>", "color":"#3B82F6", "style":"solid" }
|
|
14267
|
+
- label: { "type":"label", "target_block":"<id>", "text":"<text>", "position":"top" }
|
|
14268
|
+
- clear: { "type":"clear", "targets":"overlays" }
|
|
14269
|
+
|
|
14270
|
+
# When to use each action (match the effect to the narration's intent, not just the block type)
|
|
14271
|
+
- 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.
|
|
14272
|
+
- spotlight \u2014 when narration ISOLATES one idea, term, or sentence. Great for definitions, principles, and "the key insight is\u2026" moments.
|
|
14273
|
+
- 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.
|
|
14274
|
+
- highlight \u2014 when narration FLAGS a keyword inline without full focus. Cheap, fast, great for list items, definitions-in-context, callback references.
|
|
14275
|
+
- 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.
|
|
14276
|
+
- 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.
|
|
14277
|
+
- 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.
|
|
14278
|
+
- 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.
|
|
14279
|
+
- 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.
|
|
14280
|
+
- clear \u2014 rarely needed; the engine auto-expires overlays. Use only when you explicitly want to wipe prior state before a new beat.
|
|
14281
|
+
|
|
14282
|
+
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.
|
|
14283
|
+
|
|
14284
|
+
# Intent Taxonomy \u2014 canonical "recipes" you should use as your default vocabulary
|
|
14285
|
+
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.
|
|
14286
|
+
|
|
14287
|
+
## define \u2014 the narration introduces or defines a term
|
|
14288
|
+
Shape: spotlight the term + underline it + drop a label tag. No camera move if the block is already on-screen.
|
|
14289
|
+
{
|
|
14290
|
+
"version": 1,
|
|
14291
|
+
"reasoning": "define recipe: spotlighting and underlining the term, labeling as 'definition'",
|
|
14292
|
+
"steps": [
|
|
14293
|
+
{ "at_ms":0, "duration_ms":700, "action": { "type":"spotlight", "target_block":"p1_para0", "dim_opacity":0.6, "feather_px":40, "shape":"rounded" } },
|
|
14294
|
+
{ "at_ms":200, "duration_ms":800, "action": { "type":"underline", "target_block":"p1_para0", "color":"#FBBF24", "style":"sketch", "draw_duration_ms":700 } },
|
|
14295
|
+
{ "at_ms":900, "duration_ms":1200, "action": { "type":"label", "target_block":"p1_para0", "text":"definition", "position":"top" } }
|
|
14296
|
+
]
|
|
14297
|
+
}
|
|
14298
|
+
|
|
14299
|
+
## point_out \u2014 the narration directs the viewer's eye to a figure, diagram, or specific region
|
|
14300
|
+
Shape: gentle camera move + callout arrow from caption to figure + pulse the figure.
|
|
14301
|
+
{
|
|
14302
|
+
"version": 1,
|
|
14303
|
+
"reasoning": "point_out recipe: drawing attention from caption p1_cap1 to figure p1_fig0",
|
|
14304
|
+
"steps": [
|
|
14305
|
+
{ "at_ms":0, "duration_ms":600, "action": { "type":"camera", "target_block":"p1_fig0", "scale":1.3, "padding":80, "easing":"ease-out" } },
|
|
14306
|
+
{ "at_ms":400, "duration_ms":900, "action": { "type":"callout", "from_block":"p1_cap1", "to_block":"p1_fig0", "label":"see here", "curve":"curved" } },
|
|
14307
|
+
{ "at_ms":900, "duration_ms":1200, "action": { "type":"pulse", "target_block":"p1_fig0", "count":2, "intensity":"normal" } }
|
|
14308
|
+
]
|
|
14309
|
+
}
|
|
14310
|
+
|
|
14311
|
+
## compare \u2014 the narration contrasts two things on the page
|
|
14312
|
+
Shape: box A + box B + callout between them with a relational label.
|
|
14313
|
+
{
|
|
14314
|
+
"version": 1,
|
|
14315
|
+
"reasoning": "compare recipe: framing fibrous vs synovial joints",
|
|
14316
|
+
"steps": [
|
|
14317
|
+
{ "at_ms":0, "duration_ms":600, "action": { "type":"box", "target_block":"p1_list5", "color":"#3B82F6", "style":"solid" } },
|
|
14318
|
+
{ "at_ms":300, "duration_ms":600, "action": { "type":"box", "target_block":"p1_list12", "color":"#F472B6", "style":"solid" } },
|
|
14319
|
+
{ "at_ms":800, "duration_ms":1000, "action": { "type":"callout", "from_block":"p1_list5", "to_block":"p1_list12", "label":"vs", "curve":"curved" } }
|
|
14320
|
+
]
|
|
14321
|
+
}
|
|
14322
|
+
|
|
14323
|
+
## emphasize \u2014 the narration stresses a keyword, warning, or takeaway
|
|
14324
|
+
Shape: highlight + pulse. Fast, punchy, no camera.
|
|
14325
|
+
{
|
|
14326
|
+
"version": 1,
|
|
14327
|
+
"reasoning": "emphasize recipe: highlighting key keyword and pulsing for stress",
|
|
14328
|
+
"steps": [
|
|
14329
|
+
{ "at_ms":0, "duration_ms":500, "action": { "type":"highlight", "target_block":"p1_list0", "color":"rgba(250,204,21,0.35)", "draw_duration_ms":450 } },
|
|
14330
|
+
{ "at_ms":350, "duration_ms":800, "action": { "type":"pulse", "target_block":"p1_list0", "count":2, "intensity":"strong" } }
|
|
14331
|
+
]
|
|
14332
|
+
}
|
|
14333
|
+
|
|
14334
|
+
# Choreography rules
|
|
14335
|
+
- 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.
|
|
14336
|
+
- 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.
|
|
14337
|
+
- 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.
|
|
14338
|
+
- 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.
|
|
14339
|
+
- 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.
|
|
14340
|
+
- 2\u20134 steps is typical; single-step overlays (no camera) are PREFERRED when the target is already visible.
|
|
14341
|
+
- 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).
|
|
14342
|
+
- 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\`.
|
|
14343
|
+
- Output ONLY valid JSON. No markdown, no code fences, no commentary, no trailing whitespace inside property values.
|
|
14344
|
+
|
|
14345
|
+
# Forbidden outputs \u2014 these will be rejected:
|
|
14346
|
+
- A storyboard with only a camera step.
|
|
14347
|
+
- A camera step with scale < 0.5 or > 4.0.
|
|
14348
|
+
- target_block values not listed in "Page blocks" or "Cross-page figures index".
|
|
14349
|
+
- Tab characters, newlines, or explanatory text inside JSON string values.`;
|
|
14350
|
+
function truncate(text, max = 200) {
|
|
14351
|
+
if (!text) return "";
|
|
14352
|
+
if (text.length <= max) return text;
|
|
14353
|
+
const slice = text.slice(0, max);
|
|
14354
|
+
const last = slice.lastIndexOf(" ");
|
|
14355
|
+
return (last > 40 ? slice.slice(0, last) : slice) + "\u2026";
|
|
14356
|
+
}
|
|
14357
|
+
function buildUserPrompt(input) {
|
|
14358
|
+
const {
|
|
14359
|
+
chunk,
|
|
14360
|
+
pageNumber,
|
|
14361
|
+
page,
|
|
14362
|
+
index,
|
|
14363
|
+
history,
|
|
14364
|
+
camera,
|
|
14365
|
+
activeOverlays,
|
|
14366
|
+
maxSteps = 4
|
|
14367
|
+
} = input;
|
|
14368
|
+
const pageBlocks = page.blocks.map((b) => ({
|
|
14369
|
+
block_id: b.block_id,
|
|
14370
|
+
type: b.type,
|
|
14371
|
+
text: truncate(b.text, 200),
|
|
14372
|
+
bbox: b.bbox,
|
|
14373
|
+
default_action: b.default_action
|
|
14374
|
+
}));
|
|
14375
|
+
const xPageFigures = index.crossPageFigures.filter((f) => f.page !== pageNumber).slice(0, 20).map((f) => ({
|
|
14376
|
+
block_id: f.block_id,
|
|
14377
|
+
page: f.page,
|
|
14378
|
+
type: f.type,
|
|
14379
|
+
text: truncate(f.text, 200)
|
|
14380
|
+
}));
|
|
14381
|
+
const recent = history.slice(-3).map((h) => h.text);
|
|
14382
|
+
const overlaySummary = activeOverlays.map((o) => ({ id: o.id, kind: o.kind }));
|
|
14383
|
+
const blockIdList = pageBlocks.map((b) => b.block_id);
|
|
14384
|
+
return [
|
|
14385
|
+
`Current page: ${pageNumber}`,
|
|
14386
|
+
`Page blocks (${pageBlocks.length}) \u2014 you MUST pick target_block from this list:`,
|
|
14387
|
+
JSON.stringify(pageBlocks),
|
|
14388
|
+
"",
|
|
14389
|
+
`Valid block_ids for this page: ${JSON.stringify(blockIdList)}`,
|
|
14390
|
+
"",
|
|
14391
|
+
`Cross-page figures index: ${JSON.stringify(xPageFigures)}`,
|
|
14392
|
+
"",
|
|
14393
|
+
`Current chunk (what the tutor just said): ${JSON.stringify(chunk)}`,
|
|
14394
|
+
`Recent chunks: ${JSON.stringify(recent)}`,
|
|
14395
|
+
`Current camera: ${JSON.stringify(camera)}`,
|
|
14396
|
+
`Active overlays: ${JSON.stringify(overlaySummary)}`,
|
|
14397
|
+
"",
|
|
14398
|
+
`Max steps: ${maxSteps}`,
|
|
14399
|
+
`Output JSON storyboard. Every target_block MUST be one of the ids above.`
|
|
14400
|
+
].join("\n");
|
|
14401
|
+
}
|
|
14402
|
+
|
|
14403
|
+
// src/director/sse-parser.ts
|
|
14404
|
+
async function* parseSse(body) {
|
|
14405
|
+
const reader = body.getReader();
|
|
14406
|
+
const decoder = new TextDecoder();
|
|
14407
|
+
let buffer = "";
|
|
14408
|
+
try {
|
|
14409
|
+
while (true) {
|
|
14410
|
+
const { value, done } = await reader.read();
|
|
14411
|
+
if (done) break;
|
|
14412
|
+
buffer += decoder.decode(value, { stream: true });
|
|
14413
|
+
let idx;
|
|
14414
|
+
while ((idx = buffer.indexOf("\n")) !== -1) {
|
|
14415
|
+
const rawLine = buffer.slice(0, idx).trim();
|
|
14416
|
+
buffer = buffer.slice(idx + 1);
|
|
14417
|
+
if (!rawLine.startsWith("data:")) continue;
|
|
14418
|
+
const payload = rawLine.slice(5).trim();
|
|
14419
|
+
if (!payload || payload === "[DONE]") continue;
|
|
14420
|
+
try {
|
|
14421
|
+
yield JSON.parse(payload);
|
|
14422
|
+
} catch {
|
|
14423
|
+
}
|
|
14424
|
+
}
|
|
14425
|
+
}
|
|
14426
|
+
} finally {
|
|
14427
|
+
reader.releaseLock();
|
|
14428
|
+
}
|
|
14429
|
+
}
|
|
14430
|
+
function extractDelta(chunk) {
|
|
14431
|
+
if (!chunk || typeof chunk !== "object") return null;
|
|
14432
|
+
const choices = chunk.choices;
|
|
14433
|
+
if (!choices || !choices.length) return null;
|
|
14434
|
+
return choices[0].delta?.content ?? null;
|
|
14435
|
+
}
|
|
14436
|
+
|
|
14437
|
+
// src/director/llm-director.ts
|
|
14438
|
+
async function directStoryboard(config, input) {
|
|
14439
|
+
const {
|
|
14440
|
+
endpointUrl,
|
|
14441
|
+
model,
|
|
14442
|
+
authToken,
|
|
14443
|
+
extraBody,
|
|
14444
|
+
maxTokens = 1024,
|
|
14445
|
+
temperature = 0.3,
|
|
14446
|
+
useJsonSchema = true,
|
|
14447
|
+
stream = false
|
|
14448
|
+
} = config;
|
|
14449
|
+
const userContent = buildUserPrompt(input);
|
|
14450
|
+
const body = {
|
|
14451
|
+
model,
|
|
14452
|
+
stream,
|
|
14453
|
+
temperature,
|
|
14454
|
+
max_tokens: maxTokens,
|
|
14455
|
+
messages: [
|
|
14456
|
+
{ role: "system", content: SYSTEM_PROMPT },
|
|
14457
|
+
{ role: "user", content: userContent }
|
|
14458
|
+
],
|
|
14459
|
+
...extraBody ?? {}
|
|
14460
|
+
};
|
|
14461
|
+
if (useJsonSchema) {
|
|
14462
|
+
const validBlockIds = input.page.blocks.map((b) => b.block_id);
|
|
14463
|
+
const validCrossPageBlockIds = input.index.crossPageFigures.filter((f) => f.page !== input.pageNumber).map((f) => f.block_id);
|
|
14464
|
+
body.response_format = {
|
|
14465
|
+
type: "json_schema",
|
|
14466
|
+
json_schema: {
|
|
14467
|
+
name: "storyboard",
|
|
14468
|
+
strict: true,
|
|
14469
|
+
schema: storyboardJsonSchema({
|
|
14470
|
+
validBlockIds,
|
|
14471
|
+
validCrossPageBlockIds
|
|
14472
|
+
})
|
|
14473
|
+
}
|
|
14474
|
+
};
|
|
14475
|
+
}
|
|
14476
|
+
const headers = {
|
|
14477
|
+
"Content-Type": "application/json",
|
|
14478
|
+
Accept: stream ? "text/event-stream" : "application/json"
|
|
14479
|
+
};
|
|
14480
|
+
if (authToken) headers.Authorization = `Bearer ${authToken}`;
|
|
14481
|
+
const timeoutController = new AbortController();
|
|
14482
|
+
const timer = setTimeout(
|
|
14483
|
+
() => timeoutController.abort(),
|
|
14484
|
+
input.timeoutMs ?? 2500
|
|
14485
|
+
);
|
|
14486
|
+
const signal = mergeSignals(input.signal, timeoutController.signal);
|
|
14487
|
+
try {
|
|
14488
|
+
const response = await fetch(endpointUrl, {
|
|
14489
|
+
method: "POST",
|
|
14490
|
+
headers,
|
|
14491
|
+
body: JSON.stringify(body),
|
|
14492
|
+
signal
|
|
14493
|
+
});
|
|
14494
|
+
if (!response.ok || !response.body) {
|
|
14495
|
+
return {
|
|
14496
|
+
storyboard: null,
|
|
14497
|
+
raw: "",
|
|
14498
|
+
error: `HTTP ${response.status}`
|
|
14499
|
+
};
|
|
14500
|
+
}
|
|
14501
|
+
let raw = "";
|
|
14502
|
+
if (stream && response.body) {
|
|
14503
|
+
for await (const chunk of parseSse(response.body)) {
|
|
14504
|
+
const delta = extractDelta(chunk);
|
|
14505
|
+
if (delta) raw += delta;
|
|
14506
|
+
}
|
|
14507
|
+
} else {
|
|
14508
|
+
const json = await response.json();
|
|
14509
|
+
raw = json.choices?.[0]?.message?.content ?? "";
|
|
14510
|
+
}
|
|
14511
|
+
const stripped = collapseWhitespaceRuns(stripCodeFences(raw).trim());
|
|
14512
|
+
let parsed;
|
|
14513
|
+
try {
|
|
14514
|
+
parsed = JSON.parse(stripped);
|
|
14515
|
+
} catch (e) {
|
|
14516
|
+
return {
|
|
14517
|
+
storyboard: null,
|
|
14518
|
+
raw,
|
|
14519
|
+
error: `parse error: ${e.message}`
|
|
14520
|
+
};
|
|
14521
|
+
}
|
|
14522
|
+
const cleaned = clampNumericRanges(stripNullsDeep(parsed));
|
|
14523
|
+
const validation = StoryboardSchema.safeParse(cleaned);
|
|
14524
|
+
if (validation.success) {
|
|
14525
|
+
return {
|
|
14526
|
+
storyboard: enforceOverlayPresence(validation.data),
|
|
14527
|
+
raw
|
|
14528
|
+
};
|
|
14529
|
+
}
|
|
14530
|
+
const salvaged = salvageStoryboard(cleaned);
|
|
14531
|
+
if (salvaged) {
|
|
14532
|
+
return { storyboard: enforceOverlayPresence(salvaged), raw };
|
|
14533
|
+
}
|
|
14534
|
+
return {
|
|
14535
|
+
storyboard: null,
|
|
14536
|
+
raw,
|
|
14537
|
+
error: `validation failed: ${validation.error.message}`
|
|
14538
|
+
};
|
|
14539
|
+
} catch (e) {
|
|
14540
|
+
const name = e.name;
|
|
14541
|
+
const msg = name === "AbortError" ? "aborted" : e.message;
|
|
14542
|
+
return { storyboard: null, raw: "", error: msg };
|
|
14543
|
+
} finally {
|
|
14544
|
+
clearTimeout(timer);
|
|
14545
|
+
}
|
|
14546
|
+
}
|
|
14547
|
+
function stripCodeFences(s) {
|
|
14548
|
+
const m = s.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
14549
|
+
return m ? m[1] : s;
|
|
14550
|
+
}
|
|
14551
|
+
function collapseWhitespaceRuns(src) {
|
|
14552
|
+
let out = "";
|
|
14553
|
+
let inString = false;
|
|
14554
|
+
let escape = false;
|
|
14555
|
+
let run = 0;
|
|
14556
|
+
for (let i = 0; i < src.length; i++) {
|
|
14557
|
+
const c = src[i];
|
|
14558
|
+
if (inString) {
|
|
14559
|
+
out += c;
|
|
14560
|
+
if (escape) {
|
|
14561
|
+
escape = false;
|
|
14562
|
+
} else if (c === "\\") {
|
|
14563
|
+
escape = true;
|
|
14564
|
+
} else if (c === '"') {
|
|
14565
|
+
inString = false;
|
|
14566
|
+
}
|
|
14567
|
+
continue;
|
|
14568
|
+
}
|
|
14569
|
+
if (c === '"') {
|
|
14570
|
+
out += c;
|
|
14571
|
+
inString = true;
|
|
14572
|
+
run = 0;
|
|
14573
|
+
continue;
|
|
14574
|
+
}
|
|
14575
|
+
if (c === " " || c === " " || c === "\n" || c === "\r") {
|
|
14576
|
+
run++;
|
|
14577
|
+
if (run <= 1) out += " ";
|
|
14578
|
+
continue;
|
|
14579
|
+
}
|
|
14580
|
+
run = 0;
|
|
14581
|
+
out += c;
|
|
14582
|
+
}
|
|
14583
|
+
return out;
|
|
14584
|
+
}
|
|
14585
|
+
function clampNumericRanges(input) {
|
|
14586
|
+
if (input === null || input === void 0) return input;
|
|
14587
|
+
if (Array.isArray(input)) return input.map(clampNumericRanges);
|
|
14588
|
+
if (typeof input !== "object") return input;
|
|
14589
|
+
const obj = input;
|
|
14590
|
+
const out = {};
|
|
14591
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
14592
|
+
out[k] = clampNumericRanges(v);
|
|
14593
|
+
}
|
|
14594
|
+
const type = typeof out.type === "string" ? out.type : void 0;
|
|
14595
|
+
if (type === "camera") {
|
|
14596
|
+
if (typeof out.scale === "number") out.scale = clamp(out.scale, 0.5, 4);
|
|
14597
|
+
if (typeof out.padding === "number") {
|
|
14598
|
+
out.padding = clamp(out.padding, 0, 400);
|
|
14599
|
+
}
|
|
14600
|
+
}
|
|
14601
|
+
if (typeof out.dim_opacity === "number") {
|
|
14602
|
+
out.dim_opacity = clamp(out.dim_opacity, 0, 1);
|
|
14603
|
+
}
|
|
14604
|
+
if (typeof out.feather_px === "number") {
|
|
14605
|
+
out.feather_px = clamp(out.feather_px, 0, 200);
|
|
14606
|
+
}
|
|
14607
|
+
if (typeof out.draw_duration_ms === "number") {
|
|
14608
|
+
out.draw_duration_ms = clamp(out.draw_duration_ms, 100, 3e3);
|
|
14609
|
+
}
|
|
14610
|
+
if (typeof out.count === "number") {
|
|
14611
|
+
out.count = Math.round(clamp(out.count, 1, 5));
|
|
14612
|
+
}
|
|
14613
|
+
if (typeof out.at_ms === "number") {
|
|
14614
|
+
out.at_ms = clamp(out.at_ms, 0, 5e3);
|
|
14615
|
+
}
|
|
14616
|
+
if (typeof out.duration_ms === "number" && type === void 0) {
|
|
14617
|
+
out.duration_ms = clamp(out.duration_ms, 100, 5e3);
|
|
14618
|
+
}
|
|
14619
|
+
return out;
|
|
14620
|
+
}
|
|
14621
|
+
function clamp(v, lo, hi) {
|
|
14622
|
+
return Math.min(hi, Math.max(lo, v));
|
|
14623
|
+
}
|
|
14624
|
+
function enforceOverlayPresence(sb) {
|
|
14625
|
+
if (sb.steps.length === 0) return sb;
|
|
14626
|
+
const hasOverlay = sb.steps.some(
|
|
14627
|
+
(s) => s.action.type !== "camera" && s.action.type !== "clear"
|
|
14628
|
+
);
|
|
14629
|
+
if (hasOverlay) return sb;
|
|
14630
|
+
const cameraStep = sb.steps.find((s) => s.action.type === "camera");
|
|
14631
|
+
if (!cameraStep || cameraStep.action.type !== "camera") return sb;
|
|
14632
|
+
const target = cameraStep.action.target_block;
|
|
14633
|
+
if (!target) return sb;
|
|
14634
|
+
return {
|
|
14635
|
+
...sb,
|
|
14636
|
+
reasoning: `${sb.reasoning} [auto-appended pulse: camera-only storyboards are forbidden]`,
|
|
14637
|
+
steps: [
|
|
14638
|
+
...sb.steps,
|
|
14639
|
+
{
|
|
14640
|
+
at_ms: Math.min(4800, (cameraStep.at_ms ?? 0) + 200),
|
|
14641
|
+
duration_ms: 900,
|
|
14642
|
+
action: {
|
|
14643
|
+
type: "pulse",
|
|
14644
|
+
target_block: target,
|
|
14645
|
+
count: 2,
|
|
14646
|
+
intensity: "normal"
|
|
14647
|
+
}
|
|
14648
|
+
}
|
|
14649
|
+
]
|
|
14650
|
+
};
|
|
14651
|
+
}
|
|
14652
|
+
function stripNullsDeep(input) {
|
|
14653
|
+
if (input === null) return void 0;
|
|
14654
|
+
if (Array.isArray(input)) {
|
|
14655
|
+
return input.map(stripNullsDeep).filter((v) => v !== void 0);
|
|
14656
|
+
}
|
|
14657
|
+
if (input && typeof input === "object") {
|
|
14658
|
+
const out = {};
|
|
14659
|
+
for (const [k, v] of Object.entries(input)) {
|
|
14660
|
+
const cleaned = stripNullsDeep(v);
|
|
14661
|
+
if (cleaned !== void 0) out[k] = cleaned;
|
|
14662
|
+
}
|
|
14663
|
+
return out;
|
|
14664
|
+
}
|
|
14665
|
+
return input;
|
|
14666
|
+
}
|
|
14667
|
+
function salvageStoryboard(parsed) {
|
|
14668
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
14669
|
+
const obj = parsed;
|
|
14670
|
+
if (!Array.isArray(obj.steps)) return null;
|
|
14671
|
+
const goodSteps = [];
|
|
14672
|
+
for (const step of obj.steps) {
|
|
14673
|
+
const r = StoryboardStepSchema.safeParse(step);
|
|
14674
|
+
if (r.success) goodSteps.push(r.data);
|
|
14675
|
+
if (goodSteps.length >= 4) break;
|
|
14676
|
+
}
|
|
14677
|
+
if (goodSteps.length === 0) return null;
|
|
14678
|
+
return {
|
|
14679
|
+
version: 1,
|
|
14680
|
+
reasoning: typeof obj.reasoning === "string" ? obj.reasoning + " (salvaged)" : "salvaged",
|
|
14681
|
+
steps: goodSteps
|
|
14682
|
+
};
|
|
14683
|
+
}
|
|
14684
|
+
function mergeSignals(a, b) {
|
|
14685
|
+
if (!a) return b;
|
|
14686
|
+
if (!b) return a;
|
|
14687
|
+
const ctrl = new AbortController();
|
|
14688
|
+
const onAbort = () => ctrl.abort();
|
|
14689
|
+
a.addEventListener("abort", onAbort);
|
|
14690
|
+
b.addEventListener("abort", onAbort);
|
|
14691
|
+
return ctrl.signal;
|
|
14692
|
+
}
|
|
14693
|
+
|
|
14694
|
+
// src/director/embedding-fallback.ts
|
|
14695
|
+
function cosineSimilarity(a, b) {
|
|
14696
|
+
let dot = 0;
|
|
14697
|
+
let na = 0;
|
|
14698
|
+
let nb = 0;
|
|
14699
|
+
const n = Math.min(a.length, b.length);
|
|
14700
|
+
for (let i = 0; i < n; i++) {
|
|
14701
|
+
dot += a[i] * b[i];
|
|
14702
|
+
na += a[i] * a[i];
|
|
14703
|
+
nb += b[i] * b[i];
|
|
14704
|
+
}
|
|
14705
|
+
const denom = Math.sqrt(na) * Math.sqrt(nb);
|
|
14706
|
+
return denom === 0 ? 0 : dot / denom;
|
|
14707
|
+
}
|
|
14708
|
+
async function matchChunkToBlock(chunk, page, provider) {
|
|
14709
|
+
const textBlocks = page.blocks.filter(
|
|
14710
|
+
(b) => typeof b.text === "string" && b.text.trim().length > 0
|
|
14711
|
+
);
|
|
14712
|
+
if (textBlocks.length === 0) return null;
|
|
14713
|
+
const inputs = [chunk, ...textBlocks.map((b) => b.text)];
|
|
14714
|
+
const embeds = await provider.embed(inputs);
|
|
14715
|
+
if (embeds.length < 2) return null;
|
|
14716
|
+
const chunkEmbed = embeds[0];
|
|
14717
|
+
let best = null;
|
|
14718
|
+
for (let i = 0; i < textBlocks.length; i++) {
|
|
14719
|
+
const score = cosineSimilarity(chunkEmbed, embeds[i + 1]);
|
|
14720
|
+
if (!best || score > best.score) best = { block: textBlocks[i], score };
|
|
14721
|
+
}
|
|
14722
|
+
return best;
|
|
14723
|
+
}
|
|
14724
|
+
function nearestFigureOnPage(caption, page) {
|
|
14725
|
+
if (!page) return null;
|
|
14726
|
+
const [cx1, cy1, cx2, cy2] = caption.bbox;
|
|
14727
|
+
const ccx = (cx1 + cx2) / 2;
|
|
14728
|
+
const ccy = (cy1 + cy2) / 2;
|
|
14729
|
+
let best = null;
|
|
14730
|
+
for (const b of page.blocks) {
|
|
14731
|
+
if (b.block_id === caption.block_id) continue;
|
|
14732
|
+
if (b.type !== "figure" && b.type !== "figure_region") continue;
|
|
14733
|
+
const [x1, y1, x2, y2] = b.bbox;
|
|
14734
|
+
const fx = (x1 + x2) / 2;
|
|
14735
|
+
const fy = (y1 + y2) / 2;
|
|
14736
|
+
const dist = Math.hypot(fx - ccx, fy - ccy);
|
|
14737
|
+
if (!best || dist < best.dist) best = { block: b, dist };
|
|
14738
|
+
}
|
|
14739
|
+
return best?.block ?? null;
|
|
14740
|
+
}
|
|
14741
|
+
function truncateLabel(text, max) {
|
|
14742
|
+
if (!text) return "";
|
|
14743
|
+
const clean = text.replace(/\s+/g, " ").trim();
|
|
14744
|
+
if (clean.length <= max) return clean;
|
|
14745
|
+
return clean.slice(0, max - 1) + "\u2026";
|
|
14746
|
+
}
|
|
14747
|
+
function storyboardFromMatch(match, page) {
|
|
14748
|
+
if (!match) {
|
|
14749
|
+
return {
|
|
14750
|
+
version: 1,
|
|
14751
|
+
reasoning: "fallback: no match \u2014 clearing overlays",
|
|
14752
|
+
steps: [
|
|
14753
|
+
{
|
|
14754
|
+
at_ms: 0,
|
|
14755
|
+
duration_ms: 800,
|
|
14756
|
+
action: { type: "clear", targets: "overlays" }
|
|
14757
|
+
}
|
|
14758
|
+
]
|
|
14759
|
+
};
|
|
14760
|
+
}
|
|
14761
|
+
const { block } = match;
|
|
14762
|
+
const id = block.block_id;
|
|
14763
|
+
const reason = `fallback (block.type=${block.type}): matched ${id} (${match.score.toFixed(2)})`;
|
|
14764
|
+
switch (block.type) {
|
|
14765
|
+
case "heading": {
|
|
14766
|
+
return {
|
|
14767
|
+
version: 1,
|
|
14768
|
+
reasoning: reason,
|
|
14769
|
+
steps: [
|
|
14770
|
+
{
|
|
14771
|
+
at_ms: 0,
|
|
14772
|
+
duration_ms: 700,
|
|
14773
|
+
action: {
|
|
14774
|
+
type: "spotlight",
|
|
14775
|
+
target_block: id,
|
|
14776
|
+
dim_opacity: 0.6,
|
|
14777
|
+
feather_px: 40,
|
|
14778
|
+
shape: "rounded"
|
|
14779
|
+
}
|
|
14780
|
+
},
|
|
14781
|
+
{
|
|
14782
|
+
at_ms: 300,
|
|
14783
|
+
duration_ms: 1200,
|
|
14784
|
+
action: {
|
|
14785
|
+
type: "label",
|
|
14786
|
+
target_block: id,
|
|
14787
|
+
text: truncateLabel(block.text, 32) || "section",
|
|
14788
|
+
position: "top"
|
|
14789
|
+
}
|
|
14790
|
+
}
|
|
14791
|
+
]
|
|
14792
|
+
};
|
|
14793
|
+
}
|
|
14794
|
+
case "paragraph": {
|
|
14795
|
+
return {
|
|
14796
|
+
version: 1,
|
|
14797
|
+
reasoning: reason,
|
|
14798
|
+
steps: [
|
|
14799
|
+
{
|
|
14800
|
+
at_ms: 0,
|
|
14801
|
+
duration_ms: 600,
|
|
14802
|
+
action: {
|
|
14803
|
+
type: "camera",
|
|
14804
|
+
target_block: id,
|
|
14805
|
+
scale: 1.1,
|
|
14806
|
+
padding: 80,
|
|
14807
|
+
easing: "ease-out"
|
|
14808
|
+
}
|
|
14809
|
+
},
|
|
14810
|
+
{
|
|
14811
|
+
at_ms: 300,
|
|
14812
|
+
duration_ms: 900,
|
|
14813
|
+
action: {
|
|
14814
|
+
type: "underline",
|
|
14815
|
+
target_block: id,
|
|
14816
|
+
color: "#FBBF24",
|
|
14817
|
+
style: "sketch",
|
|
14818
|
+
draw_duration_ms: 800
|
|
14819
|
+
}
|
|
14820
|
+
}
|
|
14821
|
+
]
|
|
14822
|
+
};
|
|
14823
|
+
}
|
|
14824
|
+
case "list_item":
|
|
14825
|
+
case "mcq_option": {
|
|
14826
|
+
return {
|
|
14827
|
+
version: 1,
|
|
14828
|
+
reasoning: reason,
|
|
14829
|
+
steps: [
|
|
14830
|
+
{
|
|
14831
|
+
at_ms: 0,
|
|
14832
|
+
duration_ms: 500,
|
|
14833
|
+
action: {
|
|
14834
|
+
type: "highlight",
|
|
14835
|
+
target_block: id,
|
|
14836
|
+
color: "rgba(250, 204, 21, 0.35)",
|
|
14837
|
+
draw_duration_ms: 450
|
|
14838
|
+
}
|
|
14839
|
+
}
|
|
14840
|
+
]
|
|
14841
|
+
};
|
|
14842
|
+
}
|
|
14843
|
+
case "caption": {
|
|
14844
|
+
const figure = nearestFigureOnPage(block, page);
|
|
14845
|
+
if (figure) {
|
|
14846
|
+
return {
|
|
14847
|
+
version: 1,
|
|
14848
|
+
reasoning: `${reason}; caption \u2192 figure ${figure.block_id}`,
|
|
14849
|
+
steps: [
|
|
14850
|
+
{
|
|
14851
|
+
at_ms: 0,
|
|
14852
|
+
duration_ms: 900,
|
|
14853
|
+
action: {
|
|
14854
|
+
type: "callout",
|
|
14855
|
+
from_block: id,
|
|
14856
|
+
to_block: figure.block_id,
|
|
14857
|
+
label: "see",
|
|
14858
|
+
curve: "curved"
|
|
14859
|
+
}
|
|
14860
|
+
},
|
|
14861
|
+
{
|
|
14862
|
+
at_ms: 600,
|
|
14863
|
+
duration_ms: 1e3,
|
|
14864
|
+
action: {
|
|
14865
|
+
type: "pulse",
|
|
14866
|
+
target_block: figure.block_id,
|
|
14867
|
+
count: 2,
|
|
14868
|
+
intensity: "normal"
|
|
14869
|
+
}
|
|
14870
|
+
}
|
|
14871
|
+
]
|
|
14872
|
+
};
|
|
14873
|
+
}
|
|
14874
|
+
return {
|
|
14875
|
+
version: 1,
|
|
14876
|
+
reasoning: `${reason}; no figure on page, underlining caption`,
|
|
14877
|
+
steps: [
|
|
14878
|
+
{
|
|
14879
|
+
at_ms: 0,
|
|
14880
|
+
duration_ms: 800,
|
|
14881
|
+
action: {
|
|
14882
|
+
type: "underline",
|
|
14883
|
+
target_block: id,
|
|
14884
|
+
color: "#FBBF24",
|
|
14885
|
+
style: "sketch",
|
|
14886
|
+
draw_duration_ms: 700
|
|
14887
|
+
}
|
|
14888
|
+
}
|
|
14889
|
+
]
|
|
14890
|
+
};
|
|
14891
|
+
}
|
|
14892
|
+
case "figure": {
|
|
14893
|
+
return {
|
|
14894
|
+
version: 1,
|
|
14895
|
+
reasoning: reason,
|
|
14896
|
+
steps: [
|
|
14897
|
+
{
|
|
14898
|
+
at_ms: 0,
|
|
14899
|
+
duration_ms: 900,
|
|
14900
|
+
action: {
|
|
14901
|
+
type: "pulse",
|
|
14902
|
+
target_block: id,
|
|
14903
|
+
count: 2,
|
|
14904
|
+
intensity: "strong"
|
|
14905
|
+
}
|
|
14906
|
+
},
|
|
14907
|
+
{
|
|
14908
|
+
at_ms: 400,
|
|
14909
|
+
duration_ms: 1200,
|
|
14910
|
+
action: {
|
|
14911
|
+
type: "box",
|
|
14912
|
+
target_block: id,
|
|
14913
|
+
color: "#3B82F6",
|
|
14914
|
+
style: "solid"
|
|
14915
|
+
}
|
|
14916
|
+
}
|
|
14917
|
+
]
|
|
14918
|
+
};
|
|
14919
|
+
}
|
|
14920
|
+
case "figure_region": {
|
|
14921
|
+
return {
|
|
14922
|
+
version: 1,
|
|
14923
|
+
reasoning: reason,
|
|
14924
|
+
steps: [
|
|
14925
|
+
{
|
|
14926
|
+
at_ms: 0,
|
|
14927
|
+
duration_ms: 900,
|
|
14928
|
+
action: {
|
|
14929
|
+
type: "pulse",
|
|
14930
|
+
target_block: id,
|
|
14931
|
+
count: 2,
|
|
14932
|
+
intensity: "normal"
|
|
14933
|
+
}
|
|
14934
|
+
}
|
|
14935
|
+
]
|
|
14936
|
+
};
|
|
14937
|
+
}
|
|
14938
|
+
case "table": {
|
|
14939
|
+
return {
|
|
14940
|
+
version: 1,
|
|
14941
|
+
reasoning: reason,
|
|
14942
|
+
steps: [
|
|
14943
|
+
{
|
|
14944
|
+
at_ms: 0,
|
|
14945
|
+
duration_ms: 700,
|
|
14946
|
+
action: {
|
|
14947
|
+
type: "camera",
|
|
14948
|
+
target_block: id,
|
|
14949
|
+
scale: 1.2,
|
|
14950
|
+
padding: 60,
|
|
14951
|
+
easing: "ease-out"
|
|
14952
|
+
}
|
|
14953
|
+
},
|
|
14954
|
+
{
|
|
14955
|
+
at_ms: 300,
|
|
14956
|
+
duration_ms: 1e3,
|
|
14957
|
+
action: {
|
|
14958
|
+
type: "box",
|
|
14959
|
+
target_block: id,
|
|
14960
|
+
color: "#3B82F6",
|
|
14961
|
+
style: "dashed"
|
|
14962
|
+
}
|
|
14963
|
+
}
|
|
14964
|
+
]
|
|
14965
|
+
};
|
|
14966
|
+
}
|
|
14967
|
+
default: {
|
|
14968
|
+
return {
|
|
14969
|
+
version: 1,
|
|
14970
|
+
reasoning: `${reason}; unknown block.type, using highlight`,
|
|
14971
|
+
steps: [
|
|
14972
|
+
{
|
|
14973
|
+
at_ms: 0,
|
|
14974
|
+
duration_ms: 600,
|
|
14975
|
+
action: {
|
|
14976
|
+
type: "highlight",
|
|
14977
|
+
target_block: id,
|
|
14978
|
+
color: "rgba(250, 204, 21, 0.35)",
|
|
14979
|
+
draw_duration_ms: 500
|
|
14980
|
+
}
|
|
14981
|
+
}
|
|
14982
|
+
]
|
|
14983
|
+
};
|
|
14984
|
+
}
|
|
14985
|
+
}
|
|
14986
|
+
}
|
|
14987
|
+
|
|
14988
|
+
// src/components/TutorMode/TutorModeContainer.tsx
|
|
14989
|
+
var import_jsx_runtime52 = require("react/jsx-runtime");
|
|
14990
|
+
function buildBBoxIndex(bboxData) {
|
|
14991
|
+
const byPage = /* @__PURE__ */ new Map();
|
|
14992
|
+
const blockById = /* @__PURE__ */ new Map();
|
|
14993
|
+
const crossPageFigures = [];
|
|
14994
|
+
for (const page of bboxData) {
|
|
14995
|
+
byPage.set(page.page_number, page);
|
|
14996
|
+
for (const block of page.blocks) {
|
|
14997
|
+
blockById.set(block.block_id, { block, pageNumber: page.page_number });
|
|
14998
|
+
if ((block.type === "figure" || block.type === "figure_region" || block.type === "caption") && typeof block.text === "string" && block.text.length > 0) {
|
|
14999
|
+
crossPageFigures.push({
|
|
15000
|
+
block_id: block.block_id,
|
|
15001
|
+
page: page.page_number,
|
|
15002
|
+
type: block.type,
|
|
15003
|
+
text: block.text
|
|
15004
|
+
});
|
|
15005
|
+
}
|
|
15006
|
+
}
|
|
15007
|
+
}
|
|
15008
|
+
return { byPage, blockById, crossPageFigures };
|
|
15009
|
+
}
|
|
15010
|
+
function TutorModeContainer({
|
|
15011
|
+
pageNumber,
|
|
15012
|
+
bboxData,
|
|
15013
|
+
narrationStore,
|
|
15014
|
+
scale,
|
|
15015
|
+
rotation = 0,
|
|
15016
|
+
currentChunk,
|
|
15017
|
+
llm,
|
|
15018
|
+
idleTimeoutMs = 5e3,
|
|
15019
|
+
llmTimeoutMs = 3e4,
|
|
15020
|
+
embeddingProvider,
|
|
15021
|
+
showSubtitles = false,
|
|
15022
|
+
showExitButton = true,
|
|
15023
|
+
onExitTutorMode,
|
|
15024
|
+
minOverlayDurationMs,
|
|
15025
|
+
backgroundColor = "#ffffff",
|
|
15026
|
+
loadingComponent,
|
|
15027
|
+
className
|
|
15028
|
+
}) {
|
|
15029
|
+
const containerRef = (0, import_react56.useRef)(null);
|
|
15030
|
+
const index = (0, import_react56.useMemo)(() => buildBBoxIndex(bboxData), [bboxData]);
|
|
15031
|
+
const { document: document2 } = usePDFViewer();
|
|
15032
|
+
const [pageProxy, setPageProxy] = (0, import_react56.useState)(null);
|
|
15033
|
+
const [viewport, setViewport] = (0, import_react56.useState)({ width: 800, height: 1e3 });
|
|
15034
|
+
const camera = (0, import_zustand2.useStore)(narrationStore, (s) => s.camera);
|
|
15035
|
+
const activeOverlays = (0, import_zustand2.useStore)(narrationStore, (s) => s.activeOverlays);
|
|
15036
|
+
(0, import_react56.useEffect)(() => {
|
|
15037
|
+
if (!containerRef.current) return;
|
|
15038
|
+
const el = containerRef.current;
|
|
15039
|
+
const update = () => setViewport({ width: el.clientWidth, height: el.clientHeight });
|
|
15040
|
+
update();
|
|
15041
|
+
const ro = new ResizeObserver(update);
|
|
15042
|
+
ro.observe(el);
|
|
15043
|
+
return () => ro.disconnect();
|
|
15044
|
+
}, []);
|
|
15045
|
+
(0, import_react56.useEffect)(() => {
|
|
15046
|
+
if (!document2) {
|
|
15047
|
+
setPageProxy(null);
|
|
15048
|
+
return;
|
|
15049
|
+
}
|
|
15050
|
+
let cancelled = false;
|
|
15051
|
+
document2.getPage(pageNumber).then((p) => {
|
|
15052
|
+
if (!cancelled) setPageProxy(p);
|
|
15053
|
+
}).catch(() => {
|
|
15054
|
+
if (!cancelled) setPageProxy(null);
|
|
15055
|
+
});
|
|
15056
|
+
return () => {
|
|
15057
|
+
cancelled = true;
|
|
15058
|
+
};
|
|
15059
|
+
}, [document2, pageNumber]);
|
|
15060
|
+
(0, import_react56.useEffect)(() => {
|
|
15061
|
+
narrationStore.getState().setCurrentPage(pageNumber);
|
|
15062
|
+
}, [pageNumber, narrationStore]);
|
|
15063
|
+
(0, import_react56.useEffect)(() => {
|
|
15064
|
+
const page2 = index.byPage.get(pageNumber);
|
|
15065
|
+
if (!page2) return;
|
|
15066
|
+
if (viewport.width === 0 || viewport.height === 0) return;
|
|
15067
|
+
if (narrationStore.getState().activeOverlays.length > 0) return;
|
|
15068
|
+
const fit = Math.min(
|
|
15069
|
+
viewport.width / page2.page_dimensions.width,
|
|
15070
|
+
viewport.height / page2.page_dimensions.height
|
|
15071
|
+
) * 0.95;
|
|
15072
|
+
narrationStore.getState().setCamera({ scale: fit, x: 0, y: 0 });
|
|
15073
|
+
}, [pageNumber, viewport, index, narrationStore]);
|
|
15074
|
+
const engineRef = (0, import_react56.useRef)(null);
|
|
15075
|
+
(0, import_react56.useEffect)(() => {
|
|
15076
|
+
engineRef.current = new StoryboardEngine({
|
|
15077
|
+
narrationStore,
|
|
15078
|
+
bboxIndex: index,
|
|
15079
|
+
getViewport: () => viewport,
|
|
15080
|
+
minOverlayDurationMs
|
|
15081
|
+
});
|
|
15082
|
+
return () => engineRef.current?.cancelPending();
|
|
15083
|
+
}, [narrationStore, index, viewport, minOverlayDurationMs]);
|
|
15084
|
+
const abortRef = (0, import_react56.useRef)(null);
|
|
15085
|
+
const debounceRef = (0, import_react56.useRef)(null);
|
|
15086
|
+
const lastChunkRef = (0, import_react56.useRef)(null);
|
|
15087
|
+
(0, import_react56.useEffect)(() => {
|
|
15088
|
+
if (!llm) return;
|
|
15089
|
+
if (!currentChunk || currentChunk === lastChunkRef.current) return;
|
|
15090
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
15091
|
+
debounceRef.current = setTimeout(async () => {
|
|
15092
|
+
const chunk = currentChunk;
|
|
15093
|
+
if (chunk === lastChunkRef.current) return;
|
|
15094
|
+
lastChunkRef.current = chunk;
|
|
15095
|
+
const page2 = index.byPage.get(pageNumber);
|
|
15096
|
+
if (!page2) return;
|
|
15097
|
+
narrationStore.getState().pushChunkHistory({
|
|
15098
|
+
text: chunk,
|
|
15099
|
+
pageNumber,
|
|
15100
|
+
timestamp: Date.now()
|
|
15101
|
+
});
|
|
15102
|
+
narrationStore.getState().appendDebugEvent({
|
|
15103
|
+
kind: "chunk",
|
|
15104
|
+
summary: `chunk \u2192 ${chunk.slice(0, 80)}${chunk.length > 80 ? "\u2026" : ""}`,
|
|
15105
|
+
payload: { chunk, pageNumber }
|
|
15106
|
+
});
|
|
15107
|
+
abortRef.current?.abort();
|
|
15108
|
+
abortRef.current = new AbortController();
|
|
15109
|
+
narrationStore.getState().setLlmStatus("in-flight");
|
|
15110
|
+
narrationStore.getState().appendDebugEvent({
|
|
15111
|
+
kind: "llm-request",
|
|
15112
|
+
summary: `LLM ${llm.model} (page ${pageNumber}, ${page2.blocks.length} blocks)`,
|
|
15113
|
+
payload: { model: llm.model, pageNumber, blockCount: page2.blocks.length }
|
|
15114
|
+
});
|
|
15115
|
+
const result = await directStoryboard(llm, {
|
|
15116
|
+
chunk,
|
|
15117
|
+
pageNumber,
|
|
15118
|
+
page: page2,
|
|
15119
|
+
index,
|
|
15120
|
+
history: narrationStore.getState().chunkHistory,
|
|
15121
|
+
camera: narrationStore.getState().camera,
|
|
15122
|
+
activeOverlays: narrationStore.getState().activeOverlays,
|
|
15123
|
+
signal: abortRef.current.signal,
|
|
15124
|
+
timeoutMs: llmTimeoutMs
|
|
15125
|
+
});
|
|
15126
|
+
if (result.storyboard) {
|
|
15127
|
+
narrationStore.getState().setLlmStatus("idle");
|
|
15128
|
+
narrationStore.getState().appendDebugEvent({
|
|
15129
|
+
kind: "llm-response",
|
|
15130
|
+
summary: `storyboard \u2713 ${result.storyboard.steps.length} steps \u2014 ${result.storyboard.reasoning.slice(0, 60)}`,
|
|
15131
|
+
payload: { raw: result.raw, storyboard: result.storyboard }
|
|
15132
|
+
});
|
|
15133
|
+
engineRef.current?.execute(result.storyboard);
|
|
15134
|
+
narrationStore.getState().appendDebugEvent({
|
|
15135
|
+
kind: "storyboard-execute",
|
|
15136
|
+
summary: `engine executing ${result.storyboard.steps.length} steps`,
|
|
15137
|
+
payload: result.storyboard.steps.map((s) => ({
|
|
15138
|
+
at_ms: s.at_ms,
|
|
15139
|
+
type: s.action.type,
|
|
15140
|
+
target: "target_block" in s.action ? s.action.target_block : "target" in s.action ? s.action.target : void 0
|
|
15141
|
+
}))
|
|
15142
|
+
});
|
|
15143
|
+
} else {
|
|
15144
|
+
narrationStore.getState().setLlmStatus("failed", result.error ?? "unknown");
|
|
15145
|
+
narrationStore.getState().appendDebugEvent({
|
|
15146
|
+
kind: "llm-error",
|
|
15147
|
+
summary: `LLM failed: ${(result.error ?? "unknown").slice(0, 80)}`,
|
|
15148
|
+
payload: { error: result.error, raw: result.raw }
|
|
15149
|
+
});
|
|
15150
|
+
if (embeddingProvider) {
|
|
15151
|
+
try {
|
|
15152
|
+
const match = await matchChunkToBlock(chunk, page2, embeddingProvider);
|
|
15153
|
+
const fallbackSb = storyboardFromMatch(match, page2);
|
|
15154
|
+
narrationStore.getState().appendDebugEvent({
|
|
15155
|
+
kind: "fallback-fired",
|
|
15156
|
+
summary: `embedding fallback \u2192 ${match?.block.block_id ?? "no match"}`,
|
|
15157
|
+
payload: { match, storyboard: fallbackSb }
|
|
15158
|
+
});
|
|
15159
|
+
engineRef.current?.execute(fallbackSb);
|
|
15160
|
+
} catch (e) {
|
|
15161
|
+
narrationStore.getState().appendDebugEvent({
|
|
15162
|
+
kind: "llm-error",
|
|
15163
|
+
summary: `fallback also failed: ${e.message}`,
|
|
15164
|
+
payload: e
|
|
15165
|
+
});
|
|
15166
|
+
}
|
|
15167
|
+
}
|
|
15168
|
+
}
|
|
15169
|
+
}, 200);
|
|
15170
|
+
return () => {
|
|
15171
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
15172
|
+
};
|
|
15173
|
+
}, [currentChunk, llm, index, pageNumber, narrationStore, embeddingProvider, llmTimeoutMs]);
|
|
15174
|
+
(0, import_react56.useEffect)(() => {
|
|
15175
|
+
if (!currentChunk) return;
|
|
15176
|
+
const t = setTimeout(() => {
|
|
15177
|
+
if (!engineRef.current) return;
|
|
15178
|
+
const hist = narrationStore.getState().chunkHistory;
|
|
15179
|
+
const latest = hist.length > 0 ? hist[hist.length - 1] : null;
|
|
15180
|
+
if (!latest) return;
|
|
15181
|
+
if (Date.now() - latest.timestamp < idleTimeoutMs) return;
|
|
15182
|
+
engineRef.current.resetVisuals();
|
|
15183
|
+
}, idleTimeoutMs + 100);
|
|
15184
|
+
return () => clearTimeout(t);
|
|
15185
|
+
}, [currentChunk, idleTimeoutMs, narrationStore]);
|
|
15186
|
+
const page = index.byPage.get(pageNumber);
|
|
15187
|
+
const dpiScale = page ? page.page_dimensions.dpi / 72 : 1;
|
|
15188
|
+
const rasterScale = dpiScale * (scale || 1);
|
|
15189
|
+
const baseW = page ? page.page_dimensions.width * (scale || 1) : 0;
|
|
15190
|
+
const baseH = page ? page.page_dimensions.height * (scale || 1) : 0;
|
|
15191
|
+
const isReady = !!page && !!pageProxy;
|
|
15192
|
+
return /* @__PURE__ */ (0, import_jsx_runtime52.jsxs)(
|
|
15193
|
+
"div",
|
|
15194
|
+
{
|
|
15195
|
+
ref: containerRef,
|
|
15196
|
+
className,
|
|
15197
|
+
style: {
|
|
15198
|
+
position: "relative",
|
|
15199
|
+
width: "100%",
|
|
15200
|
+
height: "100%",
|
|
15201
|
+
overflow: "hidden",
|
|
15202
|
+
background: backgroundColor
|
|
15203
|
+
},
|
|
15204
|
+
"data-role": "tutor-mode-container",
|
|
15205
|
+
"data-page-loaded": isReady ? "true" : "false",
|
|
15206
|
+
children: [
|
|
15207
|
+
showExitButton && isReady ? /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
|
|
15208
|
+
"button",
|
|
15209
|
+
{
|
|
15210
|
+
onClick: () => {
|
|
15211
|
+
engineRef.current?.resetVisuals();
|
|
15212
|
+
onExitTutorMode?.();
|
|
15213
|
+
},
|
|
15214
|
+
style: {
|
|
15215
|
+
position: "absolute",
|
|
15216
|
+
top: 12,
|
|
15217
|
+
right: 12,
|
|
15218
|
+
zIndex: 60,
|
|
15219
|
+
minHeight: 40,
|
|
15220
|
+
minWidth: 40,
|
|
15221
|
+
padding: "8px 14px",
|
|
15222
|
+
border: "none",
|
|
15223
|
+
borderRadius: 8,
|
|
15224
|
+
// Dark translucent pill with white text reads cleanly on both
|
|
15225
|
+
// light and dark container backgrounds.
|
|
15226
|
+
background: "rgba(17,24,39,0.72)",
|
|
15227
|
+
color: "white",
|
|
15228
|
+
cursor: "pointer",
|
|
15229
|
+
fontFamily: "system-ui, sans-serif",
|
|
15230
|
+
fontSize: 14,
|
|
15231
|
+
touchAction: "manipulation"
|
|
15232
|
+
},
|
|
15233
|
+
"aria-label": "Reset view \u2014 clear overlays and fit the page",
|
|
15234
|
+
"data-role": "exit-tutor",
|
|
15235
|
+
children: "Reset view"
|
|
15236
|
+
}
|
|
15237
|
+
) : null,
|
|
15238
|
+
isReady ? /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(CameraView, { camera, children: /* @__PURE__ */ (0, import_jsx_runtime52.jsxs)(
|
|
15239
|
+
"div",
|
|
15240
|
+
{
|
|
15241
|
+
style: {
|
|
15242
|
+
position: "absolute",
|
|
15243
|
+
top: "50%",
|
|
15244
|
+
left: "50%",
|
|
15245
|
+
width: baseW,
|
|
15246
|
+
height: baseH,
|
|
15247
|
+
transform: "translate(-50%, -50%)"
|
|
15248
|
+
},
|
|
15249
|
+
children: [
|
|
15250
|
+
/* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
|
|
15251
|
+
PDFPage,
|
|
15252
|
+
{
|
|
15253
|
+
pageNumber,
|
|
15254
|
+
page: pageProxy,
|
|
15255
|
+
scale: rasterScale,
|
|
15256
|
+
rotation,
|
|
15257
|
+
showTextLayer: false,
|
|
15258
|
+
showHighlightLayer: false,
|
|
15259
|
+
showAnnotationLayer: false
|
|
15260
|
+
}
|
|
15261
|
+
),
|
|
15262
|
+
/* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
|
|
15263
|
+
CinemaLayer,
|
|
15264
|
+
{
|
|
15265
|
+
page,
|
|
15266
|
+
index,
|
|
15267
|
+
overlays: activeOverlays,
|
|
15268
|
+
scale: scale || 1
|
|
15269
|
+
}
|
|
15270
|
+
)
|
|
15271
|
+
]
|
|
15272
|
+
}
|
|
15273
|
+
) }) : /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(TutorLoadingState, { custom: loadingComponent }),
|
|
15274
|
+
showSubtitles ? /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(SubtitleBar, { text: currentChunk ?? null }) : null
|
|
15275
|
+
]
|
|
15276
|
+
}
|
|
15277
|
+
);
|
|
15278
|
+
}
|
|
15279
|
+
function TutorLoadingState({
|
|
15280
|
+
custom
|
|
15281
|
+
}) {
|
|
15282
|
+
if (custom) {
|
|
15283
|
+
return /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
|
|
15284
|
+
"div",
|
|
15285
|
+
{
|
|
15286
|
+
style: {
|
|
15287
|
+
position: "absolute",
|
|
15288
|
+
inset: 0,
|
|
15289
|
+
display: "flex",
|
|
15290
|
+
alignItems: "center",
|
|
15291
|
+
justifyContent: "center"
|
|
15292
|
+
},
|
|
15293
|
+
"data-role": "tutor-loading",
|
|
15294
|
+
children: custom
|
|
15295
|
+
}
|
|
15296
|
+
);
|
|
15297
|
+
}
|
|
15298
|
+
return /* @__PURE__ */ (0, import_jsx_runtime52.jsxs)(
|
|
15299
|
+
"div",
|
|
15300
|
+
{
|
|
15301
|
+
style: {
|
|
15302
|
+
position: "absolute",
|
|
15303
|
+
inset: 0,
|
|
15304
|
+
display: "flex",
|
|
15305
|
+
flexDirection: "column",
|
|
15306
|
+
alignItems: "center",
|
|
15307
|
+
justifyContent: "center",
|
|
15308
|
+
gap: 12,
|
|
15309
|
+
color: "rgba(0,0,0,0.55)",
|
|
15310
|
+
fontFamily: "system-ui, sans-serif",
|
|
15311
|
+
fontSize: 13
|
|
15312
|
+
},
|
|
15313
|
+
"data-role": "tutor-loading",
|
|
15314
|
+
children: [
|
|
15315
|
+
/* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
|
|
15316
|
+
"div",
|
|
15317
|
+
{
|
|
15318
|
+
"aria-hidden": true,
|
|
15319
|
+
style: {
|
|
15320
|
+
width: 36,
|
|
15321
|
+
height: 36,
|
|
15322
|
+
borderRadius: "50%",
|
|
15323
|
+
border: "3px solid rgba(0,0,0,0.1)",
|
|
15324
|
+
borderTopColor: "rgba(0,0,0,0.45)",
|
|
15325
|
+
animation: "pdf-tutor-spin 0.9s linear infinite"
|
|
15326
|
+
}
|
|
15327
|
+
}
|
|
15328
|
+
),
|
|
15329
|
+
/* @__PURE__ */ (0, import_jsx_runtime52.jsx)("span", { children: "Loading document\u2026" }),
|
|
15330
|
+
/* @__PURE__ */ (0, import_jsx_runtime52.jsx)("style", { children: `
|
|
15331
|
+
@keyframes pdf-tutor-spin {
|
|
15332
|
+
from { transform: rotate(0deg); }
|
|
15333
|
+
to { transform: rotate(360deg); }
|
|
15334
|
+
}
|
|
15335
|
+
` })
|
|
15336
|
+
]
|
|
15337
|
+
}
|
|
15338
|
+
);
|
|
15339
|
+
}
|
|
15340
|
+
|
|
15341
|
+
// src/director/transformers-embedding.ts
|
|
15342
|
+
var loaded = null;
|
|
15343
|
+
function getLocalMiniLM() {
|
|
15344
|
+
if (loaded) return loaded;
|
|
15345
|
+
loaded = (async () => {
|
|
15346
|
+
const mod = await import(
|
|
15347
|
+
/* webpackIgnore: true */
|
|
15348
|
+
"@xenova/transformers"
|
|
15349
|
+
);
|
|
15350
|
+
const { pipeline } = mod;
|
|
15351
|
+
const extractor = await pipeline(
|
|
15352
|
+
"feature-extraction",
|
|
15353
|
+
"Xenova/all-MiniLM-L6-v2"
|
|
15354
|
+
);
|
|
15355
|
+
return {
|
|
15356
|
+
async embed(texts) {
|
|
15357
|
+
const out = [];
|
|
15358
|
+
for (const t of texts) {
|
|
15359
|
+
const result = await extractor(t, {
|
|
15360
|
+
pooling: "mean",
|
|
15361
|
+
normalize: true
|
|
15362
|
+
});
|
|
15363
|
+
out.push(new Float32Array(result.data.slice()));
|
|
15364
|
+
}
|
|
15365
|
+
return out;
|
|
15366
|
+
}
|
|
15367
|
+
};
|
|
15368
|
+
})();
|
|
15369
|
+
return loaded;
|
|
15370
|
+
}
|
|
15371
|
+
|
|
12967
15372
|
// src/index.ts
|
|
12968
15373
|
init_hooks();
|
|
12969
15374
|
init_store();
|
|
@@ -12975,19 +15380,26 @@ init_PluginManager();
|
|
|
12975
15380
|
init_utils();
|
|
12976
15381
|
// Annotate the CommonJS export names for ESM import in node:
|
|
12977
15382
|
0 && (module.exports = {
|
|
15383
|
+
AnimatedHighlight,
|
|
15384
|
+
AnimatedUnderline,
|
|
12978
15385
|
AnnotationLayer,
|
|
12979
15386
|
AnnotationToolbar,
|
|
12980
15387
|
AskAboutOverlay,
|
|
12981
15388
|
AskAboutTrigger,
|
|
12982
15389
|
BookModeContainer,
|
|
12983
15390
|
BookmarksPanel,
|
|
15391
|
+
BoxOverlay,
|
|
15392
|
+
CalloutArrow,
|
|
15393
|
+
CameraView,
|
|
12984
15394
|
CanvasLayer,
|
|
15395
|
+
CinemaLayer,
|
|
12985
15396
|
ContinuousScrollContainer,
|
|
12986
15397
|
DocumentContainer,
|
|
12987
15398
|
DrawingCanvas,
|
|
12988
15399
|
DualPageContainer,
|
|
12989
15400
|
FloatingZoomControls,
|
|
12990
15401
|
FocusRegionLayer,
|
|
15402
|
+
GhostReference,
|
|
12991
15403
|
HighlightLayer,
|
|
12992
15404
|
HighlightPopover,
|
|
12993
15405
|
HighlightsPanel,
|
|
@@ -13004,32 +15416,46 @@ init_utils();
|
|
|
13004
15416
|
PDFViewerContext,
|
|
13005
15417
|
PDFViewerProvider,
|
|
13006
15418
|
PluginManager,
|
|
15419
|
+
PulseOverlay,
|
|
13007
15420
|
QuickNoteButton,
|
|
13008
15421
|
QuickNotePopover,
|
|
15422
|
+
SYSTEM_PROMPT,
|
|
13009
15423
|
SearchPanel,
|
|
13010
15424
|
SelectionToolbar,
|
|
13011
15425
|
ShapePreview,
|
|
13012
15426
|
ShapeRenderer,
|
|
13013
15427
|
Sidebar,
|
|
15428
|
+
SpotlightMask,
|
|
15429
|
+
StickyLabel,
|
|
13014
15430
|
StickyNote,
|
|
15431
|
+
StoryboardActionSchema,
|
|
15432
|
+
StoryboardEngine,
|
|
15433
|
+
StoryboardSchema,
|
|
15434
|
+
SubtitleBar,
|
|
13015
15435
|
TakeawaysPanel,
|
|
13016
15436
|
TextLayer,
|
|
13017
15437
|
ThumbnailPanel,
|
|
13018
15438
|
Toolbar,
|
|
15439
|
+
TutorModeContainer,
|
|
13019
15440
|
VirtualizedDocumentContainer,
|
|
13020
15441
|
applyRotation,
|
|
15442
|
+
buildBBoxIndex,
|
|
15443
|
+
buildUserPrompt,
|
|
13021
15444
|
clearHighlights,
|
|
13022
15445
|
clearStudentData,
|
|
13023
15446
|
cn,
|
|
15447
|
+
cosineSimilarity,
|
|
13024
15448
|
countTextOnPage,
|
|
13025
15449
|
createAgentAPI,
|
|
13026
15450
|
createAgentStore,
|
|
13027
15451
|
createAnnotationStore,
|
|
15452
|
+
createNarrationStore,
|
|
13028
15453
|
createPDFViewer,
|
|
13029
15454
|
createPluginManager,
|
|
13030
15455
|
createSearchStore,
|
|
13031
15456
|
createStudentStore,
|
|
13032
15457
|
createViewerStore,
|
|
15458
|
+
directStoryboard,
|
|
13033
15459
|
doRectsIntersect,
|
|
13034
15460
|
downloadAnnotationsAsJSON,
|
|
13035
15461
|
downloadAnnotationsAsMarkdown,
|
|
@@ -13044,6 +15470,7 @@ init_utils();
|
|
|
13044
15470
|
generateDocumentId,
|
|
13045
15471
|
getAllDocumentIds,
|
|
13046
15472
|
getAllStudentDataDocumentIds,
|
|
15473
|
+
getLocalMiniLM,
|
|
13047
15474
|
getMetadata,
|
|
13048
15475
|
getOutline,
|
|
13049
15476
|
getPage,
|
|
@@ -13061,6 +15488,8 @@ init_utils();
|
|
|
13061
15488
|
loadDocumentWithCallbacks,
|
|
13062
15489
|
loadHighlights,
|
|
13063
15490
|
loadStudentData,
|
|
15491
|
+
makeOverlayId,
|
|
15492
|
+
matchChunkToBlock,
|
|
13064
15493
|
mergeAdjacentRects,
|
|
13065
15494
|
pdfToPercent,
|
|
13066
15495
|
pdfToViewport,
|
|
@@ -13073,6 +15502,9 @@ init_utils();
|
|
|
13073
15502
|
saveHighlights,
|
|
13074
15503
|
saveStudentData,
|
|
13075
15504
|
scaleRect,
|
|
15505
|
+
storyboardFromMatch,
|
|
15506
|
+
storyboardJsonSchema,
|
|
15507
|
+
truncate,
|
|
13076
15508
|
useAgentContext,
|
|
13077
15509
|
useAgentStore,
|
|
13078
15510
|
useAnnotationStore,
|