@promptbook/components 0.102.0-4 → 0.102.0-5

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/esm/index.es.js CHANGED
@@ -21,7 +21,7 @@ const BOOK_LANGUAGE_VERSION = '1.0.0';
21
21
  * @generated
22
22
  * @see https://github.com/webgptorg/promptbook
23
23
  */
24
- const PROMPTBOOK_ENGINE_VERSION = '0.102.0-4';
24
+ const PROMPTBOOK_ENGINE_VERSION = '0.102.0-5';
25
25
  /**
26
26
  * TODO: string_promptbook_version should be constrained to the all versions of Promptbook engine
27
27
  * Note: [💞] Ignore a discrepancy between file name and entity name
@@ -3161,6 +3161,23 @@ const DEFAULT_BOOK_FONT_CLASS = styles.bookEditorSerif;
3161
3161
  * Note: [💞] Ignore a discrepancy between file name and entity name
3162
3162
  */
3163
3163
 
3164
+ // Debounce utility for BookEditor and related components
3165
+ // [🧠] Use this for debouncing highlight and other expensive operations
3166
+ /**
3167
+ * @private
3168
+ */
3169
+ function debounce(fn, delay) {
3170
+ let timeout = null;
3171
+ return (...args) => {
3172
+ if (timeout)
3173
+ clearTimeout(timeout);
3174
+ timeout = setTimeout(() => fn(...args), delay);
3175
+ };
3176
+ }
3177
+ /**
3178
+ * Note: [💞] Ignore a discrepancy between file name and entity name
3179
+ * TODO: !!! remove this file */
3180
+
3164
3181
  /**
3165
3182
  * @private util of `<BookEditor />`
3166
3183
  */
