XspecT 0.4.1__py3-none-any.whl → 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of XspecT might be problematic. Click here for more details.
- xspect/classify.py +32 -0
- xspect/file_io.py +3 -9
- xspect/filter_sequences.py +56 -0
- xspect/main.py +13 -18
- xspect/mlst_feature/mlst_helper.py +102 -13
- xspect/mlst_feature/pub_mlst_handler.py +32 -6
- xspect/models/probabilistic_filter_mlst_model.py +160 -32
- xspect/models/probabilistic_filter_model.py +1 -0
- xspect/ncbi.py +8 -6
- xspect/train.py +13 -5
- xspect/web.py +173 -0
- xspect/xspect-web/.gitignore +24 -0
- xspect/xspect-web/README.md +54 -0
- xspect/xspect-web/components.json +21 -0
- xspect/xspect-web/dist/assets/index-CMG4V7fZ.js +290 -0
- xspect/xspect-web/dist/assets/index-jIKg1HIy.css +1 -0
- xspect/xspect-web/dist/index.html +14 -0
- xspect/xspect-web/dist/vite.svg +1 -0
- xspect/xspect-web/eslint.config.js +28 -0
- xspect/xspect-web/index.html +13 -0
- xspect/xspect-web/package-lock.json +6865 -0
- xspect/xspect-web/package.json +58 -0
- xspect/xspect-web/pnpm-lock.yaml +4317 -0
- xspect/xspect-web/public/vite.svg +1 -0
- xspect/xspect-web/src/App.tsx +29 -0
- xspect/xspect-web/src/api.tsx +62 -0
- xspect/xspect-web/src/assets/react.svg +1 -0
- xspect/xspect-web/src/components/classification-form.tsx +284 -0
- xspect/xspect-web/src/components/classify.tsx +18 -0
- xspect/xspect-web/src/components/data-table.tsx +78 -0
- xspect/xspect-web/src/components/dropdown-checkboxes.tsx +63 -0
- xspect/xspect-web/src/components/dropdown-slider.tsx +42 -0
- xspect/xspect-web/src/components/filter-form.tsx +423 -0
- xspect/xspect-web/src/components/filter.tsx +15 -0
- xspect/xspect-web/src/components/header.tsx +46 -0
- xspect/xspect-web/src/components/landing.tsx +7 -0
- xspect/xspect-web/src/components/models-details.tsx +138 -0
- xspect/xspect-web/src/components/models.tsx +53 -0
- xspect/xspect-web/src/components/result-chart.tsx +44 -0
- xspect/xspect-web/src/components/result.tsx +155 -0
- xspect/xspect-web/src/components/spinner.tsx +30 -0
- xspect/xspect-web/src/components/ui/accordion.tsx +64 -0
- xspect/xspect-web/src/components/ui/button.tsx +59 -0
- xspect/xspect-web/src/components/ui/card.tsx +92 -0
- xspect/xspect-web/src/components/ui/chart.tsx +351 -0
- xspect/xspect-web/src/components/ui/command.tsx +175 -0
- xspect/xspect-web/src/components/ui/dialog.tsx +135 -0
- xspect/xspect-web/src/components/ui/dropdown-menu.tsx +255 -0
- xspect/xspect-web/src/components/ui/file-upload.tsx +1459 -0
- xspect/xspect-web/src/components/ui/form.tsx +165 -0
- xspect/xspect-web/src/components/ui/input.tsx +21 -0
- xspect/xspect-web/src/components/ui/label.tsx +24 -0
- xspect/xspect-web/src/components/ui/navigation-menu.tsx +168 -0
- xspect/xspect-web/src/components/ui/popover.tsx +46 -0
- xspect/xspect-web/src/components/ui/select.tsx +183 -0
- xspect/xspect-web/src/components/ui/separator.tsx +26 -0
- xspect/xspect-web/src/components/ui/slider.tsx +61 -0
- xspect/xspect-web/src/components/ui/switch.tsx +29 -0
- xspect/xspect-web/src/components/ui/table.tsx +113 -0
- xspect/xspect-web/src/components/ui/tabs.tsx +64 -0
- xspect/xspect-web/src/index.css +120 -0
- xspect/xspect-web/src/lib/utils.ts +6 -0
- xspect/xspect-web/src/main.tsx +10 -0
- xspect/xspect-web/src/types.tsx +34 -0
- xspect/xspect-web/src/utils.tsx +6 -0
- xspect/xspect-web/src/vite-env.d.ts +1 -0
- xspect/xspect-web/tsconfig.app.json +32 -0
- xspect/xspect-web/tsconfig.json +13 -0
- xspect/xspect-web/tsconfig.node.json +24 -0
- xspect/xspect-web/vite.config.ts +24 -0
- {xspect-0.4.1.dist-info → xspect-0.5.0.dist-info}/METADATA +6 -8
- xspect-0.5.0.dist-info/RECORD +85 -0
- {xspect-0.4.1.dist-info → xspect-0.5.0.dist-info}/WHEEL +1 -1
- xspect/fastapi.py +0 -102
- xspect-0.4.1.dist-info/RECORD +0 -24
- {xspect-0.4.1.dist-info → xspect-0.5.0.dist-info}/entry_points.txt +0 -0
- {xspect-0.4.1.dist-info → xspect-0.5.0.dist-info}/licenses/LICENSE +0 -0
- {xspect-0.4.1.dist-info → xspect-0.5.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
import { zodResolver } from "@hookform/resolvers/zod"
|
|
2
|
+
import { useForm } from "react-hook-form"
|
|
3
|
+
import { z } from "zod"
|
|
4
|
+
import { Button } from "@/components/ui/button"
|
|
5
|
+
import { Switch } from "@/components/ui/switch"
|
|
6
|
+
import { Input } from "@/components/ui/input"
|
|
7
|
+
import {
|
|
8
|
+
Form,
|
|
9
|
+
FormControl,
|
|
10
|
+
FormDescription,
|
|
11
|
+
FormField,
|
|
12
|
+
FormItem,
|
|
13
|
+
FormLabel,
|
|
14
|
+
FormMessage,
|
|
15
|
+
} from "@/components/ui/form"
|
|
16
|
+
import {
|
|
17
|
+
Select,
|
|
18
|
+
SelectContent,
|
|
19
|
+
SelectItem,
|
|
20
|
+
SelectTrigger,
|
|
21
|
+
SelectValue,
|
|
22
|
+
} from "@/components/ui/select"
|
|
23
|
+
import { getModelMetadata, getModels, uploadFile } from "../api"
|
|
24
|
+
import {
|
|
25
|
+
FileUpload,
|
|
26
|
+
FileUploadDropzone,
|
|
27
|
+
FileUploadItem,
|
|
28
|
+
FileUploadItemDelete,
|
|
29
|
+
FileUploadItemMetadata,
|
|
30
|
+
FileUploadItemPreview,
|
|
31
|
+
FileUploadItemProgress,
|
|
32
|
+
FileUploadList,
|
|
33
|
+
FileUploadTrigger,
|
|
34
|
+
} from "@/components/ui/file-upload";
|
|
35
|
+
import { Check, ChevronsUpDown, Upload, X } from "lucide-react";
|
|
36
|
+
import { useState, useCallback, useEffect } from "react"
|
|
37
|
+
import { useNavigate } from "react-router-dom"
|
|
38
|
+
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
|
39
|
+
import { ModelMetadata } from "../types"
|
|
40
|
+
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"
|
|
41
|
+
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "./ui/command"
|
|
42
|
+
import { cn } from "@/lib/utils"
|
|
43
|
+
|
|
44
|
+
const FormSchema = z.object({
|
|
45
|
+
input_file: z.string().min(1, "Please upload a file"),
|
|
46
|
+
filter_type: z.enum([
|
|
47
|
+
"Genus",
|
|
48
|
+
"Species",
|
|
49
|
+
]),
|
|
50
|
+
model: z.string(),
|
|
51
|
+
filter_species: z.string().optional(),
|
|
52
|
+
filter_mode: z.enum([
|
|
53
|
+
"threshold",
|
|
54
|
+
"maximum"
|
|
55
|
+
]),
|
|
56
|
+
threshold: z.number().min(0).max(1),
|
|
57
|
+
sparse_sampling: z.boolean(),
|
|
58
|
+
sparse_sampling_step: z.number().min(1).max(500)
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
export default function FilterForm() {
|
|
62
|
+
|
|
63
|
+
const [models, setModels] = useState<Record<string, string[]>>({});
|
|
64
|
+
const [modelMetadata, setModelMetadata] = useState<ModelMetadata | null>(null);
|
|
65
|
+
|
|
66
|
+
const form = useForm<z.infer<typeof FormSchema>>({
|
|
67
|
+
resolver: zodResolver(FormSchema),
|
|
68
|
+
defaultValues: {
|
|
69
|
+
filter_type: "Species",
|
|
70
|
+
model: undefined,
|
|
71
|
+
filter_mode: "threshold",
|
|
72
|
+
threshold: 0.7,
|
|
73
|
+
sparse_sampling: false,
|
|
74
|
+
sparse_sampling_step: 1,
|
|
75
|
+
},
|
|
76
|
+
})
|
|
77
|
+
const [files, setFiles] = useState<File[]>([]);
|
|
78
|
+
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
getModels()
|
|
81
|
+
.then((data) => {
|
|
82
|
+
setModels(data);
|
|
83
|
+
})
|
|
84
|
+
.catch((error) => {
|
|
85
|
+
console.error("Error fetching models:", error);
|
|
86
|
+
});
|
|
87
|
+
}, []);
|
|
88
|
+
|
|
89
|
+
const selectedModel = form.watch("model") !== undefined && form.watch("filter_type") !== undefined
|
|
90
|
+
? `${form.watch("model")}-${form.watch("filter_type")}`.toLowerCase()
|
|
91
|
+
: undefined;
|
|
92
|
+
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (models && selectedModel) {
|
|
95
|
+
getModelMetadata(selectedModel)
|
|
96
|
+
.then((data) => {
|
|
97
|
+
setModelMetadata(data);
|
|
98
|
+
})
|
|
99
|
+
.catch((error) => {
|
|
100
|
+
console.error("Error fetching model metadata:", error);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}, [models, selectedModel, form]);
|
|
104
|
+
|
|
105
|
+
const navigate = useNavigate()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
const onUpload = useCallback(
|
|
109
|
+
async (
|
|
110
|
+
files: File[],
|
|
111
|
+
{
|
|
112
|
+
onSuccess,
|
|
113
|
+
onError,
|
|
114
|
+
}: {
|
|
115
|
+
onSuccess: (file: File) => void;
|
|
116
|
+
onError: (file: File, error: Error) => void;
|
|
117
|
+
},
|
|
118
|
+
) => {
|
|
119
|
+
const file = files[0]
|
|
120
|
+
try {
|
|
121
|
+
const { filename } = await uploadFile(file)
|
|
122
|
+
onSuccess(file)
|
|
123
|
+
console.log("File uploaded successfully:", filename)
|
|
124
|
+
form.setValue("input_file", filename)
|
|
125
|
+
} catch (error) {
|
|
126
|
+
onError(file, error as Error)
|
|
127
|
+
console.error("Error uploading file:", error)
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
[form]
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const onFileReject = useCallback((file: File, message: string) => {
|
|
134
|
+
console.log(message, {
|
|
135
|
+
description: `"${file.name.length > 20 ? `${file.name.slice(0, 20)}...` : file.name}" has been rejected`,
|
|
136
|
+
});
|
|
137
|
+
}, []);
|
|
138
|
+
|
|
139
|
+
function onSubmit(data: z.infer<typeof FormSchema>) {
|
|
140
|
+
alert("Coming soon! This feature is not yet implemented.");
|
|
141
|
+
console.log("Form data:", data);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
|
|
146
|
+
<Form {...form}>
|
|
147
|
+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
|
148
|
+
<FormField
|
|
149
|
+
control={form.control}
|
|
150
|
+
name="input_file"
|
|
151
|
+
render={({ field }) => (
|
|
152
|
+
<FormItem>
|
|
153
|
+
<FormLabel>Input File</FormLabel>
|
|
154
|
+
<FormControl>
|
|
155
|
+
<FileUpload
|
|
156
|
+
value={files}
|
|
157
|
+
onValueChange={setFiles}
|
|
158
|
+
onUpload={onUpload}
|
|
159
|
+
onFileReject={onFileReject}
|
|
160
|
+
maxFiles={1}
|
|
161
|
+
multiple
|
|
162
|
+
>
|
|
163
|
+
{files.length === 0 && (
|
|
164
|
+
<FileUploadDropzone>
|
|
165
|
+
<div className="flex flex-col items-center gap-1 text-center">
|
|
166
|
+
<div className="flex items-center justify-center rounded-full border p-2.5">
|
|
167
|
+
<Upload className="size-6 text-muted-foreground" />
|
|
168
|
+
</div>
|
|
169
|
+
<p className="font-medium text-sm">Drag & drop file here</p>
|
|
170
|
+
<p className="text-muted-foreground text-xs">
|
|
171
|
+
Or click to browse
|
|
172
|
+
</p>
|
|
173
|
+
</div>
|
|
174
|
+
<FileUploadTrigger asChild>
|
|
175
|
+
<Button variant="outline" size="sm" className="mt-2 w-fit">
|
|
176
|
+
Browse files
|
|
177
|
+
</Button>
|
|
178
|
+
</FileUploadTrigger>
|
|
179
|
+
</FileUploadDropzone>
|
|
180
|
+
)}
|
|
181
|
+
<FileUploadList>
|
|
182
|
+
{files.map((file, index) => (
|
|
183
|
+
<FileUploadItem key={index} value={file} className="flex-col">
|
|
184
|
+
<div className="flex w-full items-center gap-2">
|
|
185
|
+
<FileUploadItemPreview />
|
|
186
|
+
<FileUploadItemMetadata />
|
|
187
|
+
<FileUploadItemDelete asChild>
|
|
188
|
+
<Button variant="ghost" size="icon" className="size-7" onClick={() => {
|
|
189
|
+
setFiles((prev) => prev.filter((_, i) => i !== index));
|
|
190
|
+
form.setValue("input_file", "")
|
|
191
|
+
}}>
|
|
192
|
+
<X />
|
|
193
|
+
</Button>
|
|
194
|
+
</FileUploadItemDelete>
|
|
195
|
+
</div>
|
|
196
|
+
<FileUploadItemProgress />
|
|
197
|
+
</FileUploadItem>
|
|
198
|
+
))}
|
|
199
|
+
</FileUploadList>
|
|
200
|
+
</FileUpload>
|
|
201
|
+
</FormControl>
|
|
202
|
+
<FormDescription>
|
|
203
|
+
Upload the file you would like to filter.
|
|
204
|
+
</FormDescription>
|
|
205
|
+
<FormMessage />
|
|
206
|
+
</FormItem>
|
|
207
|
+
)}
|
|
208
|
+
/>
|
|
209
|
+
<FormField
|
|
210
|
+
control={form.control}
|
|
211
|
+
name="filter_type"
|
|
212
|
+
render={({ field }) => (
|
|
213
|
+
<FormItem>
|
|
214
|
+
<FormLabel>Filter Type</FormLabel>
|
|
215
|
+
<FormControl>
|
|
216
|
+
<Select defaultValue="Species" onValueChange={(value) => field.onChange(value)}>
|
|
217
|
+
<SelectTrigger className="w-full">
|
|
218
|
+
<SelectValue />
|
|
219
|
+
</SelectTrigger>
|
|
220
|
+
<SelectContent>
|
|
221
|
+
<SelectItem value="Genus">Genus</SelectItem>
|
|
222
|
+
<SelectItem value="Species">Species</SelectItem>
|
|
223
|
+
</SelectContent>
|
|
224
|
+
</Select>
|
|
225
|
+
</FormControl>
|
|
226
|
+
<FormDescription>
|
|
227
|
+
Select the type of filter you would like to perform.
|
|
228
|
+
</FormDescription>
|
|
229
|
+
<FormMessage />
|
|
230
|
+
</FormItem>
|
|
231
|
+
)}
|
|
232
|
+
/>
|
|
233
|
+
<FormField
|
|
234
|
+
control={form.control}
|
|
235
|
+
name="model"
|
|
236
|
+
render={({ field }) => (
|
|
237
|
+
<FormItem>
|
|
238
|
+
<FormLabel>Model</FormLabel>
|
|
239
|
+
<FormControl>
|
|
240
|
+
<Select onValueChange={field.onChange}>
|
|
241
|
+
<SelectTrigger className="w-full">
|
|
242
|
+
<SelectValue />
|
|
243
|
+
</SelectTrigger>
|
|
244
|
+
<SelectContent>
|
|
245
|
+
{models[form.watch("filter_type")]?.map((model: string) => (
|
|
246
|
+
<SelectItem key={model} value={model}>
|
|
247
|
+
{model}
|
|
248
|
+
</SelectItem>
|
|
249
|
+
))}
|
|
250
|
+
</SelectContent>
|
|
251
|
+
</Select>
|
|
252
|
+
</FormControl>
|
|
253
|
+
<FormDescription>
|
|
254
|
+
Select the model you would like to use for filtering.
|
|
255
|
+
</FormDescription>
|
|
256
|
+
<FormMessage />
|
|
257
|
+
</FormItem>
|
|
258
|
+
)}
|
|
259
|
+
/>
|
|
260
|
+
{form.watch("filter_type") === "Species" && (
|
|
261
|
+
<>
|
|
262
|
+
{form.watch("model") != undefined && (
|
|
263
|
+
<FormField
|
|
264
|
+
control={form.control}
|
|
265
|
+
name="filter_species"
|
|
266
|
+
render={({ field }) => (
|
|
267
|
+
<FormItem className="flex flex-col">
|
|
268
|
+
<FormLabel>Filter Species</FormLabel>
|
|
269
|
+
<Popover>
|
|
270
|
+
<PopoverTrigger asChild>
|
|
271
|
+
<FormControl>
|
|
272
|
+
<Button
|
|
273
|
+
variant="outline"
|
|
274
|
+
role="combobox"
|
|
275
|
+
className={cn(
|
|
276
|
+
"w-full justify-between",
|
|
277
|
+
!field.value && "text-muted-foreground"
|
|
278
|
+
)}
|
|
279
|
+
>
|
|
280
|
+
{field.value
|
|
281
|
+
? modelMetadata?.display_names[field.value] as string
|
|
282
|
+
: "Select species..."}
|
|
283
|
+
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
284
|
+
</Button>
|
|
285
|
+
</FormControl>
|
|
286
|
+
</PopoverTrigger>
|
|
287
|
+
<PopoverContent className="p-0">
|
|
288
|
+
<Command>
|
|
289
|
+
<CommandInput placeholder="Search species..." />
|
|
290
|
+
<CommandList>
|
|
291
|
+
<CommandEmpty>No species found.</CommandEmpty>
|
|
292
|
+
<CommandGroup>
|
|
293
|
+
{modelMetadata?.display_names && Object.entries(modelMetadata.display_names).map(([value, label]) => (
|
|
294
|
+
<CommandItem
|
|
295
|
+
value={label as string}
|
|
296
|
+
key={value}
|
|
297
|
+
onSelect={() => {
|
|
298
|
+
form.setValue("filter_species", value)
|
|
299
|
+
}}
|
|
300
|
+
>
|
|
301
|
+
{label as string}
|
|
302
|
+
<Check
|
|
303
|
+
className={cn(
|
|
304
|
+
"ml-auto",
|
|
305
|
+
value === field.value
|
|
306
|
+
? "opacity-100"
|
|
307
|
+
: "opacity-0"
|
|
308
|
+
)}
|
|
309
|
+
/>
|
|
310
|
+
</CommandItem>
|
|
311
|
+
))}
|
|
312
|
+
</CommandGroup>
|
|
313
|
+
</CommandList>
|
|
314
|
+
</Command>
|
|
315
|
+
</PopoverContent>
|
|
316
|
+
</Popover>
|
|
317
|
+
<FormDescription>
|
|
318
|
+
Select the species you would like to filter for.
|
|
319
|
+
</FormDescription>
|
|
320
|
+
<FormMessage />
|
|
321
|
+
</FormItem>
|
|
322
|
+
)}
|
|
323
|
+
/>
|
|
324
|
+
)}
|
|
325
|
+
|
|
326
|
+
<FormField
|
|
327
|
+
control={form.control}
|
|
328
|
+
name="filter_mode"
|
|
329
|
+
render={({ field }) => (
|
|
330
|
+
<FormItem>
|
|
331
|
+
<FormLabel>Filter Mode</FormLabel>
|
|
332
|
+
<FormControl>
|
|
333
|
+
<Tabs defaultValue="threshold" onValueChange={field.onChange}>
|
|
334
|
+
<TabsList className="w-full">
|
|
335
|
+
<TabsTrigger value="threshold">Filter by threshold</TabsTrigger>
|
|
336
|
+
<TabsTrigger value="maximum">Filter by maximum scoring species</TabsTrigger>
|
|
337
|
+
</TabsList>
|
|
338
|
+
</Tabs>
|
|
339
|
+
</FormControl>
|
|
340
|
+
<FormDescription>
|
|
341
|
+
Select the filtering mode. You can either filter by threshold or by maximum scoring species, meaning that contigs will only be included if the selected species is the highest-scoring one.
|
|
342
|
+
</FormDescription>
|
|
343
|
+
<FormMessage />
|
|
344
|
+
</FormItem>
|
|
345
|
+
)}
|
|
346
|
+
/>
|
|
347
|
+
</>
|
|
348
|
+
)}
|
|
349
|
+
{form.watch("filter_mode") === "threshold" && (
|
|
350
|
+
<FormField
|
|
351
|
+
control={form.control}
|
|
352
|
+
name="threshold"
|
|
353
|
+
render={({ field }) => (
|
|
354
|
+
<FormItem>
|
|
355
|
+
<FormLabel>Threshold</FormLabel>
|
|
356
|
+
<FormControl>
|
|
357
|
+
<Input
|
|
358
|
+
type="number"
|
|
359
|
+
value={field.value}
|
|
360
|
+
defaultValue={0.7}
|
|
361
|
+
step={0.01}
|
|
362
|
+
min={0}
|
|
363
|
+
max={1}
|
|
364
|
+
onChange={(e) => field.onChange(Number(e.target.value))}
|
|
365
|
+
/>
|
|
366
|
+
</FormControl>
|
|
367
|
+
<FormDescription>
|
|
368
|
+
Set the threshold for filtering.
|
|
369
|
+
</FormDescription>
|
|
370
|
+
<FormMessage />
|
|
371
|
+
</FormItem>
|
|
372
|
+
)}
|
|
373
|
+
/>
|
|
374
|
+
)}
|
|
375
|
+
<FormField
|
|
376
|
+
control={form.control}
|
|
377
|
+
name="sparse_sampling"
|
|
378
|
+
render={({ field }) => (
|
|
379
|
+
<FormItem className="flex flex-row items-center justify-between pr-2">
|
|
380
|
+
<div className="space-y-0.5">
|
|
381
|
+
<FormLabel>Sparse Sampling</FormLabel>
|
|
382
|
+
<FormDescription>
|
|
383
|
+
Enable sparse sampling for filtering.
|
|
384
|
+
</FormDescription>
|
|
385
|
+
</div>
|
|
386
|
+
<FormControl>
|
|
387
|
+
<Switch
|
|
388
|
+
checked={field.value}
|
|
389
|
+
onCheckedChange={field.onChange}
|
|
390
|
+
/>
|
|
391
|
+
</FormControl>
|
|
392
|
+
</FormItem>
|
|
393
|
+
)}
|
|
394
|
+
/>
|
|
395
|
+
{
|
|
396
|
+
form.watch("sparse_sampling") && (
|
|
397
|
+
<FormField
|
|
398
|
+
control={form.control}
|
|
399
|
+
name="sparse_sampling_step"
|
|
400
|
+
render={({ field }) => (
|
|
401
|
+
<FormItem>
|
|
402
|
+
<FormLabel>Sparse Sampling Step</FormLabel>
|
|
403
|
+
<FormControl>
|
|
404
|
+
<Input
|
|
405
|
+
type="number"
|
|
406
|
+
value={field.value}
|
|
407
|
+
onChange={(e) => field.onChange(Number(e.target.value))}
|
|
408
|
+
/>
|
|
409
|
+
</FormControl>
|
|
410
|
+
<FormDescription>
|
|
411
|
+
Set the step size for sparse sampling.
|
|
412
|
+
</FormDescription>
|
|
413
|
+
<FormMessage />
|
|
414
|
+
</FormItem>
|
|
415
|
+
)}
|
|
416
|
+
/>
|
|
417
|
+
)
|
|
418
|
+
}
|
|
419
|
+
<Button type="submit">Filter</Button>
|
|
420
|
+
</form >
|
|
421
|
+
</Form >
|
|
422
|
+
)
|
|
423
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import FilterForm from "./filter-form";
|
|
2
|
+
import { Separator } from "./ui/separator";
|
|
3
|
+
|
|
4
|
+
export default function Filter() {
|
|
5
|
+
return (
|
|
6
|
+
<main className="flex-1 flex flex-col items-center justify-center p-4">
|
|
7
|
+
<div className="w-1/2">
|
|
8
|
+
<h1 className="text-xl font-bold">Filter your data</h1>
|
|
9
|
+
<p>Upload your data and select parameters to filter it.</p>
|
|
10
|
+
<Separator className="my-4" />
|
|
11
|
+
<FilterForm />
|
|
12
|
+
</div>
|
|
13
|
+
</main>
|
|
14
|
+
)
|
|
15
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {
|
|
2
|
+
NavigationMenu,
|
|
3
|
+
NavigationMenuItem,
|
|
4
|
+
NavigationMenuList,
|
|
5
|
+
} from "@/components/ui/navigation-menu"
|
|
6
|
+
import { Link, NavLink } from "react-router-dom"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
export default function Header() {
|
|
10
|
+
|
|
11
|
+
const navItems = [
|
|
12
|
+
{ name: "Classify", href: "classify", external: false },
|
|
13
|
+
{ name: "Filter", href: "filter", external: false },
|
|
14
|
+
{ name: "Models", href: "models", external: false },
|
|
15
|
+
{ name: "Documentation", href: "https://bionf.github.io/XspecT2/index.html", external: true },
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<header className="bg-white shadow-sm">
|
|
20
|
+
<div className="container mx-auto px-4 py-4">
|
|
21
|
+
<div className="flex items-center justify-between">
|
|
22
|
+
<div className="flex items-center text-xl font-bold">
|
|
23
|
+
<Link to="/">
|
|
24
|
+
XspecT
|
|
25
|
+
</Link>
|
|
26
|
+
</div>
|
|
27
|
+
<NavigationMenu>
|
|
28
|
+
<NavigationMenuList>
|
|
29
|
+
{navItems.map((item) => (
|
|
30
|
+
<NavigationMenuItem key={item.name} className="text-gray-700 hover:text-gray-900 font-medium">
|
|
31
|
+
<NavLink
|
|
32
|
+
className="block px-4 py-2 rounded-md transition-colors duration-200"
|
|
33
|
+
to={item.href}
|
|
34
|
+
{...item.external ? { onClick: (e) => { e.preventDefault(); window.open(item.href, "_blank"); } } : {}}
|
|
35
|
+
>
|
|
36
|
+
{item.name}
|
|
37
|
+
</NavLink>
|
|
38
|
+
</NavigationMenuItem>
|
|
39
|
+
))}
|
|
40
|
+
</NavigationMenuList>
|
|
41
|
+
</NavigationMenu>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
</header>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { ModelMetadata } from "@/types";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { getModelMetadata } from "../api";
|
|
4
|
+
import { Link, useParams } from "react-router-dom";
|
|
5
|
+
import { Separator } from "@/components/ui/separator";
|
|
6
|
+
import { DataTable } from "@/components/data-table"
|
|
7
|
+
|
|
8
|
+
export default function ModelDetails() {
|
|
9
|
+
const { model_slug } = useParams();
|
|
10
|
+
const [modelMetadata, setModelMetadata] = useState<ModelMetadata | null>(null);
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (model_slug) {
|
|
14
|
+
getModelMetadata(model_slug)
|
|
15
|
+
.then((data: ModelMetadata) => {
|
|
16
|
+
setModelMetadata(data);
|
|
17
|
+
})
|
|
18
|
+
.catch((err) => console.error('Fetch error:', err));
|
|
19
|
+
}
|
|
20
|
+
}, [model_slug]);
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<main className="flex-1 flex flex-col items-center justify-center p-4">
|
|
24
|
+
<div className="w-1/2">
|
|
25
|
+
{modelMetadata && (
|
|
26
|
+
<>
|
|
27
|
+
<h1 className="text-xl font-bold">
|
|
28
|
+
{modelMetadata.model_display_name} {modelMetadata.model_type} Model
|
|
29
|
+
</h1>
|
|
30
|
+
|
|
31
|
+
<div className="flex h-5 items-center space-x-4">
|
|
32
|
+
<div>k = {modelMetadata.k}</div>
|
|
33
|
+
<Separator orientation="vertical" />
|
|
34
|
+
<div>num_hashes = {modelMetadata.num_hashes}</div>
|
|
35
|
+
<Separator orientation="vertical" />
|
|
36
|
+
<div>fpr = {modelMetadata.fpr}</div>
|
|
37
|
+
|
|
38
|
+
{modelMetadata.model_type === "Species" && (
|
|
39
|
+
<>
|
|
40
|
+
<Separator orientation="vertical" />
|
|
41
|
+
<div>kernel = {modelMetadata.kernel}</div>
|
|
42
|
+
<Separator orientation="vertical" />
|
|
43
|
+
<div>C = {modelMetadata.C}</div>
|
|
44
|
+
</>
|
|
45
|
+
)}
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<Separator className="my-4" />
|
|
49
|
+
|
|
50
|
+
<h2 className="text-l font-bold">Author</h2>
|
|
51
|
+
<div className="flex flex-col gap-2">
|
|
52
|
+
<p>Model author: {modelMetadata.author == null ? "N/A" : modelMetadata.author}</p>
|
|
53
|
+
<p>
|
|
54
|
+
Model author email: {modelMetadata.author_email == null
|
|
55
|
+
? "N/A"
|
|
56
|
+
: <Link to={`mailto:${modelMetadata.author_email}`} className="font-medium underline underline-offset-4">
|
|
57
|
+
{modelMetadata.author_email}
|
|
58
|
+
</Link>
|
|
59
|
+
}
|
|
60
|
+
</p>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<Separator className="my-4" />
|
|
64
|
+
|
|
65
|
+
<h2 className="text-l font-bold mb-2">Model display names</h2>
|
|
66
|
+
<DataTable
|
|
67
|
+
columns={[
|
|
68
|
+
{ accessorKey: "key", header: "ID" },
|
|
69
|
+
{ accessorKey: "value", header: "Display name" }
|
|
70
|
+
]}
|
|
71
|
+
data={Object.entries(modelMetadata.display_names).map(([key, value]) => ({ key, value }))}
|
|
72
|
+
/>
|
|
73
|
+
|
|
74
|
+
<Separator className="my-4" />
|
|
75
|
+
|
|
76
|
+
<h2 className="text-l font-bold mb-2">K-mer Index Training Data</h2>
|
|
77
|
+
{typeof modelMetadata.training_accessions === "object" && Array.isArray(modelMetadata.training_accessions) && (
|
|
78
|
+
<p>
|
|
79
|
+
The model was trained on the following accessions:
|
|
80
|
+
<ul className="list-disc list-inside">
|
|
81
|
+
{modelMetadata.training_accessions.map((accession, i) => (
|
|
82
|
+
<li key={i}>
|
|
83
|
+
<Link
|
|
84
|
+
to={`https://www.ncbi.nlm.nih.gov/datasets/genome/${accession}/`}
|
|
85
|
+
target="_blank"
|
|
86
|
+
rel="noopener noreferrer"
|
|
87
|
+
className="font-medium underline underline-offset-4"
|
|
88
|
+
>
|
|
89
|
+
{accession}
|
|
90
|
+
</Link>
|
|
91
|
+
</li>
|
|
92
|
+
))}
|
|
93
|
+
</ul>
|
|
94
|
+
</p>
|
|
95
|
+
)}
|
|
96
|
+
{typeof modelMetadata.training_accessions === "object" && !Array.isArray(modelMetadata.training_accessions) && (
|
|
97
|
+
<DataTable
|
|
98
|
+
columns={[
|
|
99
|
+
{ accessorKey: "key", header: "Name" },
|
|
100
|
+
{
|
|
101
|
+
accessorKey: "value", header: "Accessions", cell: ({ getValue }) => {
|
|
102
|
+
const value = getValue();
|
|
103
|
+
return Array.isArray(value)
|
|
104
|
+
? value.map((accession, i) => (
|
|
105
|
+
<>
|
|
106
|
+
{i > 0 && ", "}
|
|
107
|
+
<Link
|
|
108
|
+
to={`https://www.ncbi.nlm.nih.gov/datasets/genome/${accession}/`}
|
|
109
|
+
target="_blank"
|
|
110
|
+
rel="noopener noreferrer"
|
|
111
|
+
className="font-medium underline underline-offset-4"
|
|
112
|
+
>
|
|
113
|
+
{accession}
|
|
114
|
+
</Link>
|
|
115
|
+
</>
|
|
116
|
+
))
|
|
117
|
+
: value;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
]}
|
|
121
|
+
data={Object.entries(modelMetadata.training_accessions).map(([key, value]) => ({
|
|
122
|
+
key: modelMetadata.display_names[key],
|
|
123
|
+
value
|
|
124
|
+
}))}
|
|
125
|
+
/>
|
|
126
|
+
)}
|
|
127
|
+
</>
|
|
128
|
+
)}
|
|
129
|
+
{!modelMetadata && (
|
|
130
|
+
<>
|
|
131
|
+
<h1 className="text-xl font-bold">Error</h1>
|
|
132
|
+
<p>No model could be found with slug <span className="font-mono">{model_slug}</span>.</p>
|
|
133
|
+
</>
|
|
134
|
+
)}
|
|
135
|
+
</div>
|
|
136
|
+
</main>
|
|
137
|
+
)
|
|
138
|
+
}
|