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/dist/index.js CHANGED
@@ -12996,10 +12996,26 @@ var PAPER = "#faf6ec";
12996
12996
  var ACCENT = "#b04a1a";
12997
12997
  var ACCENT_SOFT = "rgba(176, 74, 26, 0.18)";
12998
12998
  var ACCENT_GLOW = "rgba(176, 74, 26, 0.35)";
12999
- var MARKER = "#e6b422";
13000
- var MARKER_SOFT = "rgba(230, 180, 34, 0.38)";
13001
12999
  var SERIF = "'Iowan Old Style', 'Palatino Linotype', Palatino, 'Book Antiqua', 'EB Garamond', 'Hoefler Text', Georgia, serif";
13002
13000
  var EASE_OUT_EXPO = [0.22, 1, 0.36, 1];
13001
+ var PILL_FONT_CAPS = "clamp(10.5px, 0.55vw + 8.5px, 14.5px)";
13002
+ var PILL_FONT_BODY = "clamp(12px, 0.6vw + 10px, 16.5px)";
13003
+ var PILL_FONT_DISPLAY = "clamp(14px, 0.75vw + 12px, 19px)";
13004
+ var PILL_MAX_W_CAPS = "clamp(180px, 26vw, 380px)";
13005
+ var PILL_MAX_W_BODY = "clamp(200px, 28vw, 440px)";
13006
+ function resolvePillOffset(viewportWidthPx) {
13007
+ return clamp(0.022 * viewportWidthPx + 12, 20, 44);
13008
+ }
13009
+ function resolveMaxPillW(viewportWidthPx) {
13010
+ return clamp(0.26 * viewportWidthPx, 180, 380);
13011
+ }
13012
+ function resolveMaxPillH(viewportWidthPx) {
13013
+ const font = clamp(55e-4 * viewportWidthPx + 8.5, 10.5, 14.5);
13014
+ return clamp(font * 2.6, 28, 42);
13015
+ }
13016
+ function clamp(v, lo, hi) {
13017
+ return Math.min(hi, Math.max(lo, v));
13018
+ }
13003
13019
 
13004
13020
  // src/components/TutorMode/SpotlightMask.tsx
13005
13021
  import { jsx as jsx42, jsxs as jsxs35 } from "react/jsx-runtime";
