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.

Files changed (78) hide show
  1. xspect/classify.py +32 -0
  2. xspect/file_io.py +3 -9
  3. xspect/filter_sequences.py +56 -0
  4. xspect/main.py +13 -18
  5. xspect/mlst_feature/mlst_helper.py +102 -13
  6. xspect/mlst_feature/pub_mlst_handler.py +32 -6
  7. xspect/models/probabilistic_filter_mlst_model.py +160 -32
  8. xspect/models/probabilistic_filter_model.py +1 -0
  9. xspect/ncbi.py +8 -6
  10. xspect/train.py +13 -5
  11. xspect/web.py +173 -0
  12. xspect/xspect-web/.gitignore +24 -0
  13. xspect/xspect-web/README.md +54 -0
  14. xspect/xspect-web/components.json +21 -0
  15. xspect/xspect-web/dist/assets/index-CMG4V7fZ.js +290 -0
  16. xspect/xspect-web/dist/assets/index-jIKg1HIy.css +1 -0
  17. xspect/xspect-web/dist/index.html +14 -0
  18. xspect/xspect-web/dist/vite.svg +1 -0
  19. xspect/xspect-web/eslint.config.js +28 -0
  20. xspect/xspect-web/index.html +13 -0
  21. xspect/xspect-web/package-lock.json +6865 -0
  22. xspect/xspect-web/package.json +58 -0
  23. xspect/xspect-web/pnpm-lock.yaml +4317 -0
  24. xspect/xspect-web/public/vite.svg +1 -0
  25. xspect/xspect-web/src/App.tsx +29 -0
  26. xspect/xspect-web/src/api.tsx +62 -0
  27. xspect/xspect-web/src/assets/react.svg +1 -0
  28. xspect/xspect-web/src/components/classification-form.tsx +284 -0
  29. xspect/xspect-web/src/components/classify.tsx +18 -0
  30. xspect/xspect-web/src/components/data-table.tsx +78 -0
  31. xspect/xspect-web/src/components/dropdown-checkboxes.tsx +63 -0
  32. xspect/xspect-web/src/components/dropdown-slider.tsx +42 -0
  33. xspect/xspect-web/src/components/filter-form.tsx +423 -0
  34. xspect/xspect-web/src/components/filter.tsx +15 -0
  35. xspect/xspect-web/src/components/header.tsx +46 -0
  36. xspect/xspect-web/src/components/landing.tsx +7 -0
  37. xspect/xspect-web/src/components/models-details.tsx +138 -0
  38. xspect/xspect-web/src/components/models.tsx +53 -0
  39. xspect/xspect-web/src/components/result-chart.tsx +44 -0
  40. xspect/xspect-web/src/components/result.tsx +155 -0
  41. xspect/xspect-web/src/components/spinner.tsx +30 -0
  42. xspect/xspect-web/src/components/ui/accordion.tsx +64 -0
  43. xspect/xspect-web/src/components/ui/button.tsx +59 -0
  44. xspect/xspect-web/src/components/ui/card.tsx +92 -0
  45. xspect/xspect-web/src/components/ui/chart.tsx +351 -0
  46. xspect/xspect-web/src/components/ui/command.tsx +175 -0
  47. xspect/xspect-web/src/components/ui/dialog.tsx +135 -0
  48. xspect/xspect-web/src/components/ui/dropdown-menu.tsx +255 -0
  49. xspect/xspect-web/src/components/ui/file-upload.tsx +1459 -0
  50. xspect/xspect-web/src/components/ui/form.tsx +165 -0
  51. xspect/xspect-web/src/components/ui/input.tsx +21 -0
  52. xspect/xspect-web/src/components/ui/label.tsx +24 -0
  53. xspect/xspect-web/src/components/ui/navigation-menu.tsx +168 -0
  54. xspect/xspect-web/src/components/ui/popover.tsx +46 -0
  55. xspect/xspect-web/src/components/ui/select.tsx +183 -0
  56. xspect/xspect-web/src/components/ui/separator.tsx +26 -0
  57. xspect/xspect-web/src/components/ui/slider.tsx +61 -0
  58. xspect/xspect-web/src/components/ui/switch.tsx +29 -0
  59. xspect/xspect-web/src/components/ui/table.tsx +113 -0
  60. xspect/xspect-web/src/components/ui/tabs.tsx +64 -0
  61. xspect/xspect-web/src/index.css +120 -0
  62. xspect/xspect-web/src/lib/utils.ts +6 -0
  63. xspect/xspect-web/src/main.tsx +10 -0
  64. xspect/xspect-web/src/types.tsx +34 -0
  65. xspect/xspect-web/src/utils.tsx +6 -0
  66. xspect/xspect-web/src/vite-env.d.ts +1 -0
  67. xspect/xspect-web/tsconfig.app.json +32 -0
  68. xspect/xspect-web/tsconfig.json +13 -0
  69. xspect/xspect-web/tsconfig.node.json +24 -0
  70. xspect/xspect-web/vite.config.ts +24 -0
  71. {xspect-0.4.1.dist-info → xspect-0.5.0.dist-info}/METADATA +6 -8
  72. xspect-0.5.0.dist-info/RECORD +85 -0
  73. {xspect-0.4.1.dist-info → xspect-0.5.0.dist-info}/WHEEL +1 -1
  74. xspect/fastapi.py +0 -102
  75. xspect-0.4.1.dist-info/RECORD +0 -24
  76. {xspect-0.4.1.dist-info → xspect-0.5.0.dist-info}/entry_points.txt +0 -0
  77. {xspect-0.4.1.dist-info → xspect-0.5.0.dist-info}/licenses/LICENSE +0 -0
  78. {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,7 @@
1
+ export default function Landing() {
2
+ return (
3
+ <main className="flex-1 flex items-center justify-center">
4
+ <h1 className="text-4xl md:text-6xl font-bold text-center">Welcome to XspecT</h1>
5
+ </main>
6
+ )
7
+ }
@@ -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
+ }