Skip to content

Commit 9a43f51

Browse files
authored
ENG-1649: Model selector dropdown for extraction sidebar (#965)
* 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. * ENG-1595: Wire extract button; build system prompt from selected node types * ENG-1649: Model selector dropdown for extraction sidebar Adds Select component and supporting shadcn infra (radix-select dep, color tokens in shared tailwind config, CSS variables in globals.css, packages/ui content path). Node type checkboxes render in each node's defined color. * ENG-1649: Simplify Select to concrete colors Drop shadcn theme tokens, CSS variables, animations, and unused exports. Dropdown uses slate colors directly. * Move extraction question into system prompt * ENG-1649: Replace arbitrary text-[*px] with default utilities * 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. * ENG-1649: Drop per-type color from node type checkboxes Revert to default shadcn Checkbox styling for node type rows. * ENG-1649: Preserve Checkbox border-only look on node type rows The Select dropdown's content path addition made the shared Checkbox's data-[state=checked]:bg-primary apply, flipping rows to a solid fill. Override at the call site to keep the border-only style with a dark check mark.
1 parent 6588879 commit 9a43f51

7 files changed

Lines changed: 410 additions & 37 deletions

File tree

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

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,16 @@ import { useRef } from "react";
33
import { Button } from "@repo/ui/components/ui/button";
44
import { Checkbox } from "@repo/ui/components/ui/checkbox";
55
import { Label } from "@repo/ui/components/ui/label";
6+
import {
7+
Select,
8+
SelectContent,
9+
SelectItem,
10+
SelectTrigger,
11+
SelectValue,
12+
} from "@repo/ui/components/ui/select";
613
import { Textarea } from "@repo/ui/components/ui/textarea";
7-
import { ChevronDown, Upload } from "lucide-react";
8-
import { NODE_TYPE_DEFINITIONS } from "~/types/extraction";
14+
import { Upload } from "lucide-react";
15+
import { MODEL_OPTIONS, NODE_TYPE_DEFINITIONS } from "~/types/extraction";
916

1017
const SECTION_LABEL_CLASS =
1118
"mb-3 block px-1 text-lg font-semibold tracking-tight text-slate-800";
@@ -17,6 +24,8 @@ type SidebarProps = {
1724
onResearchQuestionChange: (value: string) => void;
1825
selectedTypes: Set<string>;
1926
onToggleType: (candidateTag: string) => void;
27+
model: string;
28+
onModelChange: (model: string) => void;
2029
onExtract: () => void;
2130
canExtract: boolean;
2231
isExtracting: boolean;
@@ -30,6 +39,8 @@ export const Sidebar = ({
3039
onResearchQuestionChange,
3140
selectedTypes,
3241
onToggleType,
42+
model,
43+
onModelChange,
3344
onExtract,
3445
canExtract,
3546
isExtracting,
@@ -105,13 +116,22 @@ export const Sidebar = ({
105116

106117
<section className="mb-6">
107118
<h3 className={SECTION_LABEL_CLASS}>Model</h3>
108-
<Button
109-
variant="outline"
110-
className="w-full justify-between rounded-xl border-slate-300 px-3.5 py-3 text-base font-medium text-slate-700"
111-
>
112-
<span>Claude Sonnet 4.6</span>
113-
<ChevronDown className="text-slate-500" />
114-
</Button>
119+
<Select value={model} onValueChange={onModelChange}>
120+
<SelectTrigger className="h-12 rounded-xl text-base font-medium">
121+
<SelectValue />
122+
</SelectTrigger>
123+
<SelectContent className="rounded-xl shadow-lg">
124+
{MODEL_OPTIONS.map((option) => (
125+
<SelectItem
126+
key={option.id}
127+
value={option.id}
128+
className="text-base"
129+
>
130+
{option.label}
131+
</SelectItem>
132+
))}
133+
</SelectContent>
134+
</Select>
115135
</section>
116136

117137
<section className="mb-5">
@@ -133,7 +153,7 @@ export const Sidebar = ({
133153
<h3 className="text-lg font-semibold tracking-tight text-slate-800">
134154
Node Types
135155
</h3>
136-
<span className="text-xs font-semibold tabular-nums text-slate-500">
156+
<span className="text-sm font-semibold tabular-nums text-slate-500">
137157
{selectedTypes.size}/{NODE_TYPE_DEFINITIONS.length}
138158
</span>
139159
</div>
@@ -147,6 +167,7 @@ export const Sidebar = ({
147167
<Checkbox
148168
checked={selectedTypes.has(type.candidateTag)}
149169
onCheckedChange={() => onToggleType(type.candidateTag)}
170+
className="data-[state=checked]:bg-transparent data-[state=checked]:text-slate-800"
150171
/>
151172
<span className="min-w-0 flex-1 text-base font-medium">
152173
{type.label}

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
"use client";
22

33
import { useCallback, useState } from "react";
4-
import { NODE_TYPE_DEFINITIONS, type ExtractedNode } from "~/types/extraction";
4+
import {
5+
DEFAULT_MODEL_ID,
6+
NODE_TYPE_DEFINITIONS,
7+
type ExtractedNode,
8+
} from "~/types/extraction";
59
import { buildSystemPrompt } from "~/prompts/extraction";
610
import { MainContent } from "./components/MainContent";
711
import { Sidebar } from "./components/Sidebar";
@@ -98,6 +102,7 @@ const ExtractNodesPage = (): React.ReactElement => {
98102
const [selectedTypes, setSelectedTypes] = useState(
99103
() => new Set(["#evd-candidate", "#clm-candidate"]),
100104
);
105+
const [model, setModel] = useState(DEFAULT_MODEL_ID);
101106
const [isExtracting, setIsExtracting] = useState(false);
102107
const [extractionError, setExtractionError] = useState<string | null>(null);
103108
const [nodes] = useState<ExtractedNode[]>(SAMPLE_NODES);
@@ -132,7 +137,7 @@ const ExtractNodesPage = (): React.ReactElement => {
132137
const requestBody = {
133138
pdfBase64,
134139
provider: "anthropic",
135-
model: "claude-sonnet-4-6",
140+
model,
136141
systemPrompt,
137142
};
138143
const response = await fetch("/api/ai/extract", {
@@ -152,7 +157,7 @@ const ExtractNodesPage = (): React.ReactElement => {
152157
} finally {
153158
setIsExtracting(false);
154159
}
155-
}, [pdfFile, researchQuestion, selectedTypes]);
160+
}, [pdfFile, researchQuestion, selectedTypes, model]);
156161

157162
return (
158163
<div className="flex h-full w-full flex-1 flex-col gap-4 p-4 lg:flex-row lg:gap-5 lg:p-5">
@@ -163,6 +168,8 @@ const ExtractNodesPage = (): React.ReactElement => {
163168
onResearchQuestionChange={setResearchQuestion}
164169
selectedTypes={selectedTypes}
165170
onToggleType={toggleType}
171+
model={model}
172+
onModelChange={setModel}
166173
onExtract={() => void handleExtract()}
167174
canExtract={canExtract}
168175
isExtracting={isExtracting}

apps/website/app/types/extraction.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,19 @@ export type NodeTypeDefinition = {
6464
color?: string;
6565
};
6666

67+
type ModelOption = {
68+
id: string;
69+
label: string;
70+
};
71+
72+
export const MODEL_OPTIONS: ModelOption[] = [
73+
{ id: "claude-opus-4-6", label: "Claude Opus 4.6" },
74+
{ id: "claude-sonnet-4-6", label: "Claude Sonnet 4.6" },
75+
{ id: "claude-haiku-4-5", label: "Claude Haiku 4.5" },
76+
];
77+
78+
export const DEFAULT_MODEL_ID = "claude-sonnet-4-6";
79+
6780
export const NODE_TYPE_DEFINITIONS: NodeTypeDefinition[] = [
6881
{
6982
label: "Evidence",

apps/website/tailwind.config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ const config: Pick<Config, "content" | "presets" | "plugins"> = {
55
content: [
66
"./components/**/*.{js,ts,jsx,tsx,mdx}",
77
"./app/**/*.{js,ts,jsx,tsx,mdx}",
8+
"../../packages/ui/src/**/*.{js,ts,jsx,tsx,mdx}",
89
],
910
presets: [sharedConfig],
10-
plugins: [require('@tailwindcss/typography')],
11+
plugins: [require("@tailwindcss/typography")],
1112
};
1213

1314
export default config;

packages/ui/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"dependencies": {
3535
"@radix-ui/react-checkbox": "^1.3.3",
3636
"@radix-ui/react-label": "^2.1.7",
37+
"@radix-ui/react-select": "^2.2.6",
3738
"@radix-ui/react-slot": "^1.2.4",
3839
"class-variance-authority": "^0.7.1",
3940
"clsx": "^2.1.1",
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
"use client";
2+
3+
import * as React from "react";
4+
import * as SelectPrimitive from "@radix-ui/react-select";
5+
import { Check, ChevronDown } from "lucide-react";
6+
7+
import { cn } from "@repo/ui/lib/utils";
8+
9+
const Select = SelectPrimitive.Root;
10+
11+
const SelectValue = SelectPrimitive.Value;
12+
13+
const SelectTrigger = React.forwardRef<
14+
React.ElementRef<typeof SelectPrimitive.Trigger>,
15+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
16+
>(({ className, children, ...props }, ref) => (
17+
<SelectPrimitive.Trigger
18+
ref={ref}
19+
className={cn(
20+
"flex h-10 w-full items-center justify-between rounded-md border border-slate-200 bg-white px-3 py-2 text-sm ring-offset-white focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2",
21+
className,
22+
)}
23+
{...props}
24+
>
25+
{children}
26+
<SelectPrimitive.Icon asChild>
27+
<ChevronDown className="h-4 w-4 shrink-0 opacity-50 data-[state=open]:rotate-180" />
28+
</SelectPrimitive.Icon>
29+
</SelectPrimitive.Trigger>
30+
));
31+
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
32+
33+
const SelectContent = React.forwardRef<
34+
React.ElementRef<typeof SelectPrimitive.Content>,
35+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
36+
>(({ className, children, position = "popper", ...props }, ref) => (
37+
<SelectPrimitive.Portal>
38+
<SelectPrimitive.Content
39+
ref={ref}
40+
className={cn(
41+
"relative z-50 max-h-96 overflow-hidden rounded-md border border-slate-200 bg-white text-slate-900 shadow-md",
42+
position === "popper" && "w-[var(--radix-select-trigger-width)]",
43+
className,
44+
)}
45+
position={position}
46+
{...props}
47+
>
48+
<SelectPrimitive.Viewport className="max-h-96 overflow-y-auto p-1">
49+
{children}
50+
</SelectPrimitive.Viewport>
51+
</SelectPrimitive.Content>
52+
</SelectPrimitive.Portal>
53+
));
54+
SelectContent.displayName = SelectPrimitive.Content.displayName;
55+
56+
const SelectItem = React.forwardRef<
57+
React.ElementRef<typeof SelectPrimitive.Item>,
58+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
59+
>(({ className, children, ...props }, ref) => (
60+
<SelectPrimitive.Item
61+
ref={ref}
62+
className={cn(
63+
"relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-3 pr-8 text-sm outline-none focus:bg-slate-100 data-[state=checked]:font-semibold",
64+
className,
65+
)}
66+
{...props}
67+
>
68+
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
69+
<span className="absolute right-2 flex h-4 w-4 items-center justify-center">
70+
<SelectPrimitive.ItemIndicator>
71+
<Check className="h-4 w-4" />
72+
</SelectPrimitive.ItemIndicator>
73+
</span>
74+
</SelectPrimitive.Item>
75+
));
76+
SelectItem.displayName = SelectPrimitive.Item.displayName;
77+
78+
export { Select, SelectValue, SelectTrigger, SelectContent, SelectItem };

0 commit comments

Comments
 (0)