Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
230 changes: 230 additions & 0 deletions apps/website/app/(extract)/extract-nodes/components/MainContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
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";

const NODE_TYPE_COLORS: Record<string, string> = {
claim: "#7DA13E",
question: "#99890E",
hypothesis: "#7C4DFF",
evidence: "#dc0c4a",
result: "#E6A23C",
source: "#9E9E9E",
theory: "#8B5CF6",
};

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<Record<string, number>>((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],
})),
];

// eslint-disable-next-line @typescript-eslint/naming-convention
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't be required anymore

export const MainContent = (): React.ReactElement => {
return (
<section className="flex min-h-[420px] flex-1 overflow-hidden rounded-[24px] border border-slate-200/85 bg-white shadow-[0_24px_48px_-36px_rgba(15,23,42,0.55)]">
<div className="flex flex-1 flex-col overflow-hidden bg-[linear-gradient(180deg,#ffffff_0%,#f8fafc_100%)]">
<div className="relative shrink-0 border-b border-slate-200/80 bg-[linear-gradient(135deg,#ffffff_0%,#f8fbff_100%)] px-4 py-4 lg:px-5">
<div className="absolute inset-x-0 top-0 h-[2px] bg-[linear-gradient(90deg,#0ea5e9_0%,#22d3ee_45%,#34d399_100%)]" />
<h2 className="text-[24px] font-semibold tracking-[-0.024em] text-slate-900">
Basolateral secretion of Wnt5a in polarized epithelial cells is
required for apical lumen formation
</h2>
<p className="mt-1 text-[15px] text-slate-500">
Yamamoto H, Komekado H, Kikuchi A
</p>
</div>

<div className="shrink-0 border-b border-slate-200/70 bg-white/95 px-4 lg:px-5">
<div className="flex gap-1 py-2">
{TABS.map((tab) => (
<Badge
key={tab.id}
variant={tab.id === "all" ? "default" : "outline"}
className={
tab.id === "all"
? "bg-slate-900 px-3 py-1.5 text-[14px] font-semibold text-white hover:bg-slate-800"
: "px-3 py-1.5 text-[14px] font-semibold text-slate-600"
}
>
{tab.color && (
<span
className="mr-1.5 inline-block h-2 w-2 rounded-full"
style={{ backgroundColor: tab.color }}
/>
)}
{tab.label} {tab.count}
</Badge>
))}
</div>
</div>

<div className="flex-1 overflow-y-auto bg-[radial-gradient(120%_100%_at_50%_0%,#f8fbff_0%,#f8fafc_52%,#f3f7fb_100%)] p-4 lg:p-5">
<div className="space-y-2.5">
{SAMPLE_NODES.map((node, index) => {
const color = NODE_TYPE_COLORS[node.nodeType] ?? "#64748b";
const isExpanded = EXPANDED_INDICES.has(index);
return (
<Card key={index} className="rounded-2xl border-slate-200/85">
<CardContent className="p-4">
<div className="flex items-start gap-3">
<Checkbox checked className="mt-1" />
<div className="min-w-0 flex-1">
<div className="mb-2 flex flex-wrap items-center gap-2">
<span
className="rounded px-1.5 py-0.5 text-[11px] font-bold uppercase tracking-wider text-white"
style={{ backgroundColor: color }}
>
{node.nodeType}
</span>
{node.sourceSection && (
<span className="text-[13px] text-slate-400">
{node.sourceSection}
</span>
)}
</div>
<p className="text-[15px] leading-relaxed text-slate-800">
{node.content}
</p>
{isExpanded ? (
<>
<div className="mt-2 rounded-lg bg-slate-50 px-3 py-2">
<p className="text-[13px] italic leading-relaxed text-slate-500">
{node.supportSnippet}
</p>
</div>
<Button
variant="ghost"
size="sm"
className="mt-1 h-auto p-0 text-[13px] font-medium text-slate-400 hover:text-slate-600"
>
Hide details
</Button>
</>
) : (
<Button
variant="ghost"
size="sm"
className="mt-1 h-auto p-0 text-[13px] font-medium text-slate-400 hover:text-slate-600"
>
Show details
</Button>
)}
</div>
</div>
</CardContent>
</Card>
);
})}
</div>
</div>

<div className="flex shrink-0 flex-col gap-3 border-t border-slate-200/85 bg-white/95 px-4 py-3.5 backdrop-blur sm:flex-row sm:items-center sm:justify-between lg:px-5">
<div className="flex items-center gap-2.5">
<Button
variant="outline"
size="sm"
className="rounded-full border-slate-200 text-slate-600"
>
Deselect All
</Button>
<span className="text-[14px] font-medium tabular-nums text-slate-500">
{SAMPLE_NODES.length} of {SAMPLE_NODES.length} selected
</span>
</div>

<Button className="gap-2 rounded-full bg-slate-900 text-white hover:bg-slate-800">
<Copy className="h-4 w-4" />
Copy to Clipboard
</Button>
</div>
</div>
</section>
);
};
142 changes: 142 additions & 0 deletions apps/website/app/(extract)/extract-nodes/components/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { Button } from "@repo/ui/components/ui/button";
import { Checkbox } from "@repo/ui/components/ui/checkbox";
import { ChevronDown } from "lucide-react";

