pdfjs-reader-core 0.4.2 → 0.5.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 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,8 +13181,6 @@ 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];
13188
13186
 
@@ -13470,17 +13468,17 @@ function AnimatedUnderline({ bbox, action }) {
13470
13468
  var import_react56 = require("react");
13471
13469
  var import_framer_motion4 = require("framer-motion");
13472
13470
  var import_jsx_runtime44 = require("react/jsx-runtime");
13471
+ var WASH = "rgba(230, 180, 34, 0.22)";
13473
13472
  function AnimatedHighlight({ bbox, action }) {
13474
13473
  const [x1, y1, x2, y2] = bbox;
13475
13474
  const h = Math.max(1, y2 - y1);
13476
13475
  const bleed = Math.min(4, h * 0.12);
13477
13476
  const yTop = y1 - bleed;
13478
13477
  const yBot = y2 + bleed;
13479
- const height = yBot - yTop;
13480
13478
  const duration = action.draw_duration_ms / 1e3;
13481
13479
  const filterId = (0, import_react56.useId)();
13482
- const fill = action.color && action.color !== "rgba(250, 204, 21, 0.35)" && action.color !== "rgba(250,204,21,0.35)" ? action.color : MARKER_SOFT;
13483
- const inner = action.color && action.color !== "rgba(250, 204, 21, 0.35)" && action.color !== "rgba(250,204,21,0.35)" ? action.color : MARKER;
13480
+ const isDefaultColour = !action.color || action.color === "rgba(250, 204, 21, 0.35)" || action.color === "rgba(250,204,21,0.35)";
13481
+ const fill = isDefaultColour ? WASH : action.color;
13484
13482
  const taper = Math.min(6, h * 0.2);
13485
13483
  const pathD = `
13486
13484
  M ${x1 - 2} ${yTop + taper}
@@ -13510,44 +13508,24 @@ function AnimatedHighlight({ bbox, action }) {
13510
13508
  "feTurbulence",
13511
13509
  {
13512
13510
  type: "fractalNoise",
13513
- baseFrequency: "1.6",
13511
+ baseFrequency: "1.8",
13514
13512
  numOctaves: "1",
13515
13513
  seed: 3,
13516
13514
  result: "noise"
13517
13515
  }
13518
13516
  ),
13519
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("feDisplacementMap", { in: "SourceGraphic", in2: "noise", scale: 1.4 })
13517
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("feDisplacementMap", { in: "SourceGraphic", in2: "noise", scale: 1 })
13520
13518
  ] }) }),
13521
- /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)(
13522
- import_framer_motion4.motion.g,
13519
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
13520
+ import_framer_motion4.motion.path,
13523
13521
  {
13522
+ d: pathD,
13523
+ fill,
13524
13524
  initial: { clipPath: `inset(0 100% 0 0)` },
13525
13525
  animate: { clipPath: `inset(0 0% 0 0)` },
13526
13526
  exit: { opacity: 0 },
13527
- transition: { duration, ease: EASE_OUT_EXPO },
13528
- children: [
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
- ]
13527
+ filter: `url(#${filterId})`,
13528
+ transition: { duration, ease: EASE_OUT_EXPO }
13551
13529
  }
13552
13530
  )
13553
13531
  ]
@@ -15110,7 +15088,12 @@ var StoryboardStepSchema = import_zod.z.object({
15110
15088
  });
15111
15089
  var StoryboardSchema = import_zod.z.object({
15112
15090
  version: import_zod.z.literal(1),
15113
- reasoning: import_zod.z.string().max(500).default(""),
15091
+ // `reasoning` was required in 0.4.x as a model-generated explanation used
15092
+ // by DebugLog. It carries no visual effect and costs 50–150 output tokens
15093
+ // per call, so from 0.5.1 it's optional (default empty). Consumers who
15094
+ // still send it (from cached prompts or older directors) keep working —
15095
+ // the field is still accepted, just not required.
15096
+ reasoning: import_zod.z.string().max(500).optional().default(""),
15114
15097
  steps: import_zod.z.array(StoryboardStepSchema).min(1).max(4)
15115
15098
  });