@@ -13285,17 +13301,17 @@ function AnimatedUnderline({ bbox, action }) {
13285
13301
  import { useId as useId2 } from "react";
13286
13302
  import { motion as motion4 } from "framer-motion";
13287
13303
  import { jsx as jsx44, jsxs as jsxs37 } from "react/jsx-runtime";
13304
+ var WASH = "rgba(230, 180, 34, 0.22)";
13288
13305
  function AnimatedHighlight({ bbox, action }) {
13289
13306
  const [x1, y1, x2, y2] = bbox;
13290
13307
  const h = Math.max(1, y2 - y1);
13291
13308
  const bleed = Math.min(4, h * 0.12);
13292
13309
  const yTop = y1 - bleed;
13293
13310
  const yBot = y2 + bleed;
13294
- const height = yBot - yTop;
13295
13311
  const duration = action.draw_duration_ms / 1e3;
13296
13312
  const filterId = useId2();
13297
- const fill = action.color && action.color !== "rgba(250, 204, 21, 0.35)" && action.color !== "rgba(250,204,21,0.35)" ? action.color : MARKER_SOFT;
13298
- const inner = action.color && action.color !== "rgba(250, 204, 21, 0.35)" && action.color !== "rgba(250,204,21,0.35)" ? action.color : MARKER;
13313
+ const isDefaultColour = !action.color || action.color === "rgba(250, 204, 21, 0.35)" || action.color === "rgba(250,204,21,0.35)";
13314
+ const fill = isDefaultColour ? WASH : action.color;
13299
13315
  const taper = Math.min(6, h * 0.2);
13300
13316
  const pathD = `
13301
13317
  M ${x1 - 2} ${yTop + taper}
@@ -13325,44 +13341,24 @@ function AnimatedHighlight({ bbox, action }) {
13325
13341
  "feTurbulence",
13326
13342
  {
13327
13343
  type: "fractalNoise",
13328
- baseFrequency: "1.6",
13344
+ baseFrequency: "1.8",
13329
13345
  numOctaves: "1",
13330
13346
  seed: 3,
13331
13347
  result: "noise"
13332
13348
  }
13333
13349
  ),
13334
- /* @__PURE__ */ jsx44("feDisplacementMap", { in: "SourceGraphic", in2: "noise", scale: 1.4 })
13350
+ /* @__PURE__ */ jsx44("feDisplacementMap", { in: "SourceGraphic", in2: "noise", scale: 1 })
13335
13351
  ] }) }),
13336
- /* @__PURE__ */ jsxs37(
13337
- motion4.g,
13352
+ /* @__PURE__ */ jsx44(
13353
+ motion4.path,
13338
13354
  {
13355
+ d: pathD,
13356
+ fill,
13339
13357
  initial: { clipPath: `inset(0 100% 0 0)` },
13340
13358
  animate: { clipPath: `inset(0 0% 0 0)` },
13341
13359
  exit: { opacity: 0 },
13342
- transition: { duration, ease: EASE_OUT_EXPO },
13343
- children: [
13344
- /* @__PURE__ */ jsx44(
13345
- "path",
13346
- {
13347
- d: pathD,
13348
- fill,
13349
- opacity: 0.85,
13350
- filter: `url(#${filterId})`
13351
- }
13352
- ),
13353
- /* @__PURE__ */ jsx44(
13354
- "rect",
13355
- {
13356
- x: x1 - 1,
13357
- y: y1 - bleed * 0.4,
13358
- width: x2 - x1 + 2,
13359
- height: height - bleed * 0.8,
13360
- fill: inner,
13361
- opacity: 0.5,
13362
- filter: `url(#${filterId})`
13363
- }
13364
- )
13365
- ]
13360
+ filter: `url(#${filterId})`,
13361
+ transition: { duration, ease: EASE_OUT_EXPO }
13366
13362
  }
13367
13363
  )
13368
13364
  ]
