All files / components/chat pdf-split-panel.tsx

91.42% Statements 32/35
86.66% Branches 13/15
80% Functions 8/10
93.33% Lines 28/30

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138    1x 1x 1x 1x 1x 1x 1x   1x             9x 10x 10x 10x 10x 10x   10x 9x 9x 9x     9x 9x                           10x     1x 1x       1x               6x 3x 3x                                                                                                                       3x                            
"use client";
 
import { useEffect, useMemo, useRef, useState } from "react";
import { MagnifyingGlassIcon } from "@/components/icons/MagnifyingGlass";
import { usePdfSearch } from "@/hooks/use-pdf-search";
import { Document, Page, pdfjs } from "react-pdf";
import "react-pdf/dist/Page/AnnotationLayer.css";
import "react-pdf/dist/Page/TextLayer.css";
import { XMarkIcon } from "@/components/icons/XMark";
 
pdfjs.GlobalWorkerOptions.workerSrc = `https://unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`;
 
type PdfSplitPanelProps = {
  file: File;
  onClose: () => void;
};
 
export function PdfSplitPanel({ file, onClose }: PdfSplitPanelProps) {
  const url = useMemo(() => URL.createObjectURL(file), [file]);
  const [numPages, setNumPages] = useState<number>(0);
  const pageRefs = useRef<Map<number, HTMLDivElement>>(new Map());
  const containerRef = useRef<HTMLDivElement>(null);
  const [containerWidth, setContainerWidth] = useState<number>(0);
 
  useEffect(() => {
    const el = containerRef.current;
    Iif (!el) return;
    const observer = new ResizeObserver(([entry]) => {
      setContainerWidth(entry.contentRect.width);
    });
    observer.observe(el);
    return () => observer.disconnect();
  }, []);
 
  const {
    setPdfDoc,
    searchQuery,
    matchPages,
    matchIndex,
    isSearching,
    searched,
    navigateNext,
    handleQueryChange,
    handleClearQuery,
    highlightText,
  } = usePdfSearch(pageRefs);
 
  function handleLoadSuccess(doc: pdfjs.PDFDocumentProxy) {
    setNumPages(doc.numPages);
    setPdfDoc(doc);
  }
 
  function handleSearchKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
    Eif (e.key === "Enter") navigateNext();
  }
 
  function customTextRenderer({ str }: { str: string }) {
    return highlightText(str);
  }
 
  function setPageRef(el: HTMLDivElement | null) {
    if (!el) return;
    const pageNumber = Number(el.dataset.page);
    pageRefs.current.set(pageNumber, el);
  }
 
  return (
    <div className="flex h-full flex-1 flex-col overflow-hidden rounded-[1.75rem] border border-white/9 bg-panel backdrop-blur-[28px] shadow-navy-xl light:border-slate-200 light:shadow-glass-card-light animate-drawer-in">
      <div className="flex items-center justify-between border-b border-white/8 px-5 py-3.5 light:border-slate-200">
        <p className="truncate text-sm font-medium text-white/85 light:text-slate-800">{file.name}</p>
        <button
          type="button"
          aria-label="Close preview"
          onClick={onClose}
          className="ml-4 grid h-7 w-7 shrink-0 place-items-center rounded-lg text-white/40 transition hover:bg-rose-500/15 hover:text-rose-300 light:text-slate-400 light:hover:bg-rose-50 light:hover:text-rose-500"
        >
          <XMarkIcon className="size-4" />
        </button>
      </div>
 
      <div className="flex items-center gap-2 border-b border-white/8 px-4 py-2.5 light:border-slate-200">
        <div className="relative flex-1">
          <MagnifyingGlassIcon className="absolute left-3 top-1/2 -translate-y-1/2 size-3.5 text-white/40 light:text-slate-400" />
          <input
            type="text"
            value={searchQuery}
            onChange={handleQueryChange}
            onKeyDown={handleSearchKeyDown}
            placeholder="Search in document..."
            className="w-full rounded-lg bg-white/8 py-1.5 pl-8 pr-7 text-xs text-white/80 placeholder-white/30 outline-none focus:ring-1 focus:ring-white/20 light:bg-slate-100 light:text-slate-700 light:placeholder-slate-400 light:focus:ring-slate-300"
          />
          {searchQuery && (
            <button
              type="button"
              aria-label="Clear search"
              onClick={handleClearQuery}
              className="absolute right-2 top-1/2 -translate-y-1/2 grid h-4 w-4 place-items-center rounded text-white/30 hover:text-white/60 light:text-slate-400 light:hover:text-slate-600"
            >
              <XMarkIcon className="size-3" />
            </button>
          )}
        </div>
        <button
          type="button"
          onClick={navigateNext}
          disabled={isSearching || !searchQuery.trim()}
          className="shrink-0 rounded-lg bg-white/8 px-3 py-1.5 text-xs text-white/70 transition hover:bg-white/12 disabled:opacity-40 light:bg-slate-100 light:text-slate-600 light:hover:bg-slate-200"
        >
          {isSearching ? "..." : "Find"}
        </button>
        {searched && matchPages.length > 0 && (
          <span className="shrink-0 text-xs text-white/40 light:text-slate-400">
            {matchIndex + 1}/{matchPages.length}
          </span>
        )}
        {searched && matchPages.length === 0 && (
          <span className="shrink-0 text-xs text-red-400">Not found</span>
        )}
      </div>
 
      <div ref={containerRef} className="min-h-0 flex-1 overflow-y-auto flex flex-col items-center gap-2 bg-white/4 py-4 light:bg-slate-100">
        <Document file={url} onLoadSuccess={handleLoadSuccess}>
          {Array.from({ length: numPages }, (_, i) => (
            <div key={i + 1} data-page={i + 1} ref={setPageRef}>
              <Page
                pageNumber={i + 1}
                className="shadow-md"
                width={containerWidth}
                customTextRenderer={customTextRenderer}
              />
            </div>
          ))}
        </Document>
      </div>
    </div>
  );
}