pdfjs-reader-core 0.4.3 → 0.5.2
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 +51 -0
- package/dist/index.cjs +162 -76
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +47 -20
- package/dist/index.d.ts +47 -20
- package/dist/index.js +162 -76
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1278,6 +1278,7 @@ default. Clicking it clears every overlay and returns the camera to fit-page.
|
|
|
1278
1278
|
| `backgroundColor` | `string` | `'#ffffff'` | Surround colour visible around the PDF when the viewport is larger than the page fit. v0.4.1+ |
|
|
1279
1279
|
| `loadingComponent` | `ReactNode` | default spinner | Custom loading state shown while the PDF document/page is still fetching. v0.4.1+ |
|
|
1280
1280
|
| `onPageChange` | `(page: number) => void` | — | Called when the viewer's page changes from any source (agent API, sidebar, programmatic). Pair with the `pageNumber` prop for bidirectional sync. **Required** when agents call `goToPage`/`nextPage`/`previousPage`. v0.4.2+ |
|
|
1281
|
+
| `storyboardProvider` | `(input) => Promise<Storyboard \| null>` | — | Consumer-owned director — when provided, called per chunk INSTEAD of the built-in LLM director. Your backend owns the system prompt + bbox context and returns the storyboard JSON. v0.5.0+ |
|
|
1281
1282
|
| `className` | `string` | — | Passes through to the root container for custom theming |
|
|
1282
1283
|
|
|
1283
1284
|
### LlmConfig
|
|
@@ -1299,6 +1300,56 @@ interface LlmConfig {
|
|
|
1299
1300
|
multiple consumers and each owns its own inference endpoint. Pass the URL as
|
|
1300
1301
|
a prop at the call site, sourced from an env var or runtime config.
|
|
1301
1302
|
|
|
1303
|
+
### Alternative: `storyboardProvider` (v0.5.0+)
|
|
1304
|
+
|
|
1305
|
+
When your backend owns the system prompt and bbox context, use
|
|
1306
|
+
`storyboardProvider` instead of `llm`. The library will call your provider per
|
|
1307
|
+
chunk, validate its return value against `StoryboardSchema`, and execute the
|
|
1308
|
+
result.
|
|
1309
|
+
|
|
1310
|
+
```tsx
|
|
1311
|
+
<TutorModeContainer
|
|
1312
|
+
pageNumber={currentPage}
|
|
1313
|
+
bboxData={bboxData}
|
|
1314
|
+
narrationStore={storeRef.current}
|
|
1315
|
+
currentChunk={currentChunk}
|
|
1316
|
+
storyboardProvider={async ({ chunk, pageNumber, signal }) => {
|
|
1317
|
+
// Your director endpoint already has bbox server-side. Send just
|
|
1318
|
+
// the chunk + page number and get the storyboard back.
|
|
1319
|
+
const res = await fetch('/director', {
|
|
1320
|
+
method: 'POST',
|
|
1321
|
+
body: JSON.stringify({ chunk, pageNumber }),
|
|
1322
|
+
signal,
|
|
1323
|
+
});
|
|
1324
|
+
if (!res.ok) return null; // library skips this chunk gracefully
|
|
1325
|
+
return res.json(); // must match StoryboardSchema
|
|
1326
|
+
}}
|
|
1327
|
+
/>
|
|
1328
|
+
```
|
|
1329
|
+
|
|
1330
|
+
**Why use this instead of `llm`?**
|
|
1331
|
+
- You iterate on the system prompt without a library upgrade.
|
|
1332
|
+
- You choose the model (fine-tuned, OSS, hosted, local) without vendor lock.
|
|
1333
|
+
- You cache bbox server-side; the browser sends only the chunk + page number.
|
|
1334
|
+
- Works great with KV-cache / prefix caching on the director model.
|
|
1335
|
+
|
|
1336
|
+
**What the library still handles regardless of path:**
|
|
1337
|
+
- `StoryboardSchema` validation of the returned JSON
|
|
1338
|
+
- Range clamping of out-of-bounds numeric fields
|
|
1339
|
+
- `enforceOverlayPresence` auto-pulse if the storyboard is camera-only
|
|
1340
|
+
- Engine scheduling (per-step `setTimeout` at `at_ms`)
|
|
1341
|
+
- All overlay rendering (spotlight / underline / highlight / pulse / callout /
|
|
1342
|
+
ghost reference / box / label), plus viewport-space overlays and camera math
|
|
1343
|
+
- Debug events (`llm-request` / `llm-response` / `storyboard-execute`) so the
|
|
1344
|
+
DebugLog telemetry works identically to the built-in path
|
|
1345
|
+
|
|
1346
|
+
**Priority:** if both `storyboardProvider` and `llm` are set, the provider
|
|
1347
|
+
wins and `llm` is ignored.
|
|
1348
|
+
|
|
1349
|
+
**Return `null` to skip:** the provider may decide a given chunk shouldn't
|
|
1350
|
+
trigger visuals (e.g. filler words, acknowledgements). Returning `null`
|
|
1351
|
+
emits a `note` debug event and no storyboard fires.
|
|
1352
|
+
|
|
1302
1353
|
### The integration contract in one picture
|
|
1303
1354
|
|
|
1304
1355
|
```
|
package/dist/index.cjs
CHANGED
|
@@ -13181,10 +13181,26 @@ var PAPER = "#faf6ec";
|
|
|
13181
13181
|
var ACCENT = "#b04a1a";
|
|
13182
13182
|
var ACCENT_SOFT = "rgba(176, 74, 26, 0.18)";
|
|
13183
13183
|
var ACCENT_GLOW = "rgba(176, 74, 26, 0.35)";
|
|
13184
|
-
var MARKER = "#e6b422";
|
|
13185
|
-
var MARKER_SOFT = "rgba(230, 180, 34, 0.38)";
|
|
13186
13184
|
var SERIF = "'Iowan Old Style', 'Palatino Linotype', Palatino, 'Book Antiqua', 'EB Garamond', 'Hoefler Text', Georgia, serif";
|
|
13187
13185
|
var EASE_OUT_EXPO = [0.22, 1, 0.36, 1];
|
|
13186
|
+
var PILL_FONT_CAPS = "clamp(10.5px, 0.55vw + 8.5px, 14.5px)";
|
|
13187
|
+
var PILL_FONT_BODY = "clamp(12px, 0.6vw + 10px, 16.5px)";
|
|
13188
|
+
var PILL_FONT_DISPLAY = "clamp(14px, 0.75vw + 12px, 19px)";
|
|
13189
|
+
var PILL_MAX_W_CAPS = "clamp(180px, 26vw, 380px)";
|
|
13190
|
+
var PILL_MAX_W_BODY = "clamp(200px, 28vw, 440px)";
|
|
13191
|
+
function resolvePillOffset(viewportWidthPx) {
|
|
13192
|
+
return clamp(0.022 * viewportWidthPx + 12, 20, 44);
|
|
13193
|
+
}
|
|
13194
|
+
function resolveMaxPillW(viewportWidthPx) {
|
|
13195
|
+
return clamp(0.26 * viewportWidthPx, 180, 380);
|
|
13196
|
+
}
|
|
13197
|
+
function resolveMaxPillH(viewportWidthPx) {
|
|
13198
|
+
const font = clamp(55e-4 * viewportWidthPx + 8.5, 10.5, 14.5);
|
|
13199
|
+
return clamp(font * 2.6, 28, 42);
|
|
13200
|
+
}
|
|
13201
|
+
function clamp(v, lo, hi) {
|
|
13202
|
+
return Math.min(hi, Math.max(lo, v));
|
|
13203
|
+
}
|
|
13188
13204
|
|
|
13189
13205
|
// src/components/TutorMode/SpotlightMask.tsx
|
|
13190
13206
|
var import_jsx_runtime42 = require("react/jsx-runtime");
|
|
@@ -13470,17 +13486,17 @@ function AnimatedUnderline({ bbox, action }) {
|
|
|
13470
13486
|
var import_react56 = require("react");
|
|
13471
13487
|
var import_framer_motion4 = require("framer-motion");
|
|
13472
13488
|
var import_jsx_runtime44 = require("react/jsx-runtime");
|
|
13489
|
+
var WASH = "rgba(230, 180, 34, 0.22)";
|
|
13473
13490
|
function AnimatedHighlight({ bbox, action }) {
|
|
13474
13491
|
const [x1, y1, x2, y2] = bbox;
|
|
13475
13492
|
const h = Math.max(1, y2 - y1);
|
|
13476
13493
|
const bleed = Math.min(4, h * 0.12);
|
|
13477
13494
|
const yTop = y1 - bleed;
|
|
13478
13495
|
const yBot = y2 + bleed;
|
|
13479
|
-
const height = yBot - yTop;
|
|
13480
13496
|
const duration = action.draw_duration_ms / 1e3;
|
|
13481
13497
|
const filterId = (0, import_react56.useId)();
|
|
13482
|
-
const
|
|
13483
|
-
const
|
|
13498
|
+
const isDefaultColour = !action.color || action.color === "rgba(250, 204, 21, 0.35)" || action.color === "rgba(250,204,21,0.35)";
|
|
13499
|
+
const fill = isDefaultColour ? WASH : action.color;
|
|
13484
13500
|
const taper = Math.min(6, h * 0.2);
|
|
13485
13501
|
const pathD = `
|
|
13486
13502
|
M ${x1 - 2} ${yTop + taper}
|
|
@@ -13510,44 +13526,24 @@ function AnimatedHighlight({ bbox, action }) {
|
|
|
13510
13526
|
"feTurbulence",
|
|
13511
13527
|
{
|
|
13512
13528
|
type: "fractalNoise",
|
|
13513
|
-
baseFrequency: "1.
|
|
13529
|
+
baseFrequency: "1.8",
|
|
13514
13530
|
numOctaves: "1",
|
|
13515
13531
|
seed: 3,
|
|
13516
13532
|
result: "noise"
|
|
13517
13533
|
}
|
|
13518
13534
|
),
|
|
13519
|
-
/* @__PURE__ */ (0, import_jsx_runtime44.jsx)("feDisplacementMap", { in: "SourceGraphic", in2: "noise", scale: 1
|
|
13535
|
+
/* @__PURE__ */ (0, import_jsx_runtime44.jsx)("feDisplacementMap", { in: "SourceGraphic", in2: "noise", scale: 1 })
|
|
13520
13536
|
] }) }),
|
|
13521
|
-
/* @__PURE__ */ (0, import_jsx_runtime44.
|
|
13522
|
-
import_framer_motion4.motion.
|
|
13537
|
+
/* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
|
|
13538
|
+
import_framer_motion4.motion.path,
|
|
13523
13539
|
{
|
|
13540
|
+
d: pathD,
|
|
13541
|
+
fill,
|
|
13524
13542
|
initial: { clipPath: `inset(0 100% 0 0)` },
|
|
13525
13543
|
animate: { clipPath: `inset(0 0% 0 0)` },
|
|
13526
13544
|
exit: { opacity: 0 },
|
|
13527
|
-
|
|
13528
|
-
|
|
13529
|
-
/* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
|
|
13530
|
-
"path",
|
|
13531
|
-
{
|
|
13532
|
-
d: pathD,
|
|
13533
|
-
fill,
|
|
13534
|
-
opacity: 0.85,
|
|
13535
|
-
filter: `url(#${filterId})`
|
|
13536
|
-
}
|
|
13537
|
-
),
|
|
13538
|
-
/* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
|
|
13539
|
-
"rect",
|
|
13540
|
-
{
|
|
13541
|
-
x: x1 - 1,
|
|
13542
|
-
y: y1 - bleed * 0.4,
|
|
13543
|
-
width: x2 - x1 + 2,
|
|
13544
|
-
height: height - bleed * 0.8,
|
|
13545
|
-
fill: inner,
|
|
13546
|
-
opacity: 0.5,
|
|
13547
|
-
filter: `url(#${filterId})`
|
|
13548
|
-
}
|
|
13549
|
-
)
|
|
13550
|
-
]
|
|
13545
|
+
filter: `url(#${filterId})`,
|
|
13546
|
+
transition: { duration, ease: EASE_OUT_EXPO }
|
|
13551
13547
|
}
|
|
13552
13548
|
)
|
|
13553
13549
|
]
|
|
@@ -14117,9 +14113,11 @@ function GhostReference({
|
|
|
14117
14113
|
{
|
|
14118
14114
|
style: {
|
|
14119
14115
|
position: "relative",
|
|
14120
|
-
|
|
14116
|
+
// Padding scales with viewport: compact on phones, breathing on
|
|
14117
|
+
// desktop. Extra 4px on the left preserves the footnote-rule gap.
|
|
14118
|
+
padding: "clamp(14px, 1.4vw + 10px, 26px) clamp(14px, 1.4vw + 10px, 26px) clamp(14px, 1.4vw + 10px, 26px) clamp(18px, 1.4vw + 14px, 30px)",
|
|
14121
14119
|
display: "flex",
|
|
14122
|
-
gap:
|
|
14120
|
+
gap: "clamp(12px, 1.1vw + 8px, 20px)",
|
|
14123
14121
|
alignItems: "flex-start"
|
|
14124
14122
|
},
|
|
14125
14123
|
children: [
|
|
@@ -14133,7 +14131,7 @@ function GhostReference({
|
|
|
14133
14131
|
transition: { duration: 0.45, delay: 0.18, ease: [0.22, 1, 0.36, 1] },
|
|
14134
14132
|
style: {
|
|
14135
14133
|
flexShrink: 0,
|
|
14136
|
-
width:
|
|
14134
|
+
width: "clamp(40px, 4vw + 20px, 64px)",
|
|
14137
14135
|
aspectRatio: `${page.width} / ${page.height}`,
|
|
14138
14136
|
background: PAPER_DEEP,
|
|
14139
14137
|
borderRadius: 2,
|
|
@@ -14198,7 +14196,7 @@ function GhostReference({
|
|
|
14198
14196
|
flex: 1,
|
|
14199
14197
|
minWidth: 0,
|
|
14200
14198
|
fontFamily: SERIF2,
|
|
14201
|
-
fontSize:
|
|
14199
|
+
fontSize: PILL_FONT_DISPLAY,
|
|
14202
14200
|
lineHeight: 1.55,
|
|
14203
14201
|
color: INK2,
|
|
14204
14202
|
fontFeatureSettings: "'liga' 1, 'kern' 1, 'onum' 1",
|
|
@@ -14220,7 +14218,7 @@ function GhostReference({
|
|
|
14220
14218
|
left: -14,
|
|
14221
14219
|
top: -2,
|
|
14222
14220
|
color: ACCENT2,
|
|
14223
|
-
fontSize:
|
|
14221
|
+
fontSize: "clamp(18px, 1vw + 14px, 28px)",
|
|
14224
14222
|
lineHeight: 1,
|
|
14225
14223
|
fontWeight: 500
|
|
14226
14224
|
// ornamental flourish anchoring the paragraph
|
|
@@ -14330,10 +14328,6 @@ var import_framer_motion12 = require("framer-motion");
|
|
|
14330
14328
|
// src/components/TutorMode/StickyLabel.tsx
|
|
14331
14329
|
var import_framer_motion11 = require("framer-motion");
|
|
14332
14330
|
var import_jsx_runtime51 = require("react/jsx-runtime");
|
|
14333
|
-
var INK3 = "#2a2420";
|
|
14334
|
-
var PAPER3 = "#faf6ec";
|
|
14335
|
-
var ACCENT3 = "#b04a1a";
|
|
14336
|
-
var SERIF3 = "'Iowan Old Style', 'Palatino Linotype', Palatino, 'Book Antiqua', 'EB Garamond', 'Hoefler Text', Georgia, serif";
|
|
14337
14331
|
var STEM = 18;
|
|
14338
14332
|
function StickyLabel({ screenAnchor, action }) {
|
|
14339
14333
|
const { x, y } = screenAnchor;
|
|
@@ -14372,7 +14366,7 @@ function StickyLabel({ screenAnchor, action }) {
|
|
|
14372
14366
|
transition: { duration: 0.35, ease: [0.22, 1, 0.36, 1] },
|
|
14373
14367
|
style: {
|
|
14374
14368
|
position: "absolute",
|
|
14375
|
-
background:
|
|
14369
|
+
background: ACCENT,
|
|
14376
14370
|
transformOrigin: layout.stemOrigin,
|
|
14377
14371
|
...layout.stem
|
|
14378
14372
|
}
|
|
@@ -14391,8 +14385,8 @@ function StickyLabel({ screenAnchor, action }) {
|
|
|
14391
14385
|
width: 6,
|
|
14392
14386
|
height: 6,
|
|
14393
14387
|
borderRadius: "50%",
|
|
14394
|
-
background:
|
|
14395
|
-
boxShadow: `0 0 0 2px ${
|
|
14388
|
+
background: ACCENT,
|
|
14389
|
+
boxShadow: `0 0 0 2px ${PAPER}, 0 0 0 3px rgba(176, 74, 26, 0.25)`,
|
|
14396
14390
|
...layout.dot
|
|
14397
14391
|
}
|
|
14398
14392
|
}
|
|
@@ -14407,25 +14401,25 @@ function StickyLabel({ screenAnchor, action }) {
|
|
|
14407
14401
|
style: {
|
|
14408
14402
|
position: "absolute",
|
|
14409
14403
|
...layout.bodyAnchor,
|
|
14410
|
-
background:
|
|
14411
|
-
color:
|
|
14404
|
+
background: PAPER,
|
|
14405
|
+
color: INK,
|
|
14412
14406
|
border: "1px solid rgba(42, 36, 32, 0.10)",
|
|
14413
14407
|
borderRadius: 3,
|
|
14414
14408
|
padding: "6px 12px 6px 14px",
|
|
14415
|
-
fontFamily:
|
|
14416
|
-
fontSize:
|
|
14409
|
+
fontFamily: SERIF,
|
|
14410
|
+
fontSize: PILL_FONT_BODY,
|
|
14417
14411
|
lineHeight: 1.25,
|
|
14418
14412
|
letterSpacing: 0.6,
|
|
14419
14413
|
textTransform: "uppercase",
|
|
14420
14414
|
fontWeight: 500,
|
|
14421
14415
|
whiteSpace: "nowrap",
|
|
14422
|
-
maxWidth:
|
|
14416
|
+
maxWidth: PILL_MAX_W_BODY,
|
|
14423
14417
|
overflow: "hidden",
|
|
14424
14418
|
textOverflow: "ellipsis",
|
|
14425
14419
|
// Warm two-layer shadow (matches GhostReference's palette).
|
|
14426
14420
|
boxShadow: "0 1px 2px rgba(42, 36, 32, 0.12), 0 8px 18px -6px rgba(42, 36, 32, 0.22)",
|
|
14427
14421
|
// Internal left accent rule — a 2px terracotta stripe.
|
|
14428
|
-
backgroundImage: `linear-gradient(to right, ${
|
|
14422
|
+
backgroundImage: `linear-gradient(to right, ${ACCENT} 0, ${ACCENT} 2px, transparent 2px)`,
|
|
14429
14423
|
backgroundRepeat: "no-repeat",
|
|
14430
14424
|
backgroundSize: "2px 100%",
|
|
14431
14425
|
backgroundPosition: "left top"
|
|
@@ -14667,9 +14661,9 @@ function computePillAnchor(fromBbox, toBbox, page, camera, viewport) {
|
|
|
14667
14661
|
const tipScreenX = viewport.width / 2 + camera.x + (toX - pageCX) * camera.scale;
|
|
14668
14662
|
const tipScreenY = viewport.height / 2 + camera.y + (toY - pageCY) * camera.scale;
|
|
14669
14663
|
const isVertical = Math.abs(dy) >= Math.abs(dx);
|
|
14670
|
-
const OFFSET =
|
|
14671
|
-
const MAX_PILL_W =
|
|
14672
|
-
const MAX_PILL_H =
|
|
14664
|
+
const OFFSET = resolvePillOffset(viewport.width);
|
|
14665
|
+
const MAX_PILL_W = resolveMaxPillW(viewport.width);
|
|
14666
|
+
const MAX_PILL_H = resolveMaxPillH(viewport.width);
|
|
14673
14667
|
const SAFE = 16;
|
|
14674
14668
|
if (isVertical) {
|
|
14675
14669
|
const canFitRight = tipScreenX + OFFSET + MAX_PILL_W < viewport.width - SAFE;
|
|
@@ -14713,13 +14707,13 @@ function CalloutLabelPill({
|
|
|
14713
14707
|
borderRadius: 3,
|
|
14714
14708
|
padding: spec.padding,
|
|
14715
14709
|
fontFamily: SERIF,
|
|
14716
|
-
fontSize:
|
|
14710
|
+
fontSize: PILL_FONT_CAPS,
|
|
14717
14711
|
lineHeight: 1.2,
|
|
14718
14712
|
letterSpacing: 0.6,
|
|
14719
14713
|
textTransform: "uppercase",
|
|
14720
14714
|
fontWeight: 500,
|
|
14721
14715
|
whiteSpace: "nowrap",
|
|
14722
|
-
maxWidth:
|
|
14716
|
+
maxWidth: PILL_MAX_W_CAPS,
|
|
14723
14717
|
overflow: "hidden",
|
|
14724
14718
|
textOverflow: "ellipsis",
|
|
14725
14719
|
boxShadow: "0 1px 2px rgba(42, 36, 32, 0.12), 0 8px 18px -6px rgba(42, 36, 32, 0.22)",
|
|
@@ -15110,7 +15104,12 @@ var StoryboardStepSchema = import_zod.z.object({
|
|
|
15110
15104
|
});
|
|
15111
15105
|
var StoryboardSchema = import_zod.z.object({
|
|
15112
15106
|
version: import_zod.z.literal(1),
|
|
15113
|
-
reasoning
|
|
15107
|
+
// `reasoning` was required in 0.4.x as a model-generated explanation used
|
|
15108
|
+
// by DebugLog. It carries no visual effect and costs 50–150 output tokens
|
|
15109
|
+
// per call, so from 0.5.1 it's optional (default empty). Consumers who
|
|
15110
|
+
// still send it (from cached prompts or older directors) keep working —
|
|
15111
|
+
// the field is still accepted, just not required.
|
|
15112
|
+
reasoning: import_zod.z.string().max(500).optional().default(""),
|
|
15114
15113
|
steps: import_zod.z.array(StoryboardStepSchema).min(1).max(4)
|
|
15115
15114
|
});
|
|
15116
15115
|
function storyboardJsonSchema(opts = {}) {
|
|
@@ -15225,7 +15224,10 @@ function storyboardJsonSchema(opts = {}) {
|
|
|
15225
15224
|
return {
|
|
15226
15225
|
type: "object",
|
|
15227
15226
|
additionalProperties: false,
|
|
15228
|
-
required
|
|
15227
|
+
// `reasoning` intentionally omitted from `required` — the field is still
|
|
15228
|
+
// accepted when present but the model doesn't need to generate it,
|
|
15229
|
+
// which saves 50–150 output tokens per call. See zod schema above.
|
|
15230
|
+
required: ["version", "steps"],
|
|
15229
15231
|
properties: {
|
|
15230
15232
|
version: { type: "integer", enum: [1] },
|
|
15231
15233
|
reasoning: { type: "string" },
|
|
@@ -15264,7 +15266,6 @@ Anchoring rules:
|
|
|
15264
15266
|
Output ONLY this JSON, nothing else:
|
|
15265
15267
|
{
|
|
15266
15268
|
"version": 1,
|
|
15267
|
-
"reasoning": "<which block(s) you picked, which intent you used, and why \u2014 name the block_id>",
|
|
15268
15269
|
"steps": [ { "at_ms": <int>, "duration_ms": <int>, "action": <action> }, ... ]
|
|
15269
15270
|
}
|
|
15270
15271
|
|
|
@@ -15301,7 +15302,6 @@ When narration fits one of these patterns, emit the corresponding storyboard sha
|
|
|
15301
15302
|
Shape: spotlight the term + underline it + drop a label tag. No camera move if the block is already on-screen.
|
|
15302
15303
|
{
|
|
15303
15304
|
"version": 1,
|
|
15304
|
-
"reasoning": "define recipe: spotlighting and underlining the term, labeling as 'definition'",
|
|
15305
15305
|
"steps": [
|
|
15306
15306
|
{ "at_ms":0, "duration_ms":700, "action": { "type":"spotlight", "target_block":"p1_para0", "dim_opacity":0.6, "feather_px":40, "shape":"rounded" } },
|
|
15307
15307
|
{ "at_ms":200, "duration_ms":800, "action": { "type":"underline", "target_block":"p1_para0", "color":"#FBBF24", "style":"sketch", "draw_duration_ms":700 } },
|
|
@@ -15313,7 +15313,6 @@ Shape: spotlight the term + underline it + drop a label tag. No camera move if t
|
|
|
15313
15313
|
Shape: gentle camera move + callout arrow from caption to figure + pulse the figure.
|
|
15314
15314
|
{
|
|
15315
15315
|
"version": 1,
|
|
15316
|
-
"reasoning": "point_out recipe: drawing attention from caption p1_cap1 to figure p1_fig0",
|
|
15317
15316
|
"steps": [
|
|
15318
15317
|
{ "at_ms":0, "duration_ms":600, "action": { "type":"camera", "target_block":"p1_fig0", "scale":1.3, "padding":80, "easing":"ease-out" } },
|
|
15319
15318
|
{ "at_ms":400, "duration_ms":900, "action": { "type":"callout", "from_block":"p1_cap1", "to_block":"p1_fig0", "label":"see here", "curve":"curved" } },
|
|
@@ -15325,7 +15324,6 @@ Shape: gentle camera move + callout arrow from caption to figure + pulse the fig
|
|
|
15325
15324
|
Shape: box A + box B + callout between them with a relational label.
|
|
15326
15325
|
{
|
|
15327
15326
|
"version": 1,
|
|
15328
|
-
"reasoning": "compare recipe: framing fibrous vs synovial joints",
|
|
15329
15327
|
"steps": [
|
|
15330
15328
|
{ "at_ms":0, "duration_ms":600, "action": { "type":"box", "target_block":"p1_list5", "color":"#3B82F6", "style":"solid" } },
|
|
15331
15329
|
{ "at_ms":300, "duration_ms":600, "action": { "type":"box", "target_block":"p1_list12", "color":"#F472B6", "style":"solid" } },
|
|
@@ -15337,7 +15335,6 @@ Shape: box A + box B + callout between them with a relational label.
|
|
|
15337
15335
|
Shape: highlight + pulse. Fast, punchy, no camera.
|
|
15338
15336
|
{
|
|
15339
15337
|
"version": 1,
|
|
15340
|
-
"reasoning": "emphasize recipe: highlighting key keyword and pulsing for stress",
|
|
15341
15338
|
"steps": [
|
|
15342
15339
|
{ "at_ms":0, "duration_ms":500, "action": { "type":"highlight", "target_block":"p1_list0", "color":"rgba(250,204,21,0.35)", "draw_duration_ms":450 } },
|
|
15343
15340
|
{ "at_ms":350, "duration_ms":800, "action": { "type":"pulse", "target_block":"p1_list0", "count":2, "intensity":"strong" } }
|
|
@@ -15606,32 +15603,32 @@ function clampNumericRanges(input) {
|
|
|
15606
15603
|
}
|
|
15607
15604
|
const type = typeof out.type === "string" ? out.type : void 0;
|
|
15608
15605
|
if (type === "camera") {
|
|
15609
|
-
if (typeof out.scale === "number") out.scale =
|
|
15606
|
+
if (typeof out.scale === "number") out.scale = clamp2(out.scale, 0.5, 4);
|
|
15610
15607
|
if (typeof out.padding === "number") {
|
|
15611
|
-
out.padding =
|
|
15608
|
+
out.padding = clamp2(out.padding, 0, 400);
|
|
15612
15609
|
}
|
|
15613
15610
|
}
|
|
15614
15611
|
if (typeof out.dim_opacity === "number") {
|
|
15615
|
-
out.dim_opacity =
|
|
15612
|
+
out.dim_opacity = clamp2(out.dim_opacity, 0, 1);
|
|
15616
15613
|
}
|
|
15617
15614
|
if (typeof out.feather_px === "number") {
|
|
15618
|
-
out.feather_px =
|
|
15615
|
+
out.feather_px = clamp2(out.feather_px, 0, 200);
|
|
15619
15616
|
}
|
|
15620
15617
|
if (typeof out.draw_duration_ms === "number") {
|
|
15621
|
-
out.draw_duration_ms =
|
|
15618
|
+
out.draw_duration_ms = clamp2(out.draw_duration_ms, 100, 3e3);
|
|
15622
15619
|
}
|
|
15623
15620
|
if (typeof out.count === "number") {
|
|
15624
|
-
out.count = Math.round(
|
|
15621
|
+
out.count = Math.round(clamp2(out.count, 1, 5));
|
|
15625
15622
|
}
|
|
15626
15623
|
if (typeof out.at_ms === "number") {
|
|
15627
|
-
out.at_ms =
|
|
15624
|
+
out.at_ms = clamp2(out.at_ms, 0, 5e3);
|
|
15628
15625
|
}
|
|
15629
15626
|
if (typeof out.duration_ms === "number" && type === void 0) {
|
|
15630
|
-
out.duration_ms =
|
|
15627
|
+
out.duration_ms = clamp2(out.duration_ms, 100, 5e3);
|
|
15631
15628
|
}
|
|
15632
15629
|
return out;
|
|
15633
15630
|
}
|
|
15634
|
-
function
|
|
15631
|
+
function clamp2(v, lo, hi) {
|
|
15635
15632
|
return Math.min(hi, Math.max(lo, v));
|
|
15636
15633
|
}
|
|
15637
15634
|
function enforceOverlayPresence(sb) {
|
|
@@ -15644,9 +15641,10 @@ function enforceOverlayPresence(sb) {
|
|
|
15644
15641
|
if (!cameraStep || cameraStep.action.type !== "camera") return sb;
|
|
15645
15642
|
const target = cameraStep.action.target_block;
|
|
15646
15643
|
if (!target) return sb;
|
|
15644
|
+
const prefix = sb.reasoning ? `${sb.reasoning} ` : "";
|
|
15647
15645
|
return {
|
|
15648
15646
|
...sb,
|
|
15649
|
-
reasoning: `${
|
|
15647
|
+
reasoning: `${prefix}[auto-appended pulse: camera-only storyboards are forbidden]`,
|
|
15650
15648
|
steps: [
|
|
15651
15649
|
...sb.steps,
|
|
15652
15650
|
{
|
|
@@ -16038,6 +16036,7 @@ function TutorModeContainer({
|
|
|
16038
16036
|
backgroundColor = "#ffffff",
|
|
16039
16037
|
loadingComponent,
|
|
16040
16038
|
onPageChange,
|
|
16039
|
+
storyboardProvider,
|
|
16041
16040
|
className
|
|
16042
16041
|
}) {
|
|
16043
16042
|
const containerRef = (0, import_react58.useRef)(null);
|
|
@@ -16116,7 +16115,7 @@ function TutorModeContainer({
|
|
|
16116
16115
|
const debounceRef = (0, import_react58.useRef)(null);
|
|
16117
16116
|
const lastChunkRef = (0, import_react58.useRef)(null);
|
|
16118
16117
|
(0, import_react58.useEffect)(() => {
|
|
16119
|
-
if (!llm) return;
|
|
16118
|
+
if (!storyboardProvider && !llm) return;
|
|
16120
16119
|
if (!currentChunk || currentChunk === lastChunkRef.current) return;
|
|
16121
16120
|
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
16122
16121
|
debounceRef.current = setTimeout(async () => {
|
|
@@ -16137,6 +16136,75 @@ function TutorModeContainer({
|
|
|
16137
16136
|
});
|
|
16138
16137
|
abortRef.current?.abort();
|
|
16139
16138
|
abortRef.current = new AbortController();
|
|
16139
|
+
if (storyboardProvider) {
|
|
16140
|
+
narrationStore.getState().setLlmStatus("in-flight");
|
|
16141
|
+
narrationStore.getState().appendDebugEvent({
|
|
16142
|
+
kind: "llm-request",
|
|
16143
|
+
summary: `provider (page ${pageNumber}, ${page2.blocks.length} blocks)`,
|
|
16144
|
+
payload: {
|
|
16145
|
+
via: "storyboardProvider",
|
|
16146
|
+
pageNumber,
|
|
16147
|
+
blockCount: page2.blocks.length
|
|
16148
|
+
}
|
|
16149
|
+
});
|
|
16150
|
+
try {
|
|
16151
|
+
const raw = await storyboardProvider({
|
|
16152
|
+
chunk,
|
|
16153
|
+
pageNumber,
|
|
16154
|
+
page: page2,
|
|
16155
|
+
history: narrationStore.getState().chunkHistory,
|
|
16156
|
+
signal: abortRef.current.signal
|
|
16157
|
+
});
|
|
16158
|
+
if (!raw) {
|
|
16159
|
+
narrationStore.getState().setLlmStatus("idle");
|
|
16160
|
+
narrationStore.getState().appendDebugEvent({
|
|
16161
|
+
kind: "note",
|
|
16162
|
+
summary: "provider returned null \u2014 no storyboard for this chunk"
|
|
16163
|
+
});
|
|
16164
|
+
return;
|
|
16165
|
+
}
|
|
16166
|
+
const parsed = StoryboardSchema.safeParse(raw);
|
|
16167
|
+
if (!parsed.success) {
|
|
16168
|
+
narrationStore.getState().setLlmStatus(
|
|
16169
|
+
"failed",
|
|
16170
|
+
parsed.error.message
|
|
16171
|
+
);
|
|
16172
|
+
narrationStore.getState().appendDebugEvent({
|
|
16173
|
+
kind: "llm-error",
|
|
16174
|
+
summary: `provider storyboard rejected by schema: ${parsed.error.issues[0]?.message ?? "unknown"}`,
|
|
16175
|
+
payload: { raw, error: parsed.error.message }
|
|
16176
|
+
});
|
|
16177
|
+
return;
|
|
16178
|
+
}
|
|
16179
|
+
const storyboard = parsed.data;
|
|
16180
|
+
narrationStore.getState().setLlmStatus("idle");
|
|
16181
|
+
narrationStore.getState().appendDebugEvent({
|
|
16182
|
+
kind: "llm-response",
|
|
16183
|
+
summary: summariseStoryboard(storyboard),
|
|
16184
|
+
payload: { via: "storyboardProvider", storyboard }
|
|
16185
|
+
});
|
|
16186
|
+
engineRef.current?.execute(storyboard);
|
|
16187
|
+
narrationStore.getState().appendDebugEvent({
|
|
16188
|
+
kind: "storyboard-execute",
|
|
16189
|
+
summary: `engine executing ${storyboard.steps.length} steps`,
|
|
16190
|
+
payload: storyboard.steps.map((s) => ({
|
|
16191
|
+
at_ms: s.at_ms,
|
|
16192
|
+
type: s.action.type,
|
|
16193
|
+
target: "target_block" in s.action ? s.action.target_block : void 0
|
|
16194
|
+
}))
|
|
16195
|
+
});
|
|
16196
|
+
} catch (e) {
|
|
16197
|
+
if (e.name === "AbortError") return;
|
|
16198
|
+
narrationStore.getState().setLlmStatus("failed", e.message);
|
|
16199
|
+
narrationStore.getState().appendDebugEvent({
|
|
16200
|
+
kind: "llm-error",
|
|
16201
|
+
summary: `provider threw: ${e.message.slice(0, 80)}`,
|
|
16202
|
+
payload: e
|
|
16203
|
+
});
|
|
16204
|
+
}
|
|
16205
|
+
return;
|
|
16206
|
+
}
|
|
16207
|
+
if (!llm) return;
|
|
16140
16208
|
narrationStore.getState().setLlmStatus("in-flight");
|
|
16141
16209
|
narrationStore.getState().appendDebugEvent({
|
|
16142
16210
|
kind: "llm-request",
|
|
@@ -16158,7 +16226,7 @@ function TutorModeContainer({
|
|
|
16158
16226
|
narrationStore.getState().setLlmStatus("idle");
|
|
16159
16227
|
narrationStore.getState().appendDebugEvent({
|
|
16160
16228
|
kind: "llm-response",
|
|
16161
|
-
summary:
|
|
16229
|
+
summary: summariseStoryboard(result.storyboard),
|
|
16162
16230
|
payload: { raw: result.raw, storyboard: result.storyboard }
|
|
16163
16231
|
});
|
|
16164
16232
|
engineRef.current?.execute(result.storyboard);
|
|
@@ -16201,7 +16269,16 @@ function TutorModeContainer({
|
|
|
16201
16269
|
return () => {
|
|
16202
16270
|
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
16203
16271
|
};
|
|
16204
|
-
}, [
|
|
16272
|
+
}, [
|
|
16273
|
+
currentChunk,
|
|
16274
|
+
llm,
|
|
16275
|
+
storyboardProvider,
|
|
16276
|
+
index,
|
|
16277
|
+
pageNumber,
|
|
16278
|
+
narrationStore,
|
|
16279
|
+
embeddingProvider,
|
|
16280
|
+
llmTimeoutMs
|
|
16281
|
+
]);
|
|
16205
16282
|
(0, import_react58.useEffect)(() => {
|
|
16206
16283
|
if (!currentChunk) return;
|
|
16207
16284
|
const t = setTimeout(() => {
|
|
@@ -16391,6 +16468,15 @@ function TutorLoadingState({
|
|
|
16391
16468
|
}
|
|
16392
16469
|
);
|
|
16393
16470
|
}
|
|
16471
|
+
function summariseStoryboard(sb) {
|
|
16472
|
+
const stepCount = sb.steps.length;
|
|
16473
|
+
const trimmedReasoning = (sb.reasoning ?? "").trim();
|
|
16474
|
+
if (trimmedReasoning) {
|
|
16475
|
+
return `storyboard \u2713 ${stepCount} steps \u2014 ${trimmedReasoning.slice(0, 60)}`;
|
|
16476
|
+
}
|
|
16477
|
+
const kinds = sb.steps.map((s) => s.action.type).join(" \u2192 ");
|
|
16478
|
+
return `storyboard \u2713 ${stepCount} steps \u2014 ${kinds}`;
|
|
16479
|
+
}
|
|
16394
16480
|
|
|
16395
16481
|
// src/director/transformers-embedding.ts
|
|
16396
16482
|
var loaded = null;
|