@@ -13932,9 +13928,11 @@ function GhostReference({
13932
13928
  {
13933
13929
  style: {
13934
13930
  position: "relative",
13935
- padding: "20px 22px 20px 26px",
13931
+ // Padding scales with viewport: compact on phones, breathing on
13932
+ // desktop. Extra 4px on the left preserves the footnote-rule gap.
13933
+ 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)",
13936
13934
  display: "flex",
13937
- gap: 16,
13935
+ gap: "clamp(12px, 1.1vw + 8px, 20px)",
13938
13936
  alignItems: "flex-start"
13939
13937
  },
13940
13938
  children: [
@@ -13948,7 +13946,7 @@ function GhostReference({
13948
13946
  transition: { duration: 0.45, delay: 0.18, ease: [0.22, 1, 0.36, 1] },
13949
13947
  style: {
13950
13948
  flexShrink: 0,
13951
- width: 46,
13949
+ width: "clamp(40px, 4vw + 20px, 64px)",
13952
13950
  aspectRatio: `${page.width} / ${page.height}`,
13953
13951
  background: PAPER_DEEP,
13954
13952
  borderRadius: 2,
@@ -14013,7 +14011,7 @@ function GhostReference({
14013
14011
  flex: 1,
14014
14012
  minWidth: 0,
14015
14013
  fontFamily: SERIF2,
14016
- fontSize: 15.5,
14014
+ fontSize: PILL_FONT_DISPLAY,
14017
14015
  lineHeight: 1.55,
14018
14016
  color: INK2,
14019
14017
  fontFeatureSettings: "'liga' 1, 'kern' 1, 'onum' 1",
@@ -14035,7 +14033,7 @@ function GhostReference({
14035
14033
  left: -14,
14036
14034
  top: -2,
14037
14035
  color: ACCENT2,
14038
- fontSize: 22,
14036
+ fontSize: "clamp(18px, 1vw + 14px, 28px)",
14039
14037
  lineHeight: 1,
14040
14038
  fontWeight: 500
14041
14039
  // ornamental flourish anchoring the paragraph
@@ -14145,10 +14143,6 @@ import { AnimatePresence as AnimatePresence3 } from "framer-motion";
14145
14143
  // src/components/TutorMode/StickyLabel.tsx
14146
14144
  import { motion as motion9 } from "framer-motion";
14147
14145
  import { jsx as jsx51, jsxs as jsxs41 } from "react/jsx-runtime";
14148
- var INK3 = "#2a2420";
14149
- var PAPER3 = "#faf6ec";
14150
- var ACCENT3 = "#b04a1a";
14151
- var SERIF3 = "'Iowan Old Style', 'Palatino Linotype', Palatino, 'Book Antiqua', 'EB Garamond', 'Hoefler Text', Georgia, serif";
14152
14146
  var STEM = 18;
14153
14147
  function StickyLabel({ screenAnchor, action }) {
14154
14148
  const { x, y } = screenAnchor;
@@ -14187,7 +14181,7 @@ function StickyLabel({ screenAnchor, action }) {
14187
14181
  transition: { duration: 0.35, ease: [0.22, 1, 0.36, 1] },
14188
14182
  style: {
14189
14183
  position: "absolute",
14190
- background: ACCENT3,
14184
+ background: ACCENT,
14191
14185
  transformOrigin: layout.stemOrigin,
14192
14186
  ...layout.stem
14193
14187
  }
@@ -14206,8 +14200,8 @@ function StickyLabel({ screenAnchor, action }) {
14206
14200
  width: 6,
14207
14201
  height: 6,
14208
14202
  borderRadius: "50%",
14209
- background: ACCENT3,
14210
- boxShadow: `0 0 0 2px ${PAPER3}, 0 0 0 3px rgba(176, 74, 26, 0.25)`,
14203
+ background: ACCENT,
14204
+ boxShadow: `0 0 0 2px ${PAPER}, 0 0 0 3px rgba(176, 74, 26, 0.25)`,
14211
14205
  ...layout.dot
14212
14206
  }
14213
14207
  }
@@ -14222,25 +14216,25 @@ function StickyLabel({ screenAnchor, action }) {
14222
14216
  style: {
14223
14217
  position: "absolute",
14224
14218
  ...layout.bodyAnchor,
14225
- background: PAPER3,
14226
- color: INK3,
14219
+ background: PAPER,
14220
+ color: INK,
14227
14221
  border: "1px solid rgba(42, 36, 32, 0.10)",
14228
14222
  borderRadius: 3,
14229
14223
  padding: "6px 12px 6px 14px",
14230
- fontFamily: SERIF3,
14231
- fontSize: 12.5,
14224
+ fontFamily: SERIF,
14225
+ fontSize: PILL_FONT_BODY,
14232
14226
  lineHeight: 1.25,
14233
14227
  letterSpacing: 0.6,
14234
14228
  textTransform: "uppercase",
14235
14229
  fontWeight: 500,
14236
14230
  whiteSpace: "nowrap",
14237
- maxWidth: 240,
14231
+ maxWidth: PILL_MAX_W_BODY,
14238
14232
  overflow: "hidden",
14239
14233
  textOverflow: "ellipsis",
14240
14234
  // Warm two-layer shadow (matches GhostReference's palette).
14241
14235
  boxShadow: "0 1px 2px rgba(42, 36, 32, 0.12), 0 8px 18px -6px rgba(42, 36, 32, 0.22)",
14242
14236
  // Internal left accent rule — a 2px terracotta stripe.
14243
- backgroundImage: `linear-gradient(to right, ${ACCENT3} 0, ${ACCENT3} 2px, transparent 2px)`,
14237
+ backgroundImage: `linear-gradient(to right, ${ACCENT} 0, ${ACCENT} 2px, transparent 2px)`,
14244
14238
  backgroundRepeat: "no-repeat",
14245
14239
  backgroundSize: "2px 100%",
14246
14240
  backgroundPosition: "left top"
@@ -14482,9 +14476,9 @@ function computePillAnchor(fromBbox, toBbox, page, camera, viewport) {
14482
14476
  const tipScreenX = viewport.width / 2 + camera.x + (toX - pageCX) * camera.scale;
14483
14477
  const tipScreenY = viewport.height / 2 + camera.y + (toY - pageCY) * camera.scale;
14484
14478
  const isVertical = Math.abs(dy) >= Math.abs(dx);
14485
- const OFFSET = 32;
14486
- const MAX_PILL_W = 220;
14487
- const MAX_PILL_H = 30;
14479
+ const OFFSET = resolvePillOffset(viewport.width);
14480
+ const MAX_PILL_W = resolveMaxPillW(viewport.width);
14481
+ const MAX_PILL_H = resolveMaxPillH(viewport.width);
14488
14482
  const SAFE = 16;
14489
14483
  if (isVertical) {
14490
14484
  const canFitRight = tipScreenX + OFFSET + MAX_PILL_W < viewport.width - SAFE;
@@ -14528,13 +14522,13 @@ function CalloutLabelPill({
14528
14522
  borderRadius: 3,
14529
14523
  padding: spec.padding,
14530
14524
  fontFamily: SERIF,
14531
- fontSize: 11.5,
14525
+ fontSize: PILL_FONT_CAPS,
14532
14526
  lineHeight: 1.2,
14533
14527
  letterSpacing: 0.6,
14534
14528
  textTransform: "uppercase",
14535
14529
  fontWeight: 500,
14536
14530
  whiteSpace: "nowrap",
14537
- maxWidth: 220,
14531
+ maxWidth: PILL_MAX_W_CAPS,
14538
14532
  overflow: "hidden",
14539
14533
  textOverflow: "ellipsis",
14540
14534
  boxShadow: "0 1px 2px rgba(42, 36, 32, 0.12), 0 8px 18px -6px rgba(42, 36, 32, 0.22)",
@@ -14925,7 +14919,12 @@ var StoryboardStepSchema = z.object({
14925
14919
  });
14926
14920
  var StoryboardSchema = z.object({
14927
14921
  version: z.literal(1),
14928
- reasoning: z.string().max(500).default(""),
14922
+ // `reasoning` was required in 0.4.x as a model-generated explanation used
14923
+ // by DebugLog. It carries no visual effect and costs 50–150 output tokens
14924
+ // per call, so from 0.5.1 it's optional (default empty). Consumers who
14925
+ // still send it (from cached prompts or older directors) keep working —
14926
+ // the field is still accepted, just not required.
14927
+ reasoning: z.string().max(500).optional().default(""),
14929
14928
  steps: z.array(StoryboardStepSchema).min(1).max(4)
14930
14929
  });
14931
14930
  function storyboardJsonSchema(opts = {}) {
@@ -15040,7 +15039,10 @@ function storyboardJsonSchema(opts = {}) {
15040
15039
  return {
15041
15040
  type: "object",
15042
15041
  additionalProperties: false,
15043
- required: ["version", "reasoning", "steps"],
15042
+ // `reasoning` intentionally omitted from `required` the field is still
15043
+ // accepted when present but the model doesn't need to generate it,
15044
+ // which saves 50–150 output tokens per call. See zod schema above.
15045
+ required: ["version", "steps"],
15044
15046
  properties: {
15045
15047
  version: { type: "integer", enum: [1] },
15046
15048
  reasoning: { type: "string" },
@@ -15079,7 +15081,6 @@ Anchoring rules:
15079
15081
  Output ONLY this JSON, nothing else:
15080
15082
  {
15081
15083
  "version": 1,
15082
- "reasoning": "<which block(s) you picked, which intent you used, and why \u2014 name the block_id>",
15083
15084
  "steps": [ { "at_ms": <int>, "duration_ms": <int>, "action": <action> }, ... ]
15084
15085
  }
15085
15086
 
@@ -15116,7 +15117,6 @@ When narration fits one of these patterns, emit the corresponding storyboard sha
15116
15117
  Shape: spotlight the term + underline it + drop a label tag. No camera move if the block is already on-screen.
15117
15118
  {
15118
15119
  "version": 1,
15119
- "reasoning": "define recipe: spotlighting and underlining the term, labeling as 'definition'",
15120
15120
  "steps": [
15121
15121
  { "at_ms":0, "duration_ms":700, "action": { "type":"spotlight", "target_block":"p1_para0", "dim_opacity":0.6, "feather_px":40, "shape":"rounded" } },
15122
15122
  { "at_ms":200, "duration_ms":800, "action": { "type":"underline", "target_block":"p1_para0", "color":"#FBBF24", "style":"sketch", "draw_duration_ms":700 } },
@@ -15128,7 +15128,6 @@ Shape: spotlight the term + underline it + drop a label tag. No camera move if t
15128
15128
  Shape: gentle camera move + callout arrow from caption to figure + pulse the figure.
15129
15129
  {
15130
15130
  "version": 1,
15131
- "reasoning": "point_out recipe: drawing attention from caption p1_cap1 to figure p1_fig0",
15132
15131
  "steps": [
15133
15132
  { "at_ms":0, "duration_ms":600, "action": { "type":"camera", "target_block":"p1_fig0", "scale":1.3, "padding":80, "easing":"ease-out" } },
15134
15133
  { "at_ms":400, "duration_ms":900, "action": { "type":"callout", "from_block":"p1_cap1", "to_block":"p1_fig0", "label":"see here", "curve":"curved" } },
@@ -15140,7 +15139,6 @@ Shape: gentle camera move + callout arrow from caption to figure + pulse the fig
15140
15139
  Shape: box A + box B + callout between them with a relational label.
15141
15140
  {
15142
15141
  "version": 1,
15143
- "reasoning": "compare recipe: framing fibrous vs synovial joints",
15144
15142
  "steps": [
15145
15143
  { "at_ms":0, "duration_ms":600, "action": { "type":"box", "target_block":"p1_list5", "color":"#3B82F6", "style":"solid" } },
15146
15144
  { "at_ms":300, "duration_ms":600, "action": { "type":"box", "target_block":"p1_list12", "color":"#F472B6", "style":"solid" } },
@@ -15152,7 +15150,6 @@ Shape: box A + box B + callout between them with a relational label.
15152
15150
  Shape: highlight + pulse. Fast, punchy, no camera.
15153
15151
  {
15154
15152
  "version": 1,
15155
- "reasoning": "emphasize recipe: highlighting key keyword and pulsing for stress",
15156
15153
  "steps": [
15157
15154
  { "at_ms":0, "duration_ms":500, "action": { "type":"highlight", "target_block":"p1_list0", "color":"rgba(250,204,21,0.35)", "draw_duration_ms":450 } },
15158
15155
  { "at_ms":350, "duration_ms":800, "action": { "type":"pulse", "target_block":"p1_list0", "count":2, "intensity":"strong" } }
@@ -15421,32 +15418,32 @@ function clampNumericRanges(input) {
15421
15418
  }
15422
15419
  const type = typeof out.type === "string" ? out.type : void 0;
15423
15420
  if (type === "camera") {
15424
- if (typeof out.scale === "number") out.scale = clamp(out.scale, 0.5, 4);
15421
+ if (typeof out.scale === "number") out.scale = clamp2(out.scale, 0.5, 4);
15425
15422
  if (typeof out.padding === "number") {
15426
- out.padding = clamp(out.padding, 0, 400);
15423
+ out.padding = clamp2(out.padding, 0, 400);
15427
15424
  }
15428
15425
  }
15429
15426
  if (typeof out.dim_opacity === "number") {
15430
- out.dim_opacity = clamp(out.dim_opacity, 0, 1);
15427
+ out.dim_opacity = clamp2(out.dim_opacity, 0, 1);
15431
15428
  }
15432
15429
  if (typeof out.feather_px === "number") {
15433
- out.feather_px = clamp(out.feather_px, 0, 200);
15430
+ out.feather_px = clamp2(out.feather_px, 0, 200);
15434
15431
  }
15435
15432
  if (typeof out.draw_duration_ms === "number") {
15436
- out.draw_duration_ms = clamp(out.draw_duration_ms, 100, 3e3);
15433
+ out.draw_duration_ms = clamp2(out.draw_duration_ms, 100, 3e3);
15437
15434
  }
15438
15435
  if (typeof out.count === "number") {
15439
- out.count = Math.round(clamp(out.count, 1, 5));
15436
+ out.count = Math.round(clamp2(out.count, 1, 5));
15440
15437
  }
15441
15438
  if (typeof out.at_ms === "number") {
15442
- out.at_ms = clamp(out.at_ms, 0, 5e3);
15439
+ out.at_ms = clamp2(out.at_ms, 0, 5e3);
15443
15440
  }
15444
15441
  if (typeof out.duration_ms === "number" && type === void 0) {
15445
- out.duration_ms = clamp(out.duration_ms, 100, 5e3);
15442
+ out.duration_ms = clamp2(out.duration_ms, 100, 5e3);
15446
15443
  }
15447
15444
  return out;
15448
15445
  }
15449
- function clamp(v, lo, hi) {
15446
+ function clamp2(v, lo, hi) {
15450
15447
  return Math.min(hi, Math.max(lo, v));
15451
15448
  }
15452
15449
  function enforceOverlayPresence(sb) {
@@ -15459,9 +15456,10 @@ function enforceOverlayPresence(sb) {
15459
15456
  if (!cameraStep || cameraStep.action.type !== "camera") return sb;
15460
15457
  const target = cameraStep.action.target_block;
15461
15458
  if (!target) return sb;
15459
+ const prefix = sb.reasoning ? `${sb.reasoning} ` : "";
15462
15460
  return {
15463
15461
  ...sb,
15464
- reasoning: `${sb.reasoning} [auto-appended pulse: camera-only storyboards are forbidden]`,
15462
+ reasoning: `${prefix}[auto-appended pulse: camera-only storyboards are forbidden]`,
15465
15463
  steps: [
15466
15464
  ...sb.steps,
15467
15465
  {
@@ -15853,6 +15851,7 @@ function TutorModeContainer({
15853
15851
  backgroundColor = "#ffffff",
15854
15852
  loadingComponent,
15855
15853
  onPageChange,
15854
+ storyboardProvider,
15856
15855
  className
15857
15856
  }) {
15858
15857
  const containerRef = useRef27(null);
@@ -15931,7 +15930,7 @@ function TutorModeContainer({
15931
15930
  const debounceRef = useRef27(null);
15932
15931
  const lastChunkRef = useRef27(null);
15933
15932
  useEffect28(() => {
15934
- if (!llm) return;
15933
+ if (!storyboardProvider && !llm) return;
15935
15934
  if (!currentChunk || currentChunk === lastChunkRef.current) return;
15936
15935
  if (debounceRef.current) clearTimeout(debounceRef.current);
15937
15936
  debounceRef.current = setTimeout(async () => {
@@ -15952,6 +15951,75 @@ function TutorModeContainer({
15952
15951
  });
15953
15952
  abortRef.current?.abort();
15954
15953
  abortRef.current = new AbortController();
15954
+ if (storyboardProvider) {
15955
+ narrationStore.getState().setLlmStatus("in-flight");
15956
+ narrationStore.getState().appendDebugEvent({
15957
+ kind: "llm-request",
15958
+ summary: `provider (page ${pageNumber}, ${page2.blocks.length} blocks)`,
15959
+ payload: {
15960
+ via: "storyboardProvider",
15961
+ pageNumber,
15962
+ blockCount: page2.blocks.length
15963
+ }
15964
+ });
15965
+ try {
15966
+ const raw = await storyboardProvider({
15967
+ chunk,
15968
+ pageNumber,
15969
+ page: page2,
15970
+ history: narrationStore.getState().chunkHistory,
15971
+ signal: abortRef.current.signal
15972
+ });
15973
+ if (!raw) {
15974
+ narrationStore.getState().setLlmStatus("idle");
15975
+ narrationStore.getState().appendDebugEvent({
15976
+ kind: "note",
15977
+ summary: "provider returned null \u2014 no storyboard for this chunk"
15978
+ });
15979
+ return;
15980
+ }
15981
+ const parsed = StoryboardSchema.safeParse(raw);
15982
+ if (!parsed.success) {
15983
+ narrationStore.getState().setLlmStatus(
15984
+ "failed",
15985
+ parsed.error.message
15986
+ );
15987
+ narrationStore.getState().appendDebugEvent({
15988
+ kind: "llm-error",
15989
+ summary: `provider storyboard rejected by schema: ${parsed.error.issues[0]?.message ?? "unknown"}`,
15990
+ payload: { raw, error: parsed.error.message }
15991
+ });
15992
+ return;
15993
+ }
15994
+ const storyboard = parsed.data;
15995
+ narrationStore.getState().setLlmStatus("idle");
15996
+ narrationStore.getState().appendDebugEvent({
15997
+ kind: "llm-response",
15998
+ summary: summariseStoryboard(storyboard),
15999
+ payload: { via: "storyboardProvider", storyboard }
16000
+ });
16001
+ engineRef.current?.execute(storyboard);
16002
+ narrationStore.getState().appendDebugEvent({
16003
+ kind: "storyboard-execute",
16004
+ summary: `engine executing ${storyboard.steps.length} steps`,
16005
+ payload: storyboard.steps.map((s) => ({
16006
+ at_ms: s.at_ms,
16007
+ type: s.action.type,
16008
+ target: "target_block" in s.action ? s.action.target_block : void 0
16009
+ }))
16010
+ });
16011
+ } catch (e) {
16012
+ if (e.name === "AbortError") return;
16013
+ narrationStore.getState().setLlmStatus("failed", e.message);
16014
+ narrationStore.getState().appendDebugEvent({
16015
+ kind: "llm-error",
16016
+ summary: `provider threw: ${e.message.slice(0, 80)}`,
16017
+ payload: e
16018
+ });
16019
+ }
16020
+ return;
16021
+ }
16022
+ if (!llm) return;
15955
16023
  narrationStore.getState().setLlmStatus("in-flight");
15956
16024
  narrationStore.getState().appendDebugEvent({
15957
16025
  kind: "llm-request",
@@ -15973,7 +16041,7 @@ function TutorModeContainer({
15973
16041
  narrationStore.getState().setLlmStatus("idle");
15974
16042
  narrationStore.getState().appendDebugEvent({
15975
16043
  kind: "llm-response",
15976
- summary: `storyboard \u2713 ${result.storyboard.steps.length} steps \u2014 ${result.storyboard.reasoning.slice(0, 60)}`,
16044
+ summary: summariseStoryboard(result.storyboard),
15977
16045
  payload: { raw: result.raw, storyboard: result.storyboard }
15978
16046
  });
15979
16047
  engineRef.current?.execute(result.storyboard);
@@ -16016,7 +16084,16 @@ function TutorModeContainer({
16016
16084
  return () => {
16017
16085
  if (debounceRef.current) clearTimeout(debounceRef.current);
16018
16086
  };
16019
- }, [currentChunk, llm, index, pageNumber, narrationStore, embeddingProvider, llmTimeoutMs]);
16087
+ }, [
16088
+ currentChunk,
16089
+ llm,
16090
+ storyboardProvider,
16091
+ index,
16092
+ pageNumber,
16093
+ narrationStore,
16094
+ embeddingProvider,
16095
+ llmTimeoutMs
16096
+ ]);
16020
16097
  useEffect28(() => {
16021
16098
  if (!currentChunk) return;
16022
16099
  const t = setTimeout(() => {
@@ -16206,6 +16283,15 @@ function TutorLoadingState({
16206
16283
  }
16207
16284
  );
16208
16285
  }
16286
+ function summariseStoryboard(sb) {
16287
+ const stepCount = sb.steps.length;
16288
+ const trimmedReasoning = (sb.reasoning ?? "").trim();
16289
+ if (trimmedReasoning) {
16290
+ return `storyboard \u2713 ${stepCount} steps \u2014 ${trimmedReasoning.slice(0, 60)}`;
16291
+ }
16292
+ const kinds = sb.steps.map((s) => s.action.type).join(" \u2192 ");
16293
+ return `storyboard \u2713 ${stepCount} steps \u2014 ${kinds}`;
16294
+ }
16209
16295
 
16210
16296
  // src/director/transformers-embedding.ts
16211
16297
  var loaded = null;