diff --git a/apps/website/app/(extract)/extract-nodes/components/MainContent.tsx b/apps/website/app/(extract)/extract-nodes/components/MainContent.tsx index 9f321a358..f363d0136 100644 --- a/apps/website/app/(extract)/extract-nodes/components/MainContent.tsx +++ b/apps/website/app/(extract)/extract-nodes/components/MainContent.tsx @@ -1,159 +1,197 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; import { Badge } from "@repo/ui/components/ui/badge"; import { Button } from "@repo/ui/components/ui/button"; import { Card, CardContent } from "@repo/ui/components/ui/card"; import { Checkbox } from "@repo/ui/components/ui/checkbox"; import { Copy } from "lucide-react"; +import type { ExtractedNode } from "~/types/extraction"; +import { NODE_TYPE_DEFINITIONS, type NodeTypeDefinition } from "../nodeTypes"; + +const findNodeTypeDefinition = ( + nodeType: string, +): NodeTypeDefinition | undefined => + NODE_TYPE_DEFINITIONS.find( + (t) => t.label.toLowerCase() === nodeType.toLowerCase(), + ); -const NODE_TYPE_COLORS: Record = { - claim: "#7DA13E", - question: "#99890E", - hypothesis: "#7C4DFF", - evidence: "#dc0c4a", - result: "#E6A23C", - source: "#9E9E9E", - theory: "#8B5CF6", +const formatNodeForClipboard = (node: ExtractedNode): string => { + const meta = findNodeTypeDefinition(node.nodeType); + const header = meta ? `${node.content} ${meta.candidateTag}` : node.content; + const lines = [header, `\tSource quote: "${node.supportSnippet}"`]; + if (node.sourceSection) { + lines.push(`\tSection: ${node.sourceSection}`); + } + return lines.join("\n"); }; -const SAMPLE_NODES = [ - { - nodeType: "claim", - content: - "Basolateral secretion of Wnt5a is essential for establishing apical-basal polarity in epithelial cells.", - supportSnippet: - '"Wnt5a secreted from the basolateral surface was both necessary and sufficient for the establishment of apical-basal polarity" (p.9)', - sourceSection: "Discussion", - }, - { - nodeType: "evidence", - content: - "Wnt5a was detected exclusively in the basolateral medium of polarized MDCK cells grown on Transwell filters, with no detectable signal in the apical fraction.", - supportSnippet: - '"Western blot analysis of conditioned media showed Wnt5a protein exclusively in the basolateral fraction (Fig. 2A, lanes 3-4)"', - sourceSection: "Results", - }, - { - nodeType: "question", - content: - "What is the mechanism by which Wnt5a polarized secretion is directed to the basolateral membrane?", - supportSnippet: - '"The mechanism that directs Wnt5a specifically to the basolateral surface remains an open question" (p.11)', - sourceSection: "Discussion", - }, - { - nodeType: "hypothesis", - content: - "Ror2 receptor activation at the basolateral surface mediates Wnt5a-dependent lumen positioning.", - supportSnippet: - '"We hypothesize that Ror2, as the primary receptor for Wnt5a at the basolateral membrane, transduces the polarity signal required for single-lumen formation"', - sourceSection: "Discussion", - }, - { - nodeType: "result", - content: - "shRNA-mediated knockdown of Wnt5a resulted in multi-lumen cysts in 68% of colonies compared to 12% in control conditions.", - supportSnippet: - '"Quantification of cyst morphology revealed 68 ± 4% multi-lumen cysts in Wnt5a-KD versus 12 ± 3% in controls (Fig. 4B, p < 0.001)"', - sourceSection: "Results", - }, - { - nodeType: "source", - content: "Yamamoto et al. (2015) Nature Cell Biology 17(8):1024-1035", - supportSnippet: - "Primary research article on Wnt5a basolateral secretion and lumen formation in polarized epithelia.", - sourceSection: "References", - }, - { - nodeType: "theory", - content: - "Non-canonical Wnt signaling through the planar cell polarity pathway is a conserved mechanism for epithelial lumen morphogenesis.", - supportSnippet: - '"Our findings place Wnt5a upstream of the PCP pathway in the regulation of epithelial lumen morphogenesis, consistent with the broader role of non-canonical Wnt signaling in tissue polarity"', - sourceSection: "Discussion", - }, - { - nodeType: "evidence", - content: - "Co-immunoprecipitation showed that Wnt5a preferentially binds Ror2 receptor at the basolateral surface.", - supportSnippet: - '"IP-Western analysis demonstrated direct Wnt5a-Ror2 interaction in basolateral but not apical membrane fractions (Fig. 5C)"', - sourceSection: "Results", - }, - { - nodeType: "claim", - content: - "Loss of Wnt5a function disrupts lumen formation in 3D cyst cultures derived from epithelial cells.", - supportSnippet: - '"These data demonstrate that Wnt5a is required for proper lumen formation in three-dimensional culture systems"', - sourceSection: "Discussion", - }, -]; - -const EXPANDED_INDICES = new Set([0, 1]); - -const typeCounts = SAMPLE_NODES.reduce>((acc, node) => { - acc[node.nodeType] = (acc[node.nodeType] ?? 0) + 1; - return acc; -}, {}); - -const TABS = [ - { id: "all", label: "All", count: SAMPLE_NODES.length, color: undefined }, - ...Object.entries(typeCounts).map(([nodeType, count]) => ({ - id: nodeType, - label: nodeType.charAt(0).toUpperCase() + nodeType.slice(1), - count, - color: NODE_TYPE_COLORS[nodeType], - })), -]; - -export const MainContent = (): React.ReactElement => { +type MainContentProps = { + nodes: ExtractedNode[]; + paperTitle?: string; +}; + +export const MainContent = ({ + nodes, + paperTitle, +}: MainContentProps): React.ReactElement => { + const [expandedNodes, setExpandedNodes] = useState>(new Set()); + const [selected, setSelected] = useState>(new Set()); + const [activeFilter, setActiveFilter] = useState("all"); + const [copied, setCopied] = useState(false); + + useEffect(() => { + setSelected(new Set()); + }, [nodes]); + + const typeCounts = useMemo( + () => + nodes.reduce>((acc, node) => { + const key = node.nodeType.toLowerCase(); + acc[key] = (acc[key] ?? 0) + 1; + return acc; + }, {}), + [nodes], + ); + + const tabs = useMemo( + () => [ + { id: "all", label: "All", count: nodes.length, color: undefined }, + ...Object.entries(typeCounts).map(([nodeType, count]) => ({ + id: nodeType, + label: nodeType.charAt(0).toUpperCase() + nodeType.slice(1), + count, + color: findNodeTypeDefinition(nodeType)?.color, + })), + ], + [nodes.length, typeCounts], + ); + + const filteredNodes = useMemo(() => { + const indexed = nodes.map((node, originalIndex) => ({ + node, + originalIndex, + })); + return activeFilter === "all" + ? indexed + : indexed.filter( + ({ node }) => node.nodeType.toLowerCase() === activeFilter, + ); + }, [nodes, activeFilter]); + + const toggleExpanded = (index: number): void => { + setExpandedNodes((prev) => { + const next = new Set(prev); + if (next.has(index)) { + next.delete(index); + } else { + next.add(index); + } + return next; + }); + }; + + const toggleSelected = (index: number): void => { + setSelected((prev) => { + const next = new Set(prev); + if (next.has(index)) { + next.delete(index); + } else { + next.add(index); + } + return next; + }); + }; + + const selectAll = (): void => { + setSelected(new Set(nodes.keys())); + }; + + const deselectAll = (): void => { + setSelected(new Set()); + }; + + const handleCopy = async (): Promise => { + const text = [...selected] + .sort((a, b) => a - b) + .map((i) => formatNodeForClipboard(nodes[i]!)) + .join("\n"); + await navigator.clipboard.writeText(text); + setCopied(true); + setTimeout(() => setCopied(false), 1500); + }; + + if (nodes.length === 0) { + return ( +
+
+

+ No extracted nodes yet +

+

+ Upload a paper and run extraction to see results here. +

+
+
+ ); + } + return (
-
-
-

- Basolateral secretion of Wnt5a in polarized epithelial cells is - required for apical lumen formation -

-

- Yamamoto H, Komekado H, Kikuchi A -

-
+ {paperTitle && ( +
+
+

+ {paperTitle} +

+
+ )}
- {TABS.map((tab) => ( - - {tab.color && ( - - )} - {tab.label} {tab.count} - + {tabs.map((tab) => ( + ))}
- {SAMPLE_NODES.map((node, index) => { - const color = NODE_TYPE_COLORS[node.nodeType] ?? "#64748b"; - const isExpanded = EXPANDED_INDICES.has(index); + {filteredNodes.map(({ node, originalIndex }) => { + const color = + findNodeTypeDefinition(node.nodeType)?.color ?? "#64748b"; + const isExpanded = expandedNodes.has(originalIndex); + const isSelected = selected.has(originalIndex); return ( - +
- + toggleSelected(originalIndex)} + className="mt-1" + />
{ variant="ghost" size="sm" className="mt-1 h-auto p-0 text-[13px] font-medium text-slate-400 hover:text-slate-600" + onClick={() => toggleExpanded(originalIndex)} > Hide details @@ -191,6 +230,7 @@ export const MainContent = (): React.ReactElement => { variant="ghost" size="sm" className="mt-1 h-auto p-0 text-[13px] font-medium text-slate-400 hover:text-slate-600" + onClick={() => toggleExpanded(originalIndex)} > Show details @@ -206,21 +246,41 @@ export const MainContent = (): React.ReactElement => {
- - - {SAMPLE_NODES.length} of {SAMPLE_NODES.length} selected +
+ +
+ +
+ + {selected.size} of {nodes.length} selected
-
diff --git a/apps/website/app/(extract)/extract-nodes/nodeTypes.ts b/apps/website/app/(extract)/extract-nodes/nodeTypes.ts new file mode 100644 index 000000000..17470119f --- /dev/null +++ b/apps/website/app/(extract)/extract-nodes/nodeTypes.ts @@ -0,0 +1,48 @@ +// TEMPORARY: mirrors the shape of NODE_TYPE_DEFINITIONS being added in +// ENG-1595 (apps/website/app/types/extraction.ts). Delete this file and +// update imports to "~/types/extraction" once ENG-1595 merges to main. + +export type NodeTypeDefinition = { + label: string; + definition: string; + candidateTag: string; + color?: string; +}; + +export const NODE_TYPE_DEFINITIONS: NodeTypeDefinition[] = [ + { + label: "Evidence", + definition: + "A specific empirical observation from a particular study. One distinct statistical test, measurement, or analytical finding. Past tense. Includes observable, model system, method.", + candidateTag: "#evd-candidate", + color: "#DB134A", + }, + { + label: "Claim", + definition: + "An atomic, generalized assertion about the world that proposes to answer a research question. Goes beyond data to state what it means. Specific enough to test or argue against.", + candidateTag: "#clm-candidate", + color: "#7DA13E", + }, + { + label: "Question", + definition: + "A research question — explicitly stated or implied by a gap in the literature. Open-ended, answerable by empirical evidence.", + candidateTag: "#que-candidate", + color: "#99890E", + }, + { + label: "Pattern", + definition: + "A conceptual class — a theoretical object, heuristic, design pattern, or methodological approach — abstracted from specific implementations.", + candidateTag: "#ptn-candidate", + color: "#E040FB", + }, + { + label: "Artifact", + definition: + "A specific concrete system, tool, standard, dataset, or protocol that instantiates one or more patterns.", + candidateTag: "#art-candidate", + color: "#67C23A", + }, +]; diff --git a/apps/website/app/(extract)/extract-nodes/page.tsx b/apps/website/app/(extract)/extract-nodes/page.tsx index fa545f9de..129e61e4a 100644 --- a/apps/website/app/(extract)/extract-nodes/page.tsx +++ b/apps/website/app/(extract)/extract-nodes/page.tsx @@ -1,11 +1,94 @@ +"use client"; + +import { useState } from "react"; +import type { ExtractedNode } from "~/types/extraction"; import { MainContent } from "./components/MainContent"; import { Sidebar } from "./components/Sidebar"; +// TODO(ENG-1592): Replace with actual extraction results from API +const SAMPLE_NODES: ExtractedNode[] = [ + { + nodeType: "Claim", + content: + "Basolateral secretion of Wnt5a is essential for establishing apical-basal polarity in epithelial cells.", + supportSnippet: + '"Wnt5a secreted from the basolateral surface was both necessary and sufficient for the establishment of apical-basal polarity" (p.9)', + sourceSection: "Discussion", + }, + { + nodeType: "Evidence", + content: + "Wnt5a was detected exclusively in the basolateral medium of polarized MDCK cells grown on Transwell filters, with no detectable signal in the apical fraction.", + supportSnippet: + '"Western blot analysis of conditioned media showed Wnt5a protein exclusively in the basolateral fraction (Fig. 2A, lanes 3-4)"', + sourceSection: "Results", + }, + { + nodeType: "Question", + content: + "What is the mechanism by which Wnt5a polarized secretion is directed to the basolateral membrane?", + supportSnippet: + '"The mechanism that directs Wnt5a specifically to the basolateral surface remains an open question" (p.11)', + sourceSection: "Discussion", + }, + { + nodeType: "Claim", + content: + "Ror2 receptor activation at the basolateral surface mediates Wnt5a-dependent lumen positioning.", + supportSnippet: + '"We hypothesize that Ror2, as the primary receptor for Wnt5a at the basolateral membrane, transduces the polarity signal required for single-lumen formation"', + sourceSection: "Discussion", + }, + { + nodeType: "Evidence", + content: + "shRNA-mediated knockdown of Wnt5a resulted in multi-lumen cysts in 68% of colonies compared to 12% in control conditions.", + supportSnippet: + '"Quantification of cyst morphology revealed 68 ± 4% multi-lumen cysts in Wnt5a-KD versus 12 ± 3% in controls (Fig. 4B, p < 0.001)"', + sourceSection: "Results", + }, + { + nodeType: "Artifact", + content: + "MDCK 3D cyst culture model grown on Matrigel, used to visualize lumen formation by confocal live imaging of F-actin and podocalyxin markers.", + supportSnippet: + '"We used MDCK II cells seeded in 100% Matrigel and imaged cyst development over 96 hours using spinning-disk confocal microscopy (Materials and Methods)"', + sourceSection: "Methods", + }, + { + nodeType: "Pattern", + content: + "Non-canonical Wnt signaling through the planar cell polarity pathway is a conserved mechanism for epithelial lumen morphogenesis.", + supportSnippet: + '"Our findings place Wnt5a upstream of the PCP pathway in the regulation of epithelial lumen morphogenesis, consistent with the broader role of non-canonical Wnt signaling in tissue polarity"', + sourceSection: "Discussion", + }, + { + nodeType: "Evidence", + content: + "Co-immunoprecipitation showed that Wnt5a preferentially binds Ror2 receptor at the basolateral surface.", + supportSnippet: + '"IP-Western analysis demonstrated direct Wnt5a-Ror2 interaction in basolateral but not apical membrane fractions (Fig. 5C)"', + sourceSection: "Results", + }, + { + nodeType: "Claim", + content: + "Loss of Wnt5a function disrupts lumen formation in 3D cyst cultures derived from epithelial cells.", + supportSnippet: + '"These data demonstrate that Wnt5a is required for proper lumen formation in three-dimensional culture systems"', + sourceSection: "Discussion", + }, +]; + const ExtractNodesPage = (): React.ReactElement => { + // TODO(ENG-1592): Wire to actual extraction API results + const [nodes] = useState(SAMPLE_NODES); + return (
- +
); };