@@ -3173,6 +3190,33 @@ function BookEditorInner(props) {
3173
3190
  const highlightRef = useRef(null);
3174
3191
  const [lineHeight, setLineHeight] = useState(32);
3175
3192
  const [isDragOver, setIsDragOver] = useState(false);
3193
+ // Virtualization state: visible line range
3194
+ const [visibleRange, setVisibleRange] = useState([0, 30]);
3195
+ // Debounced update for visible range
3196
+ const updateVisibleRange = useCallback(debounce(() => {
3197
+ const textarea = textareaRef.current;
3198
+ if (!textarea)
3199
+ return;
3200
+ const scrollTop = textarea.scrollTop;
3201
+ const clientHeight = textarea.clientHeight;
3202
+ const totalLines = (value !== null && value !== void 0 ? value : '').split('\n').length;
3203
+ const firstLine = Math.max(0, Math.floor(scrollTop / lineHeight) - 10); // buffer
3204
+ const lastLine = Math.min(totalLines, Math.ceil((scrollTop + clientHeight) / lineHeight) + 10);
3205
+ setVisibleRange([firstLine, lastLine]);
3206
+ }, 30), [value, lineHeight]);
3207
+ // Update visible range on scroll/resize/value change
3208
+ useEffect(() => {
3209
+ updateVisibleRange();
3210
+ const textarea = textareaRef.current;
3211
+ if (!textarea)
3212
+ return;
3213
+ textarea.addEventListener('scroll', updateVisibleRange);
3214
+ window.addEventListener('resize', updateVisibleRange);
3215
+ return () => {
3216
+ textarea.removeEventListener('scroll', updateVisibleRange);
3217
+ window.removeEventListener('resize', updateVisibleRange);
3218
+ };
3219
+ }, [updateVisibleRange]);
3176
3220
  const handleChange = useCallback((event) => {
3177
3221
  const newValue = event.target.value;
3178
3222
  if (controlledValue !== undefined) {
@@ -3485,29 +3529,32 @@ function BookEditorInner(props) {
3485
3529
  });
3486
3530
  return parameters.sort((a, b) => a.start - b.start);
3487
3531
  }, [atParameterRegex, braceParameterRegex]);
3532
+ // Virtualized, debounced highlight rendering for large books
3488
3533
  const highlightedHtml = useMemo(() => {
3489
3534
  const text = value !== null && value !== void 0 ? value : '';
3490
- let lastIndex = 0;
3535
+ const lines = text.split('\n');
3536
+ const [firstLine, lastLine] = visibleRange;
3537
+ const visibleLines = lines.slice(firstLine, lastLine);
3538
+ // Compute offset for correct line numbers
3539
+ const offset = lines.slice(0, firstLine).join('\n').length + (firstLine > 0 ? 1 : 0);
3491
3540
  let out = '';
3492
3541
  const processedRanges = [];
3493
- // First, handle comment-like commitments (NOTE, COMMENT, NONCE) - they take highest priority
3494
- // and should be highlighted as gray comments, including their content
3495
- text.replace(commentRegex, (match, ...args) => {
3496
- const index = args[args.length - 2];
3497
- // Adjust index to skip the newline character if present at the beginning of match
3542
+ // Highlighting logic for visible lines only
3543
+ const visibleText = visibleLines.join('\n');
3544
+ // First, handle comment-like commitments (NOTE, COMMENT, NONCE)
3545
+ visibleText.replace(commentRegex, (match, ...args) => {
3546
+ const index = args[args.length - 2] + offset;
3498
3547
  const adjustedStart = match.startsWith('\n') ? index + 1 : index;
3499
3548
  const adjustedMatch = match.startsWith('\n') ? match.slice(1) : match;
3500
3549
  processedRanges.push({ start: adjustedStart, end: adjustedStart + adjustedMatch.length, type: 'comment' });
3501
3550
  return match;
3502
3551
  });
3503
- // Then, handle META commitments (they take priority over regular commitments)
3504
- text.replace(metaRegex, (match, ...args) => {
3505
- const index = args[args.length - 2];
3506
- // Adjust index to skip the newline character if present at the beginning of match
3552
+ // META commitments
3553
+ visibleText.replace(metaRegex, (match, ...args) => {
3554
+ const index = args[args.length - 2] + offset;
3507
3555
  const adjustedStart = match.startsWith('\n') ? index + 1 : index;
3508
3556
  const adjustedMatch = match.startsWith('\n') ? match.slice(1) : match;
3509
3557
  const matchEnd = adjustedStart + adjustedMatch.length;
3510
- // Check if this match overlaps with any existing range (especially comments)
3511
3558
  const overlaps = processedRanges.some((range) => (adjustedStart >= range.start && adjustedStart < range.end) ||
3512
3559
  (matchEnd > range.start && matchEnd <= range.end) ||
3513
3560
  (adjustedStart < range.start && matchEnd > range.end));
@@ -3516,14 +3563,12 @@ function BookEditorInner(props) {
3516
3563
  }
3517
3564
  return match;
3518
3565
  });
3519
- // Then handle regular commitment types, avoiding overlaps with META and comment ranges
3520
- text.replace(typeRegex, (match, ...args) => {
3521
- const index = args[args.length - 2];
3522
- // Adjust index to skip the newline character if present at the beginning of match
3566
+ // Regular commitment types
3567
+ visibleText.replace(typeRegex, (match, ...args) => {
3568
+ const index = args[args.length - 2] + offset;
3523
3569
  const adjustedStart = match.startsWith('\n') ? index + 1 : index;
3524
3570
  const adjustedMatch = match.startsWith('\n') ? match.slice(1) : match;
3525
3571
  const matchEnd = adjustedStart + adjustedMatch.length;
3526
- // Check if this match overlaps with any existing range
3527
3572
  const overlaps = processedRanges.some((range) => (adjustedStart >= range.start && adjustedStart < range.end) ||
3528
3573
  (matchEnd > range.start && matchEnd <= range.end) ||
3529
3574
  (adjustedStart < range.start && matchEnd > range.end));
@@ -3532,29 +3577,28 @@ function BookEditorInner(props) {
3532
3577
  }
3533
3578
  return match;
3534
3579
  });
3535
- // Handle parameters using the unified extraction function - both @Parameter and {parameter} notations
3536
- // are treated as the same syntax feature with unified highlighting
3537
- const unifiedParameters = extractUnifiedParameters(text);
3580
+ // Parameters
3581
+ const unifiedParameters = extractUnifiedParameters(visibleText);
3538
3582
  unifiedParameters.forEach((param) => {
3539
- // Check if this parameter overlaps with any existing range
3540
- const overlaps = processedRanges.some((range) => (param.start >= range.start && param.start < range.end) ||
3541
- (param.end > range.start && param.end <= range.end) ||
3542
- (param.start < range.start && param.end > range.end));
3583
+ const paramStart = param.start + offset;
3584
+ const paramEnd = param.end + offset;
3585
+ const overlaps = processedRanges.some((range) => (paramStart >= range.start && paramStart < range.end) ||
3586
+ (paramEnd > range.start && paramEnd <= range.end) ||
3587
+ (paramStart < range.start && paramEnd > range.end));
3543
3588
  if (!overlaps) {
3544
3589
  processedRanges.push({
3545
- start: param.start,
3546
- end: param.end,
3590
+ start: paramStart,
3591
+ end: paramEnd,
3547
3592
  type: 'parameter',
3548
3593
  });
3549
3594
  }
3550
3595
  });
3551
3596
  // Sort ranges by start position
3552
3597
  processedRanges.sort((a, b) => a.start - b.start);
3553
- // Build the highlighted HTML
3598
+ // Build the highlighted HTML for the visible lines only
3599
+ let visibleLastIndex = offset;
3554
3600
  processedRanges.forEach((range) => {
3555
- // Add text before this range
3556
- out += escapeHtml(text.slice(lastIndex, range.start));
3557
- // Add highlighted text with appropriate class
3601
+ out += escapeHtml(text.slice(visibleLastIndex, range.start));
3558
3602
  const matchText = text.slice(range.start, range.end);
3559
3603
  let cssClass;
3560
3604
  switch (range.type) {
@@ -3562,11 +3606,9 @@ function BookEditorInner(props) {
3562
3606
  cssClass = 'book-highlight-keyword';
3563
3607
  break;
3564
3608
  case 'parameter':
3565
- // Use the unified parameter class, but maintain backward compatibility
3566
3609
  cssClass = 'book-highlight-parameter';
3567
3610
  break;
3568
3611
  case 'comment':
3569
- // NOTE, COMMENT, NONCE commitments should be highlighted as gray comments
3570
3612
  cssClass = 'book-highlight-comment';
3571
3613
  break;
3572
3614
  default:
@@ -3574,16 +3616,15 @@ function BookEditorInner(props) {
3574
3616
  break;
3575
3617
  }
3576
3618
  out += `<span class="${cssClass}">${escapeHtml(matchText)}</span>`;
3577
- lastIndex = range.end;
3619
+ visibleLastIndex = range.end;
3578
3620
  });
3579
- // Add remaining text
3580
- out += escapeHtml(text.slice(lastIndex));
3581
- const lines = out.split('\n');
3582
- if (lines.length > 0) {
3583
- lines[0] = `<span class="book-highlight-title">${lines[0]}</span>`;
3584
- }
3585
- return lines.join('\n');
3586
- }, [value, typeRegex, metaRegex, extractUnifiedParameters]);
3621
+ out += escapeHtml(text.slice(visibleLastIndex, offset + visibleText.length));
3622
+ const resultLines = out.split('\n').slice(firstLine, lastLine);
3623
+ if (resultLines.length > 0 && firstLine === 0) {
3624
+ resultLines[0] = `<span class="book-highlight-title">${resultLines[0]}</span>`;
3625
+ }
3626
+ return resultLines.join('\n');
3627
+ }, [value, typeRegex, metaRegex, extractUnifiedParameters, visibleRange]);
3587
3628
  return (jsx("div", { className: classNames(styles.bookEditorContainer, isVerbose && styles.isVerbose, className), children: jsxs("div", { className: classNames(styles.bookEditorWrapper, effectiveFontClassName, isBorderRadiusDisabled && styles.isBorderRadiusDisabled), children: [jsx("div", { "aria-hidden": true, className: styles.bookEditorBackground, style: { backgroundImage: 'none' } }), jsx("pre", { ref: highlightRef, "aria-hidden": true, className: `${styles.bookEditorHighlight} ${effectiveFontClassName}`, style: {
3588
3629
  lineHeight: `${lineHeight}px`,
3589
3630
  backgroundImage: `linear-gradient(90deg, transparent 30px, rgba(59,130,246,0.3) 30px, rgba(59,130,246,0.3) 31px, transparent 31px), repeating-linear-gradient(0deg, transparent, transparent calc(${lineHeight}px - 1px), rgba(0,0,0,0.06) ${lineHeight}px)`,