Skip to content

Commit 9b94314

Browse files
authored
ENG-1592, ENG-1593, ENG-1594: Wire up sidebar inputs for extraction page (#958)
* ENG-1592, ENG-1593, ENG-1594: Wire up sidebar inputs for extraction page PDF upload, research question textarea, and toggleable node types with shared NodeTypeDefinition type. Evidence and Claim selected by default. * Use shadcn Button/Label and default Tailwind utilities in extraction Sidebar Address PR #958 review: swap native <button>/<label> for @repo/ui Button/Label so the sidebar uses the shared component presentation, and replace arbitrary text-[Npx]/tracking-[Nem]/rounded-[Npx]/border-[Npx] classes with Tailwind defaults (text-lg/base/sm/xs, tracking-tight/wide, rounded-3xl, border-2). Adds a new Label component to @repo/ui with @radix-ui/react-label as a direct dep (previously transitive). * Drop arbitrary sidebar width/shadow in favor of Tailwind defaults Swap lg:w-[390px] + xl:w-[420px] for lg:w-96, and the custom shadow-[0_26px_52px_-38px_rgba(15,23,42,0.6)] for shadow-xl. Defers exact visual tuning to the design phase. * Enlarge research question textarea to avoid scrollbar on placeholder min-h-20 (80px) clipped the 3-line placeholder. Bump to min-h-36 (144px) so typical research questions fit without an inner scrollbar.
1 parent 60bf5fb commit 9b94314

7 files changed

Lines changed: 245 additions & 87 deletions

File tree

apps/website/app/(extract)/extract-nodes/components/Sidebar.tsx

Lines changed: 110 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,138 +1,162 @@
1+
"use client";
2+
import { useRef } from "react";
13
import { Button } from "@repo/ui/components/ui/button";
24
import { Checkbox } from "@repo/ui/components/ui/checkbox";
3-
import { ChevronDown } from "lucide-react";
4-
5-
const NODE_TYPES = [
6-
{
7-
label: "Claim",
8-
description:
9-
"An assertion about how something works or should work. Usually a single declarative sentence.",
10-
candidateTag: "#clm-candidate",
11-
color: "#7DA13E",
12-
},
13-
{
14-
label: "Question",
15-
description:
16-
"An unknown being explored through research. Framed as a question that can be investigated.",
17-
candidateTag: "#que-candidate",
18-
color: "#99890E",
19-
},
20-
{
21-
label: "Hypothesis",
22-
description:
23-
"A proposed answer to a question. Collects evidence for or against.",
24-
candidateTag: "#hyp-candidate",
25-
color: "#7C4DFF",
26-
},
27-
{
28-
label: "Evidence",
29-
description:
30-
"A discrete observation from a published dataset or experiment. Usually in past tense with observable, model system, and method.",
31-
candidateTag: "#evd-candidate",
32-
color: "#dc0c4a",
33-
},
34-
{
35-
label: "Result",
36-
description:
37-
"A discrete observation from ongoing research, in past tense. Includes source context.",
38-
candidateTag: "#res-candidate",
39-
color: "#E6A23C",
40-
},
41-
{
42-
label: "Source",
43-
description: "A referenced publication or external resource.",
44-
candidateTag: "#src-candidate",
45-
color: "#9E9E9E",
46-
},
47-
{
48-
label: "Theory",
49-
description: "A theoretical framework or model that explains phenomena.",
50-
candidateTag: "#the-candidate",
51-
color: "#8B5CF6",
52-
},
53-
];
5+
import { Label } from "@repo/ui/components/ui/label";
6+
import { Textarea } from "@repo/ui/components/ui/textarea";
7+
import { ChevronDown, Upload } from "lucide-react";
8+
import { NODE_TYPE_DEFINITIONS } from "~/types/extraction";
549

5510
const SECTION_LABEL_CLASS =
56-
"mb-3 block px-1 text-[18px] font-semibold tracking-[-0.016em] text-slate-800";
11+
"mb-3 block px-1 text-lg font-semibold tracking-tight text-slate-800";
12+
13+
type SidebarProps = {
14+
pdfFile: File | null;
15+
onFileSelect: (file: File) => void;
16+
researchQuestion: string;
17+
onResearchQuestionChange: (value: string) => void;
18+
selectedTypes: Set<string>;
19+
onToggleType: (candidateTag: string) => void;
20+
};
21+
22+
export const Sidebar = ({
23+
pdfFile,
24+
onFileSelect,
25+
researchQuestion,
26+
onResearchQuestionChange,
27+
selectedTypes,
28+
onToggleType,
29+
}: SidebarProps): React.ReactElement => {
30+
const fileInputRef = useRef<HTMLInputElement>(null);
31+
32+
const handleFileInput = (event: React.ChangeEvent<HTMLInputElement>) => {
33+
const file = event.target.files?.[0];
34+
if (file?.type === "application/pdf") {
35+
onFileSelect(file);
36+
}
37+
event.target.value = "";
38+
};
5739

58-
export const Sidebar = (): React.ReactElement => {
5940
return (
60-
<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]">
41+
<aside className="flex w-full shrink-0 flex-col overflow-hidden rounded-3xl border border-slate-200/85 bg-white shadow-xl lg:w-96">
6142
<div className="flex-1 overflow-y-auto p-4 lg:p-5">
6243
<section className="mb-6">
6344
<h3 className={SECTION_LABEL_CLASS}>Paper</h3>
64-
<div className="flex w-full items-start gap-3 rounded-2xl border border-slate-200 bg-white p-3.5">
65-
<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">
66-
<span className="text-[11px] font-bold tracking-[0.02em] text-white">
67-
PDF
68-
</span>
69-
</div>
70-
<div className="min-w-0 flex-1 pt-0.5">
71-
<p className="truncate text-[16px] font-semibold leading-tight text-slate-900">
72-
Yamamoto et al. - 2015 - Basolate...
73-
</p>
74-
<p className="mt-1 text-[14px] leading-tight text-slate-500">
75-
7.8 MB &middot;{" "}
76-
<span className="font-medium text-slate-500">Replace file</span>
77-
</p>
78-
</div>
79-
</div>
45+
<input
46+
ref={fileInputRef}
47+
type="file"
48+
accept=".pdf,application/pdf"
49+
className="hidden"
50+
onChange={handleFileInput}
51+
/>
52+
{pdfFile ? (
53+
<Button
54+
type="button"
55+
variant="outline"
56+
onClick={() => fileInputRef.current?.click()}
57+
className="group h-auto w-full items-start justify-start gap-3 whitespace-normal rounded-2xl border-slate-200 p-3.5 text-left hover:border-slate-300 hover:bg-white"
58+
>
59+
<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">
60+
<span className="text-xs font-bold tracking-wide text-white">
61+
PDF
62+
</span>
63+
</div>
64+
<div className="min-w-0 flex-1 pt-0.5">
65+
<p className="truncate text-base font-semibold leading-tight text-slate-900">
66+
{pdfFile.name}
67+
</p>
68+
<p className="mt-1 text-sm leading-tight text-slate-500">
69+
{(pdfFile.size / 1024 / 1024).toFixed(1)} MB &middot;{" "}
70+
<span className="font-medium transition-colors group-hover:text-sky-700">
71+
Replace file
72+
</span>
73+
</p>
74+
</div>
75+
</Button>
76+
) : (
77+
<Button
78+
type="button"
79+
variant="outline"
80+
onClick={() => fileInputRef.current?.click()}
81+
className="h-auto w-full flex-col gap-2.5 rounded-2xl border-2 border-dashed border-slate-300 px-4 py-9 text-center hover:border-slate-400 hover:bg-slate-50 [&_svg]:size-5"
82+
>
83+
<div className="flex h-11 w-11 items-center justify-center rounded-xl bg-slate-100">
84+
<Upload className="text-slate-500" />
85+
</div>
86+
<div>
87+
<p className="text-base font-semibold text-slate-800">
88+
Upload PDF
89+
</p>
90+
<p className="mt-1 text-sm text-slate-500">
91+
Click to choose a file
92+
</p>
93+
</div>
94+
</Button>
95+
)}
8096
</section>
8197

8298
<section className="mb-6">
8399
<h3 className={SECTION_LABEL_CLASS}>Model</h3>
84100
<Button
85101
variant="outline"
86-
className="w-full justify-between rounded-xl border-slate-300 px-3.5 py-3 text-[16px] font-medium text-slate-700"
102+
className="w-full justify-between rounded-xl border-slate-300 px-3.5 py-3 text-base font-medium text-slate-700"
87103
>
88104
<span>Claude Sonnet 4.6</span>
89-
<ChevronDown className="h-4 w-4 text-slate-500" />
105+
<ChevronDown className="text-slate-500" />
90106
</Button>
91107
</section>
92108

93109
<section className="mb-5">
94110
<h3 className={SECTION_LABEL_CLASS}>Research Question</h3>
95-
<div className="w-full rounded-xl border border-slate-300 bg-white px-3.5 py-3 text-[16px] text-slate-700">
96-
What are the molecular determinants of lumenoid formation in hiPSCs?
97-
</div>
111+
<Textarea
112+
value={researchQuestion}
113+
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
114+
onResearchQuestionChange(e.target.value)
115+
}
116+
placeholder="e.g., What are the molecular determinants of lumenoid formation in hiPSCs?"
117+
className="min-h-36 resize-none rounded-xl border-slate-300 bg-white px-3.5 py-3 text-base text-slate-700 placeholder:text-slate-400"
118+
/>
98119
</section>
99120

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

102123
<section>
103124
<div className="mb-2.5 flex items-center justify-between px-1">
104-
<h3 className="text-[18px] font-semibold tracking-[-0.016em] text-slate-800">
125+
<h3 className="text-lg font-semibold tracking-tight text-slate-800">
105126
Node Types
106127
</h3>
107-
<span className="text-[13px] font-semibold tabular-nums text-slate-500">
108-
{NODE_TYPES.length}/{NODE_TYPES.length}
128+
<span className="text-xs font-semibold tabular-nums text-slate-500">
129+
{selectedTypes.size}/{NODE_TYPE_DEFINITIONS.length}
109130
</span>
110131
</div>
111132

112133
<div className="space-y-1.5">
113-
{NODE_TYPES.map((type) => (
114-
<label
134+
{NODE_TYPE_DEFINITIONS.map((type) => (
135+
<Label
115136
key={type.candidateTag}
116137
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"
117138
>
118-
<Checkbox checked />
119-
<span className="min-w-0 flex-1 text-[16px] font-medium">
139+
<Checkbox
140+
checked={selectedTypes.has(type.candidateTag)}
141+
onCheckedChange={() => onToggleType(type.candidateTag)}
142+
/>
143+
<span className="min-w-0 flex-1 text-base font-medium">
120144
{type.label}
121145
</span>
122-
<span className="shrink-0 text-[11px] font-medium text-slate-400">
146+
<span className="shrink-0 text-xs font-medium text-slate-400">
123147
{type.candidateTag}
124148
</span>
125-
</label>
149+
</Label>
126150
))}
127151
</div>
128152
</section>
129153
</div>
130154

131155
<div className="border-t border-slate-200/90 bg-white/95 p-4 backdrop-blur-xl">
132-
<p className="mb-2 text-[14px] font-medium text-slate-500">
156+
<p className="mb-2 text-sm font-medium text-slate-500">
133157
Ready to run extraction.
134158
</p>
135-
<Button className="w-full rounded-xl bg-slate-900 py-6 text-[17px] font-semibold text-white hover:bg-slate-800">
159+
<Button className="w-full rounded-xl bg-slate-900 py-6 text-lg font-semibold text-white hover:bg-slate-800">
136160
Re-Extract
137161
</Button>
138162
</div>

apps/website/app/(extract)/extract-nodes/page.tsx

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,38 @@
1+
"use client";
2+
3+
import { useCallback, useState } from "react";
14
import { MainContent } from "./components/MainContent";
25
import { Sidebar } from "./components/Sidebar";
36

47
const ExtractNodesPage = (): React.ReactElement => {
8+
const [pdfFile, setPdfFile] = useState<File | null>(null);
9+
const [researchQuestion, setResearchQuestion] = useState("");
10+
const [selectedTypes, setSelectedTypes] = useState(
11+
() => new Set(["#evd-candidate", "#clm-candidate"]),
12+
);
13+
14+
const toggleType = useCallback((candidateTag: string) => {
15+
setSelectedTypes((prev) => {
16+
const next = new Set(prev);
17+
if (next.has(candidateTag)) {
18+
next.delete(candidateTag);
19+
} else {
20+
next.add(candidateTag);
21+
}
22+
return next;
23+
});
24+
}, []);
25+
526
return (
627
<div className="flex h-full w-full flex-1 flex-col gap-4 p-4 lg:flex-row lg:gap-5 lg:p-5">
7-
<Sidebar />
28+
<Sidebar
29+
pdfFile={pdfFile}
30+
onFileSelect={setPdfFile}
31+
researchQuestion={researchQuestion}
32+
onResearchQuestionChange={setResearchQuestion}
33+
selectedTypes={selectedTypes}
34+
onToggleType={toggleType}
35+
/>
836
<MainContent />
937
</div>
1038
);

apps/website/app/types/extraction.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,48 @@ export const EXTRACTION_RESULT_JSON_SCHEMA: Record<string, unknown> = {
5757
export type ExtractionResponse =
5858
| { success: true; data: ExtractionResult }
5959
| { success: false; error: string };
60+
61+
export type NodeTypeDefinition = {
62+
label: string;
63+
definition: string;
64+
candidateTag: string;
65+
color?: string;
66+
};
67+
68+
export const NODE_TYPE_DEFINITIONS: NodeTypeDefinition[] = [
69+
{
70+
label: "Evidence",
71+
definition:
72+
"A specific empirical observation from a particular study. One distinct statistical test, measurement, or analytical finding. Past tense. Includes observable, model system, method.",
73+
candidateTag: "#evd-candidate",
74+
color: "#DB134A",
75+
},
76+
{
77+
label: "Claim",
78+
definition:
79+
"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.",
80+
candidateTag: "#clm-candidate",
81+
color: "#7DA13E",
82+
},
83+
{
84+
label: "Question",
85+
definition:
86+
"A research question — explicitly stated or implied by a gap in the literature. Open-ended, answerable by empirical evidence.",
87+
candidateTag: "#que-candidate",
88+
color: "#99890E",
89+
},
90+
{
91+
label: "Pattern",
92+
definition:
93+
"A conceptual class — a theoretical object, heuristic, design pattern, or methodological approach — abstracted from specific implementations.",
94+
candidateTag: "#ptn-candidate",
95+
color: "#E040FB",
96+
},
97+
{
98+
label: "Artifact",
99+
definition:
100+
"A specific concrete system, tool, standard, dataset, or protocol that instantiates one or more patterns.",
101+
candidateTag: "#art-candidate",
102+
color: "#67C23A",
103+
},
104+
];

packages/ui/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
},
3434
"dependencies": {
3535
"@radix-ui/react-checkbox": "^1.3.3",
36+
"@radix-ui/react-label": "^2.1.7",
3637
"@radix-ui/react-slot": "^1.2.4",
3738
"class-variance-authority": "^0.7.1",
3839
"clsx": "^2.1.1",
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"use client";
2+
3+
import * as React from "react";
4+
import * as LabelPrimitive from "@radix-ui/react-label";
5+
import { cva, type VariantProps } from "class-variance-authority";
6+
7+
import { cn } from "@repo/ui/lib/utils";
8+
9+
const labelVariants = cva(
10+
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
11+
);
12+
13+
const Label = React.forwardRef<
14+
React.ElementRef<typeof LabelPrimitive.Root>,
15+
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
16+
VariantProps<typeof labelVariants>
17+
>(({ className, ...props }, ref) => (
18+
<LabelPrimitive.Root
19+
ref={ref}
20+
className={cn(labelVariants(), className)}
21+
{...props}
22+
/>
23+
));
24+
Label.displayName = LabelPrimitive.Root.displayName;
25+
26+
export { Label };
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import * as React from "react";
2+
3+
import { cn } from "@repo/ui/lib/utils";
4+
5+
const Textarea = React.forwardRef<
6+
HTMLTextAreaElement,
7+
React.ComponentProps<"textarea">
8+
>(({ className, ...props }, ref) => {
9+
return (
10+
<textarea
11+
className={cn(
12+
"border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex min-h-[80px] w-full rounded-md border px-3 py-2 text-base focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
13+
className,
14+
)}
15+
ref={ref}
16+
{...props}
17+
/>
18+
);
19+
});
20+
Textarea.displayName = "Textarea";
21+
22+
export { Textarea };

0 commit comments

Comments
 (0)