15116
15099
  function storyboardJsonSchema(opts = {}) {
@@ -15225,7 +15208,10 @@ function storyboardJsonSchema(opts = {}) {
15225
15208
  return {
15226
15209
  type: "object",
15227
15210
  additionalProperties: false,
15228
- required: ["version", "reasoning", "steps"],
15211
+ // `reasoning` intentionally omitted from `required` the field is still
15212
+ // accepted when present but the model doesn't need to generate it,
15213
+ // which saves 50–150 output tokens per call. See zod schema above.
15214
+ required: ["version", "steps"],
15229
15215
  properties: {
15230
15216
  version: { type: "integer", enum: [1] },
15231
15217
  reasoning: { type: "string" },
@@ -15264,7 +15250,6 @@ Anchoring rules:
15264
15250
  Output ONLY this JSON, nothing else:
15265
15251
  {
15266
15252
  "version": 1,
15267
- "reasoning": "<which block(s) you picked, which intent you used, and why \u2014 name the block_id>",
15268
15253
  "steps": [ { "at_ms": <int>, "duration_ms": <int>, "action": <action> }, ... ]
15269
15254
  }
15270
15255
 
@@ -15301,7 +15286,6 @@ When narration fits one of these patterns, emit the corresponding storyboard sha
15301
15286
  Shape: spotlight the term + underline it + drop a label tag. No camera move if the block is already on-screen.
15302
15287
  {
15303
15288
  "version": 1,
15304
- "reasoning": "define recipe: spotlighting and underlining the term, labeling as 'definition'",
15305
15289
  "steps": [
15306
15290
  { "at_ms":0, "duration_ms":700, "action": { "type":"spotlight", "target_block":"p1_para0", "dim_opacity":0.6, "feather_px":40, "shape":"rounded" } },
15307
15291
  { "at_ms":200, "duration_ms":800, "action": { "type":"underline", "target_block":"p1_para0", "color":"#FBBF24", "style":"sketch", "draw_duration_ms":700 } },
@@ -15313,7 +15297,6 @@ Shape: spotlight the term + underline it + drop a label tag. No camera move if t
15313
15297
  Shape: gentle camera move + callout arrow from caption to figure + pulse the figure.
15314
15298
  {
15315
15299
  "version": 1,
15316
- "reasoning": "point_out recipe: drawing attention from caption p1_cap1 to figure p1_fig0",
15317
15300
  "steps": [
15318
15301
  { "at_ms":0, "duration_ms":600, "action": { "type":"camera", "target_block":"p1_fig0", "scale":1.3, "padding":80, "easing":"ease-out" } },
15319
15302
  { "at_ms":400, "duration_ms":900, "action": { "type":"callout", "from_block":"p1_cap1", "to_block":"p1_fig0", "label":"see here", "curve":"curved" } },
@@ -15325,7 +15308,6 @@ Shape: gentle camera move + callout arrow from caption to figure + pulse the fig
15325
15308
  Shape: box A + box B + callout between them with a relational label.
15326
15309
  {
15327
15310
  "version": 1,
15328
- "reasoning": "compare recipe: framing fibrous vs synovial joints",
15329
15311
  "steps": [
15330
15312
  { "at_ms":0, "duration_ms":600, "action": { "type":"box", "target_block":"p1_list5", "color":"#3B82F6", "style":"solid" } },
15331
15313
  { "at_ms":300, "duration_ms":600, "action": { "type":"box", "target_block":"p1_list12", "color":"#F472B6", "style":"solid" } },
@@ -15337,7 +15319,6 @@ Shape: box A + box B + callout between them with a relational label.
15337
15319
  Shape: highlight + pulse. Fast, punchy, no camera.
15338
15320
  {
15339
15321
  "version": 1,
15340
- "reasoning": "emphasize recipe: highlighting key keyword and pulsing for stress",
15341
15322
  "steps": [
15342
15323
  { "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
15324
  { "at_ms":350, "duration_ms":800, "action": { "type":"pulse", "target_block":"p1_list0", "count":2, "intensity":"strong" } }
@@ -15644,9 +15625,10 @@ function enforceOverlayPresence(sb) {
15644
15625
  if (!cameraStep || cameraStep.action.type !== "camera") return sb;
15645
15626
  const target = cameraStep.action.target_block;
15646
15627
  if (!target) return sb;
15628
+ const prefix = sb.reasoning ? `${sb.reasoning} ` : "";
15647
15629
  return {
15648
15630
  ...sb,
15649
- reasoning: `${sb.reasoning} [auto-appended pulse: camera-only storyboards are forbidden]`,
15631
+ reasoning: `${prefix}[auto-appended pulse: camera-only storyboards are forbidden]`,
15650
15632
  steps: [
15651
15633
  ...sb.steps,
15652
15634
  {
@@ -16038,6 +16020,7 @@ function TutorModeContainer({
16038
16020
  backgroundColor = "#ffffff",
16039
16021
  loadingComponent,
16040
16022
  onPageChange,
16023
+ storyboardProvider,
16041
16024
  className
16042
16025
  }) {
16043
16026
  const containerRef = (0, import_react58.useRef)(null);
@@ -16116,7 +16099,7 @@ function TutorModeContainer({
16116
16099
  const debounceRef = (0, import_react58.useRef)(null);
16117
16100
  const lastChunkRef = (0, import_react58.useRef)(null);
16118
16101
  (0, import_react58.useEffect)(() => {
16119
- if (!llm) return;
16102
+ if (!storyboardProvider && !llm) return;
16120
16103
  if (!currentChunk || currentChunk === lastChunkRef.current) return;
16121
16104
  if (debounceRef.current) clearTimeout(debounceRef.current);
16122
16105
  debounceRef.current = setTimeout(async () => {
@@ -16137,6 +16120,75 @@ function TutorModeContainer({
16137
16120
  });
16138
16121
  abortRef.current?.abort();
16139
16122
  abortRef.current = new AbortController();
16123
+ if (storyboardProvider) {
16124
+ narrationStore.getState().setLlmStatus("in-flight");
16125
+ narrationStore.getState().appendDebugEvent({
16126
+ kind: "llm-request",
16127
+ summary: `provider (page ${pageNumber}, ${page2.blocks.length} blocks)`,
16128
+ payload: {
16129
+ via: "storyboardProvider",
16130
+ pageNumber,
16131
+ blockCount: page2.blocks.length
16132
+ }
16133
+ });
16134
+ try {
16135
+ const raw = await storyboardProvider({
16136
+ chunk,
16137
+ pageNumber,
16138
+ page: page2,
16139
+ history: narrationStore.getState().chunkHistory,
16140
+ signal: abortRef.current.signal
16141
+ });
16142
+ if (!raw) {
16143
+ narrationStore.getState().setLlmStatus("idle");
16144
+ narrationStore.getState().appendDebugEvent({
16145
+ kind: "note",
16146
+ summary: "provider returned null \u2014 no storyboard for this chunk"
16147
+ });
16148
+ return;
16149
+ }
16150
+ const parsed = StoryboardSchema.safeParse(raw);
16151
+ if (!parsed.success) {
16152
+ narrationStore.getState().setLlmStatus(
16153
+ "failed",
16154
+ parsed.error.message
16155
+ );
16156
+ narrationStore.getState().appendDebugEvent({
16157
+ kind: "llm-error",
16158
+ summary: `provider storyboard rejected by schema: ${parsed.error.issues[0]?.message ?? "unknown"}`,
16159
+ payload: { raw, error: parsed.error.message }
16160
+ });
16161
+ return;
16162
+ }
16163
+ const storyboard = parsed.data;
16164
+ narrationStore.getState().setLlmStatus("idle");
16165
+ narrationStore.getState().appendDebugEvent({
16166
+ kind: "llm-response",
16167
+ summary: summariseStoryboard(storyboard),
16168
+ payload: { via: "storyboardProvider", storyboard }
16169
+ });
16170
+ engineRef.current?.execute(storyboard);
16171
+ narrationStore.getState().appendDebugEvent({
16172
+ kind: "storyboard-execute",
16173
+ summary: `engine executing ${storyboard.steps.length} steps`,
16174
+ payload: storyboard.steps.map((s) => ({
16175
+ at_ms: s.at_ms,
16176
+ type: s.action.type,
16177
+ target: "target_block" in s.action ? s.action.target_block : void 0
16178
+ }))
16179
+ });
16180
+ } catch (e) {
16181
+ if (e.name === "AbortError") return;
16182
+ narrationStore.getState().setLlmStatus("failed", e.message);
16183
+ narrationStore.getState().appendDebugEvent({
16184
+ kind: "llm-error",
16185
+ summary: `provider threw: ${e.message.slice(0, 80)}`,
16186
+ payload: e
16187
+ });
16188
+ }
16189
+ return;
16190
+ }
16191
+ if (!llm) return;
16140
16192
  narrationStore.getState().setLlmStatus("in-flight");
16141
16193
  narrationStore.getState().appendDebugEvent({
16142
16194
  kind: "llm-request",
@@ -16158,7 +16210,7 @@ function TutorModeContainer({
16158
16210
  narrationStore.getState().setLlmStatus("idle");
16159
16211
  narrationStore.getState().appendDebugEvent({
16160
16212
  kind: "llm-response",
16161
- summary: `storyboard \u2713 ${result.storyboard.steps.length} steps \u2014 ${result.storyboard.reasoning.slice(0, 60)}`,
16213
+ summary: summariseStoryboard(result.storyboard),
16162
16214
  payload: { raw: result.raw, storyboard: result.storyboard }
16163
16215
  });
16164
16216
  engineRef.current?.execute(result.storyboard);
@@ -16201,7 +16253,16 @@ function TutorModeContainer({
16201
16253
  return () => {
16202
16254
  if (debounceRef.current) clearTimeout(debounceRef.current);
16203
16255
  };
16204
- }, [currentChunk, llm, index, pageNumber, narrationStore, embeddingProvider, llmTimeoutMs]);
16256
+ }, [
16257
+ currentChunk,
16258
+ llm,
16259
+ storyboardProvider,
16260
+ index,
16261
+ pageNumber,
16262
+ narrationStore,
16263
+ embeddingProvider,
16264
+ llmTimeoutMs
16265
+ ]);
16205
16266
  (0, import_react58.useEffect)(() => {
16206
16267
  if (!currentChunk) return;
16207
16268
  const t = setTimeout(() => {
@@ -16391,6 +16452,15 @@ function TutorLoadingState({
16391
16452
  }
16392
16453
  );
16393
16454
  }
16455
+ function summariseStoryboard(sb) {
16456
+ const stepCount = sb.steps.length;
16457
+ const trimmedReasoning = (sb.reasoning ?? "").trim();
16458
+ if (trimmedReasoning) {
16459
+ return `storyboard \u2713 ${stepCount} steps \u2014 ${trimmedReasoning.slice(0, 60)}`;
16460
+ }
16461
+ const kinds = sb.steps.map((s) => s.action.type).join(" \u2192 ");
16462
+ return `storyboard \u2713 ${stepCount} steps \u2014 ${kinds}`;
16463
+ }
16394
16464
 
16395
16465
  // src/director/transformers-embedding.ts
16396
16466
  var loaded = null;