dwijverma2's picture
Add component: SplitView
e69f256 verified
"use client";
import { useState, useRef, useCallback, useEffect } from "react";
export default function SplitView({ paragraphs, classifications, enriched }) {
const leftRef = useRef(null);
const rightRef = useRef(null);
const [syncing, setSyncing] = useState(false);
const [hoveredIndex, setHoveredIndex] = useState(null);
// Build a lookup: paragraph index -> classification
const classMap = new Map();
if (classifications && classifications.length > 0) {
for (const c of classifications) {
classMap.set(c.index, c);
}
}
// Sync scroll between panels
const handleScroll = useCallback(
(source) => {
if (syncing) return;
setSyncing(true);
const sourceEl = source === "left" ? leftRef.current : rightRef.current;
const targetEl = source === "left" ? rightRef.current : leftRef.current;
if (sourceEl && targetEl) {
const ratio =
sourceEl.scrollTop /
(sourceEl.scrollHeight - sourceEl.clientHeight || 1);
targetEl.scrollTop =
ratio * (targetEl.scrollHeight - targetEl.clientHeight);
}
requestAnimationFrame(() => setSyncing(false));
},
[syncing]
);
// Scroll to a paragraph on click
const scrollToIndex = useCallback((index, panel) => {
const ref = panel === "left" ? leftRef : rightRef;
const el = ref.current?.querySelector(`[data-para-index="${index}"]`);
if (el) {
el.scrollIntoView({ behavior: "smooth", block: "center" });
}
}, []);
return (
<div className="flex-1 flex min-h-0">
{/* Left panel: Original document view */}
<div
className="flex-1 flex flex-col border-r"
style={{ borderColor: "var(--border-color)" }}
>
<div
className="px-4 py-2 text-xs font-medium border-b flex items-center gap-2"
style={{
background: "var(--bg-tertiary)",
borderColor: "var(--border-color)",
color: "var(--text-secondary)",
}}
>
<svg
className="w-3.5 h-3.5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
Original Document
</div>
<div
ref={leftRef}
className="split-panel px-4 py-3"
onScroll={() => handleScroll("left")}
style={{ background: "var(--bg-secondary)" }}
>
{paragraphs.map((p) => (
<div
key={`orig-${p.index}`}
data-para-index={p.index}
className={`para-row px-3 py-2 rounded-md mb-1 transition-colors cursor-default ${
hoveredIndex === p.index ? "ring-1" : ""
}`}
style={{
...(hoveredIndex === p.index
? {
background: "var(--bg-hover)",
ringColor: "var(--accent)",
}
: {}),
}}
onMouseEnter={() => setHoveredIndex(p.index)}
onMouseLeave={() => setHoveredIndex(null)}
>
{p.text ? (
<p className="text-sm leading-relaxed" style={{ color: "var(--text-primary)" }}>
<span
className="text-xs mr-2 select-none"
style={{ color: "var(--text-muted)" }}
>
{p.index}
</span>
{p.text}
</p>
) : (
<p className="text-sm" style={{ color: "var(--text-muted)", opacity: 0.3 }}>
<span className="text-xs mr-2 select-none">{p.index}</span>
(empty)
</p>
)}
{p.fontSizePt && (
<span
className="text-xs ml-6"
style={{ color: "var(--text-muted)" }}
>
{p.fontSizePt}pt
{p.isBold ? " • bold" : ""}
</span>
)}
</div>
))}
</div>
</div>
{/* Right panel: Enriched view with toggle controls */}
<div className="flex-1 flex flex-col">
<div
className="px-4 py-2 text-xs font-medium border-b flex items-center gap-2"
style={{
background: "var(--bg-tertiary)",
borderColor: "var(--border-color)",
color: "var(--text-secondary)",
}}
>
<svg
className="w-3.5 h-3.5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
/>
</svg>
Enriched Document
{enriched && (
<span className="ml-auto text-xs" style={{ color: "var(--text-muted)" }}>
Click checkboxes to toggle headings
</span>
)}
</div>
<div
ref={rightRef}
className="split-panel px-4 py-3"
onScroll={() => handleScroll("right")}
style={{ background: "var(--bg-secondary)" }}
>
{!enriched ? (
<div className="flex items-center justify-center h-full">
<div className="text-center">
<div
className="w-16 h-16 mx-auto mb-4 rounded-full flex items-center justify-center"
style={{ background: "var(--bg-tertiary)" }}
>
<svg
className="w-7 h-7"
style={{ color: "var(--text-muted)" }}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M13 10V3L4 14h7v7l9-11h-7z"
/>
</svg>
</div>
<p
className="text-sm font-medium mb-1"
style={{ color: "var(--text-secondary)" }}
>
Click &quot;Enrich with AI&quot; to start
</p>
<p className="text-xs" style={{ color: "var(--text-muted)" }}>
The AI will detect which paragraphs are headings
</p>
</div>
</div>
) : (
<EnrichedParagraphs
paragraphs={paragraphs}
classifications={classifications}
classMap={classMap}
hoveredIndex={hoveredIndex}
setHoveredIndex={setHoveredIndex}
/>
)}
</div>
</div>
</div>
);
}
function EnrichedParagraphs({
paragraphs,
classifications,
classMap,
hoveredIndex,
setHoveredIndex,
}) {
// We maintain local state for checkbox toggling
// This lets users toggle headings before clicking "Apply Changes"
const [localClassifications, setLocalClassifications] = useState(classifications);
// Sync when parent classifications change (after enrich or apply)
useEffect(() => {
setLocalClassifications(classifications);
}, [classifications]);
const localClassMap = new Map();
for (const c of localClassifications) {
localClassMap.set(c.index, c);
}
const toggleHeading = (index) => {
setLocalClassifications((prev) =>
prev.map((c) =>
c.index === index ? { ...c, isHeading: !c.isHeading } : c
)
);
// Also update the parent's classifications directly so "Apply Changes" picks them up
const existing = classMap.get(index);
if (existing) {
existing.isHeading = !existing.isHeading;
}
};
return (
<>
{paragraphs.map((p) => {
const cls = localClassMap.get(p.index);
const isHeading = cls?.isHeading || false;
return (
<div
key={`enr-${p.index}`}
data-para-index={p.index}
className={`para-row flex items-start gap-3 px-3 py-2 rounded-md mb-1 transition-colors ${
isHeading ? "is-heading" : ""
} ${hoveredIndex === p.index ? "ring-1" : ""}`}
style={{
...(hoveredIndex === p.index
? {
background: "var(--bg-hover)",
ringColor: "var(--accent)",
}
: {}),
}}
onMouseEnter={() => setHoveredIndex(p.index)}
onMouseLeave={() => setHoveredIndex(null)}
>
<input
type="checkbox"
className="heading-checkbox mt-1"
checked={isHeading}
onChange={() => toggleHeading(p.index)}
title={isHeading ? "Unmark as heading" : "Mark as heading"}
/>
<div className="flex-1 min-w-0">
{p.text ? (
<p
className={`leading-relaxed ${isHeading ? "font-bold" : ""}`}
style={{
color: isHeading
? "var(--accent)"
: "var(--text-primary)",
fontSize: isHeading ? "16px" : "14px",
}}
>
<span
className="text-xs mr-2 font-normal select-none"
style={{ color: "var(--text-muted)" }}
>
{p.index}
</span>
{p.text}
</p>
) : (
<p className="text-sm" style={{ color: "var(--text-muted)", opacity: 0.3 }}>
<span className="text-xs mr-2 select-none">{p.index}</span>
(empty)
</p>
)}
{isHeading && (
<span
className="text-xs ml-6 inline-block mt-0.5"
style={{ color: "var(--accent)" }}
>
✦ Heading — font size will be increased
</span>
)}
</div>
</div>
);
})}
</>
);
}