const NODE_TYPES = [
{
label: "Claim",
description:
"An assertion about how something works or should work. Usually a single declarative sentence.",
candidateTag: "#clm-candidate",
color: "#7DA13E",
},
{
label: "Question",
description:
"An unknown being explored through research. Framed as a question that can be investigated.",
candidateTag: "#que-candidate",
color: "#99890E",
},
{
label: "Hypothesis",
description:
"A proposed answer to a question. Collects evidence for or against.",
candidateTag: "#hyp-candidate",
color: "#7C4DFF",
},
{
label: "Evidence",
description:
"A discrete observation from a published dataset or experiment. Usually in past tense with observable, model system, and method.",
candidateTag: "#evd-candidate",
color: "#dc0c4a",
},
{
label: "Result",
description:
"A discrete observation from ongoing research, in past tense. Includes source context.",
candidateTag: "#res-candidate",
color: "#E6A23C",
},
{
label: "Source",
description: "A referenced publication or external resource.",
candidateTag: "#src-candidate",
color: "#9E9E9E",
},
{
label: "Theory",
description: "A theoretical framework or model that explains phenomena.",
candidateTag: "#the-candidate",
color: "#8B5CF6",
},
];

const SECTION_LABEL_CLASS =
"mb-3 block px-1 text-[18px] font-semibold tracking-[-0.016em] text-slate-800";

// eslint-disable-next-line @typescript-eslint/naming-convention
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't be required anymore

export const Sidebar = (): React.ReactElement => {
return (
<aside className="flex w-full shrink-0 flex-col overflow-hidden rounded-[24px] border border-slate-200/85 bg-white shadow-[0_26px_52px_-38px_rgba(15,23,42,0.6)] lg:w-[390px] xl:w-[420px]">
<div className="flex-1 overflow-y-auto p-4 lg:p-5">
<section className="mb-6">
<h3 className={SECTION_LABEL_CLASS}>Paper</h3>
<div className="flex w-full items-start gap-3 rounded-2xl border border-slate-200 bg-white p-3.5">
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-gradient-to-b from-rose-500 to-rose-600">
<span className="text-[11px] font-bold tracking-[0.02em] text-white">
PDF
</span>
</div>
<div className="min-w-0 flex-1 pt-0.5">
<p className="truncate text-[16px] font-semibold leading-tight text-slate-900">
Yamamoto et al. - 2015 - Basolate...
</p>
<p className="mt-1 text-[14px] leading-tight text-slate-500">
7.8 MB &middot;{" "}
<span className="font-medium text-slate-500">Replace file</span>
</p>
</div>
</div>
</section>

<section className="mb-6">
<h3 className={SECTION_LABEL_CLASS}>Model</h3>
<Button
variant="outline"
className="w-full justify-between rounded-xl border-slate-300 px-3.5 py-3 text-[16px] font-medium text-slate-700"
>
<span>Claude Sonnet 4.6</span>
<ChevronDown className="h-4 w-4 text-slate-500" />
</Button>
</section>

<section className="mb-5">
<h3 className={SECTION_LABEL_CLASS}>Research Question</h3>
<div className="w-full rounded-xl border border-slate-300 bg-white px-3.5 py-3 text-[16px] text-slate-700">
What are the molecular determinants of lumenoid formation in hiPSCs?
</div>
</section>

<div className="mx-1 mb-5 border-t border-slate-200" />

<section>
<div className="mb-2.5 flex items-center justify-between px-1">
<h3 className="text-[18px] font-semibold tracking-[-0.016em] text-slate-800">
Node Types
</h3>
<span className="text-[13px] font-semibold tabular-nums text-slate-500">
{NODE_TYPES.length}/{NODE_TYPES.length}
</span>
</div>

<div className="space-y-1.5">
{NODE_TYPES.map((type) => (
<label
key={type.candidateTag}
className="flex w-full cursor-pointer items-center gap-2.5 rounded-xl border border-slate-200 bg-white px-2.5 py-2.5 text-slate-800 shadow-sm"
>
<Checkbox checked />
<span className="min-w-0 flex-1 text-[16px] font-medium">
{type.label}
</span>
<span className="shrink-0 text-[11px] font-medium text-slate-400">
{type.candidateTag}
</span>
</label>
))}
</div>
</section>
</div>

<div className="border-t border-slate-200/90 bg-white/95 p-4 backdrop-blur-xl">
<p className="mb-2 text-[14px] font-medium text-slate-500">
Ready to run extraction.
</p>
<Button className="w-full rounded-xl bg-slate-900 py-6 text-[17px] font-semibold text-white hover:bg-slate-800">
Re-Extract
</Button>
</div>
</aside>
);
};
13 changes: 13 additions & 0 deletions apps/website/app/(extract)/extract-nodes/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { MainContent } from "./components/MainContent";
import { Sidebar } from "./components/Sidebar";

const ExtractNodesPage = (): React.ReactElement => {
return (
<div className="flex h-full w-full flex-1 flex-col gap-4 p-4 lg:flex-row lg:gap-5 lg:p-5">
<Sidebar />
<MainContent />
</div>
);
};

export default ExtractNodesPage;
Loading
Loading