diff --git a/README.md b/README.md index b58e0af..b064162 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,123 @@ -# Getting Started with Create React App +# NeuroJSON.io -This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). +**Free Data Worth Sharing** -## Available Scripts +[![Website](https://img.shields.io/badge/website-NeuroJSON.io-blue)](https://neurojson.io) +[![FAIR](https://img.shields.io/badge/FAIR-Findable%2C%20Accessible%2C%20Interoperable%2C%20Reusable-purple)](#) -In the project directory, you can run: +--- -### `yarn start` +## πŸ“– Overview -Runs the app in the development mode.\ -Open [http://localhost:3000](http://localhost:3000) to view it in the browser. +[NeuroJSON.io](https://neurojson.io) is an **NIH-funded open data portal** for **neuroimaging datasets**, designed to make scientific data: -The page will reload if you make edits.\ -You will also see any lint errors in the console. +- **Findable**: Fully searchable metadata and datasets +- **Accessible**: Open, lightweight JSON format +- **Interoperable**: Compatible across platforms and programming languages +- **Reusable**: Rich metadata, visualizations, and long-term viability -### `yarn test` +NeuroJSON leverages **modern web technologies and scalable NoSQL databases** and the **JSON standard** to distribute large-scale, complex imaging data in a **human- and machine-readable** form. -Launches the test runner in the interactive watch mode.\ -See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. +--- -### `yarn build` +## πŸš€ Features -Builds the app for production to the `build` folder.\ -It correctly bundles React in production mode and optimizes the build for the best performance. +- **Search** β€” Browse across multiple databases, datasets, and modalities +- **Preview** β€” Interact with JSON metadata and visualize imaging data (2D/3D) in browser +- **Download** β€” Retrieve datasets in JSON format, ready for use in Python, MATLAB/Octave, C++, and more +- **Upload** - Contribute your own datasets to NeuroJSON.io ([Steps to contribute](#-for-data-contributors)) +- **REST API** β€” Automate your workflows with lightweight endpoints, designed for smooth integration into both local analyses and large-scale pipelines -The build is minified and the filenames include the hashes.\ -Your app is ready to be deployed! +--- -See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. +## 🏁 Getting Started -### `yarn eject` +1. Visit [https://neurojson.io](https://neurojson.io) +2. Use the **search page** to find datasets or subjects of interest +3. Click any dataset to **preview** or **download** data +4. For automation, use the **[REST API](#rest-api)** -**Note: this is a one-way operation. Once you `eject`, you can’t go back!** +--- -If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. +## πŸ‘©β€πŸ”¬ For Data Contributors -Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. +We welcome your datasets! -You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. +- NeuroJSON prefers **BIDS-compliant data** +- Convert datasets to JSON using [`NeuroJSON Client(neuroj)`](https://github.com/NeuroJSON/neuroj): + - Install Docker (skip this step if you already have it): [Get Docker](https://docs.docker.com/get-docker/) + - Download neuroj via docker: + ``` + docker pull openjdata/neuroj:v2025 + ``` + - Example (convert a single dataset to JSON via neuroj): + ``` + docker run openjdata/neuroj:v2025 neuroj -i /path/to/database/rootfolder -o /path/to/output/json/folder -db openneuro -ds ds000001 --convert + ``` + - See the full list of available [NeuroJSON Client commands](https://hub.docker.com/r/openjdata/neuroj) + - Watch our [`tutorial video - convert data`](https://neurojson.io/about) -## Learn More +### Steps to contribute -You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). +1. Download NeuroJSON Client (neuroj) +2. Convert your dataset to JSON +3. Validate metadata +4. [Open a ticket](https://github.com/NeuroJSON/registry) to upload your dataset -To learn React, check out the [React documentation](https://reactjs.org/). +Contributions ensure **long-term public availability and reusability**. + +--- + +## πŸ’» For Developers + +### REST API + +- Lightweight endpoints for download +- JSON responses designed for integration with cloud and local workflows + +Example (Load by URL with REST-API in Python): + +``` +pip install jdata bjdata numpy +``` + +``` +import jdata as jd +data = jd.loadurl('https://neurojson.io:7777/openneuro/ds000001') + +# List all externally linked files +links = jd.jsonpath(data, '$.._DataLink_') + +# Download & cache anatomical nii.gz data for sub-01/sub-02 +jd.jdlink(links, {'regex': 'anat/sub-0[12]_.*.nii'}) +``` + +--- + +## πŸ“Š Current Stats (as of latest release) + +| Metric | Value | +| --------- | ----------- | +| Databases | **22** | +| Datasets | **1,529** | +| Subjects | **58,026** | +| Links | **580,857** | +| Data Size | **38 TB** | + +--- + +## 🀝 Governance & Support + +- NIH-funded data dissemination service +- Maintained by the [COTI Lab, Northeastern University](http://fanglab.org/wiki/) +- Contact: **admin@neurojson.io** + +--- + +## πŸ›  Roadmap + +We’re continuing to grow NeuroJSON.io to better serve the community. Some of the upcoming directions include: + +- πŸ”œ **Expanding databases and datasets** β€” broadening coverage to include more sources and subjects +- πŸ”œ **Enhancing visualization** β€” improving 2D/3D previews for richer and more intuitive exploration of data +- πŸ”œ **Streamlining uploads** β€” introducing new features to make dataset contributions more automatic and user-friendly diff --git a/public/img/about_page/convert.png b/public/img/about_page/convert.png new file mode 100644 index 0000000..ac50a93 Binary files /dev/null and b/public/img/about_page/convert.png differ diff --git a/public/img/section3/atlas.png b/public/img/section3/atlas.png new file mode 100644 index 0000000..c5854d9 Binary files /dev/null and b/public/img/section3/atlas.png differ diff --git a/public/img/section3/fnirs.png b/public/img/section3/fnirs.png new file mode 100644 index 0000000..6f368a9 Binary files /dev/null and b/public/img/section3/fnirs.png differ diff --git a/public/img/section3/mesh.png b/public/img/section3/mesh.png new file mode 100644 index 0000000..f1cde29 Binary files /dev/null and b/public/img/section3/mesh.png differ diff --git a/public/img/section3/mri.png b/public/img/section3/mri.png new file mode 100644 index 0000000..354cc1e Binary files /dev/null and b/public/img/section3/mri.png differ diff --git a/src/components/DatasetDetailPage/FileTree/FileTree.tsx b/src/components/DatasetDetailPage/FileTree/FileTree.tsx new file mode 100644 index 0000000..e3243d1 --- /dev/null +++ b/src/components/DatasetDetailPage/FileTree/FileTree.tsx @@ -0,0 +1,67 @@ +import FileTreeRow from "./FileTreeRow"; +import type { TreeNode } from "./types"; +import FolderIcon from "@mui/icons-material/Folder"; +import { Box, Typography } from "@mui/material"; +import React from "react"; + +type Props = { + title: string; + tree: TreeNode[]; + filesCount: number; + totalBytes: number; + onPreview: (url: string, index: number) => void; +}; + +const formatSize = (n: number) => { + if (n < 1024) return `${n} B`; + if (n < 1024 ** 2) return `${(n / 1024).toFixed(1)} KB`; + if (n < 1024 ** 3) return `${(n / 1024 ** 2).toFixed(2)} MB`; + if (n < 1024 ** 4) return `${(n / 1024 ** 3).toFixed(2)} GB`; + return `${(n / 1024 ** 4).toFixed(2)} TB`; +}; + +const FileTree: React.FC = ({ + title, + tree, + filesCount, + totalBytes, + onPreview, +}) => ( + + + + {title} + + Files: {filesCount}   Size: {formatSize(totalBytes)} + + + + + {tree.map((n) => ( + + ))} + + +); + +export default FileTree; diff --git a/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx b/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx new file mode 100644 index 0000000..cd2f900 --- /dev/null +++ b/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx @@ -0,0 +1,130 @@ +import type { TreeNode } from "./types"; +import { formatLeafValue, isPreviewable } from "./utils"; +import DownloadIcon from "@mui/icons-material/Download"; +import ExpandLess from "@mui/icons-material/ExpandLess"; +import ExpandMore from "@mui/icons-material/ExpandMore"; +import FolderIcon from "@mui/icons-material/Folder"; +import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile"; +import VisibilityIcon from "@mui/icons-material/Visibility"; +import { Box, Button, Collapse, Typography } from "@mui/material"; +import React from "react"; + +type Props = { + node: TreeNode; + level: number; + onPreview: (url: string, index: number) => void; +}; + +const FileTreeRow: React.FC = ({ node, level, onPreview }) => { + const [open, setOpen] = React.useState(false); + + if (node.kind === "folder") { + return ( + <> + setOpen((o) => !o)} + > + + + + {node.name} + {open ? : } + + + + {node.children.map((child) => ( + + ))} + + + ); + } + + return ( + + + + + + + + {node.name} + + + {!node.link && node.value !== undefined && ( + + {node.name === "_ArrayZipData_" + ? "[compressed data]" + : formatLeafValue(node.value)} + + )} + + + {node.link?.url && ( + + + {isPreviewable(node.link.url) && ( + + )} + + )} + + ); +}; + +export default FileTreeRow; diff --git a/src/components/DatasetDetailPage/FileTree/types.ts b/src/components/DatasetDetailPage/FileTree/types.ts new file mode 100644 index 0000000..211d9c5 --- /dev/null +++ b/src/components/DatasetDetailPage/FileTree/types.ts @@ -0,0 +1,6 @@ +export type LinkMeta = { url: string; index: number }; + +// this value can be one of these types +export type TreeNode = + | { kind: "folder"; name: string; path: string; children: TreeNode[] } + | { kind: "file"; name: string; path: string; value?: any; link?: LinkMeta }; diff --git a/src/components/DatasetDetailPage/FileTree/utils.ts b/src/components/DatasetDetailPage/FileTree/utils.ts new file mode 100644 index 0000000..b278647 --- /dev/null +++ b/src/components/DatasetDetailPage/FileTree/utils.ts @@ -0,0 +1,76 @@ +import type { LinkMeta, TreeNode } from "./types"; + +export const isPreviewable = (url: string) => + /\.(nii(\.gz)?|bnii|jdt|jdb|jmsh|bmsh)$/i.test( + (url.match(/file=([^&]+)/)?.[1] ?? url).toLowerCase() + ); + +export const formatLeafValue = (v: any): string => { + if (v === null) return "null"; + const t = typeof v; + if (t === "number" || t === "boolean") return String(v); + if (t === "string") return v.length > 120 ? v.slice(0, 120) + "…" : v; + if (Array.isArray(v)) { + const n = v.length; + const head = v + .slice(0, 5) + .map((x) => (typeof x === "number" ? x : JSON.stringify(x))); + return n <= 5 + ? `[${head.join(", ")}]` + : `[${head.join(", ")}, …] (${n} items)`; + } + return ""; // if it is object, return as a folder +}; + +// ignore meta keys +export const shouldSkipKey = (key: string) => + key === "_id" || key === "_rev" || key.startsWith("."); + +// build path -> {url, index} lookup, built from extractDataLinks function +// if external link objects have {path, url, index}, build a Map for the tree +export const makeLinkMap = < + T extends { path: string; url: string; index: number } +>( + links: T[] +): Map => { + const m = new Map(); + links.forEach((l) => m.set(l.path, { url: l.url, index: l.index })); + return m; +}; + +// Recursively convert the dataset JSON to a file-tree +export const buildTreeFromDoc = ( + doc: any, + linkMap: Map, + curPath = "" +): TreeNode[] => { + if (!doc || typeof doc !== "object") return []; + const out: TreeNode[] = []; + + Object.keys(doc).forEach((key) => { + if (shouldSkipKey(key)) return; + + const val = doc[key]; + const path = `${curPath}/${key}`; + const link = linkMap.get(path); + + if (link) { + out.push({ kind: "file", name: key, path, link }); + return; + } + + if (val && typeof val === "object" && !Array.isArray(val)) { + out.push({ + kind: "folder", + name: key, + path, + children: buildTreeFromDoc(val, linkMap, path), + }); + return; + } + + out.push({ kind: "file", name: key, path, value: val }); + }); + + return out; +}; diff --git a/src/components/HomePageComponents/Section3.tsx b/src/components/HomePageComponents/Section3.tsx index 5f7fa06..7f06e95 100644 --- a/src/components/HomePageComponents/Section3.tsx +++ b/src/components/HomePageComponents/Section3.tsx @@ -6,15 +6,82 @@ import { Colors } from "design/theme"; import React from "react"; import { useState } from "react"; +type Tile = { + src: string; + alt: string; + video: string; +}; + +const tiles: Tile[] = [ + { + src: `${process.env.PUBLIC_URL}/img/section3/mesh.png`, + // src: "/img/section3/mesh.png", + alt: "Brain mesh", + video: "https://neurojson.io/io/download/static/videos/preview_mesh.mp4", + }, + { + src: `${process.env.PUBLIC_URL}/img/section3/fnirs.png`, + // src: "/img/section3/fnirs.png", + alt: "fNIRS signals", + video: "https://neurojson.io/io/download/static/videos/preview_fnirs.mp4", + }, + { + src: `${process.env.PUBLIC_URL}/img/section3/atlas.png`, + // src: "/img/section3/atlas.png", + alt: "atlas", + video: "https://neurojson.io/io/download/static/videos/preview_atlas.mp4", + }, + { + src: `${process.env.PUBLIC_URL}/img/section3/mri.png`, + // src: "/img/section3/mri.png", + alt: "mri", + video: "https://neurojson.io/io/download/static/videos/preview_mri.mp4", + }, +]; + +// Vertical rectangle image tile (clickable) +const PreviewTile: React.FC<{ tile: Tile; onClick: () => void }> = ({ + tile, + onClick, +}) => ( + +); + interface Section3Props { scrollToNext: () => void; } const Section3: React.FC = ({ scrollToNext }) => { const [open, setOpen] = useState(false); + const [videoSrc, setVideoSrc] = useState(""); + + const handleOpen = (video: string) => { + setVideoSrc(video); + setOpen(true); + }; - const handleOpen = () => setOpen(true); - const handleClose = () => setOpen(false); + const handleClose = () => { + setOpen(false); + setVideoSrc(""); + }; return ( = ({ scrollToNext }) => { alignItems: "center", mt: { xs: 4, md: 2 }, mb: { xs: 8, md: 0 }, - cursor: "pointer", }} - onClick={handleOpen} > - rendering feature info cards + + + {tiles.map((t, i) => ( + handleOpen(t.video)} + /> + ))} + + {/* video dialog */} @@ -113,12 +187,12 @@ const Section3: React.FC = ({ scrollToNext }) => { > - - diff --git a/src/components/NavBar/NavItems.tsx b/src/components/NavBar/NavItems.tsx index 2a2f9be..58acd82 100644 --- a/src/components/NavBar/NavItems.tsx +++ b/src/components/NavBar/NavItems.tsx @@ -1,4 +1,4 @@ -import { Toolbar, Grid, Button, Typography, Box } from "@mui/material"; +import { Toolbar, Grid, Button, Typography, Box, Tooltip } from "@mui/material"; import { Colors } from "design/theme"; import React from "react"; import { useNavigate, Link } from "react-router-dom"; @@ -82,20 +82,71 @@ const NavItems: React.FC = () => { {[ - // { text: "ABOUT", url: "https://neurojson.org/Doc/Start" }, { text: "About", url: RoutesEnum.ABOUT }, { text: "Wiki", url: "https://neurojson.org/Wiki" }, { text: "Search", url: RoutesEnum.SEARCH }, { text: "Databases", url: RoutesEnum.DATABASES }, - { text: "V1", url: "https://neurojson.io/v1" }, - ].map(({ text, url }) => ( + { + text: "V1", + url: "https://neurojson.io/v1", + tooltip: "Visit the previous version of website", + }, + ].map(({ text, url, tooltip }) => ( - {url?.startsWith("https") ? ( + {tooltip ? ( + + + + {text} + + + + ) : url?.startsWith("https") ? ( { lineHeight={"1.5rem"} letterSpacing={"0.05rem"} sx={{ + fontSize: { + xs: "0.8rem", // font size on mobile + sm: "1rem", + }, color: Colors.white, transition: "color 0.3s ease, transform 0.3s ease", textTransform: "uppercase", @@ -128,6 +183,10 @@ const NavItems: React.FC = () => { lineHeight={"1.5rem"} letterSpacing={"0.05rem"} sx={{ + fontSize: { + xs: "0.8rem", // font size on mobile + sm: "1rem", + }, color: Colors.white, transition: "color 0.3s ease, transform 0.3s ease", textTransform: "uppercase", diff --git a/src/components/Routes.tsx b/src/components/Routes.tsx index b1918ee..8b16554 100644 --- a/src/components/Routes.tsx +++ b/src/components/Routes.tsx @@ -1,3 +1,4 @@ +import ScrollToTop from "./ScrollToTop"; import FullScreen from "design/Layouts/FullScreen"; import AboutPage from "pages/AboutPage"; import DatabasePage from "pages/DatabasePage"; @@ -5,40 +6,45 @@ import DatasetDetailPage from "pages/DatasetDetailPage"; import DatasetPage from "pages/DatasetPage"; import Home from "pages/Home"; import SearchPage from "pages/SearchPage"; +import UpdatedDatasetDetailPage from "pages/UpdatedDatasetDetailPage"; import NewDatasetPage from "pages/UpdatedDatasetPage"; import React from "react"; import { Navigate, Route, Routes as RouterRoutes } from "react-router-dom"; import RoutesEnum from "types/routes.enum"; const Routes = () => ( - - {/* FullScreen Layout */} - }> - {/* Home Page */} - } /> - {/* Databases Page */} - } /> + <> + + + {/* FullScreen Layout */} + }> + {/* Home Page */} + } /> + {/* Databases Page */} + } /> - {/* Dataset List Page */} - } - element={} - /> + {/* Dataset List Page */} + } + element={} + /> - {/* Dataset Details Page */} - } - /> + {/* Dataset Details Page */} + } + // element={} + /> - {/* Search Page */} - } /> + {/* Search Page */} + } /> - {/* About Page */} - } /> - - + {/* About Page */} + } /> + + + ); export default Routes; diff --git a/src/components/ScrollToTop.tsx b/src/components/ScrollToTop.tsx new file mode 100644 index 0000000..29df82e --- /dev/null +++ b/src/components/ScrollToTop.tsx @@ -0,0 +1,14 @@ +import { useEffect } from "react"; +import { useLocation } from "react-router-dom"; + +const ScrollToTop = () => { + const { pathname } = useLocation(); + + useEffect(() => { + window.scrollTo({ top: 0, left: 0, behavior: "instant" as ScrollBehavior }); + }, [pathname]); + + return null; +}; + +export default ScrollToTop; diff --git a/src/components/StatisticsBanner.tsx b/src/components/StatisticsBanner.tsx index 204ad70..99e148a 100644 --- a/src/components/StatisticsBanner.tsx +++ b/src/components/StatisticsBanner.tsx @@ -12,6 +12,36 @@ import { fetchDbStats } from "redux/neurojson/neurojson.action"; import { DbStatsItem } from "redux/neurojson/types/neurojson.interface"; import { RootState } from "redux/store"; +const iconStyle = { + marginRight: 1, + verticalAlign: "middle", + color: Colors.lightGray, + fontSize: { + xs: "2rem", + sm: "2.5rem", + }, +}; + +const numberTextStyle = { + color: Colors.lightGreen, + fontWeight: "medium", + textAlign: "center", + fontSize: { + xs: "1rem", + sm: "1.4rem", + }, +}; + +const labelTextStyle = { + color: Colors.lightGreen, + fontWeight: "medium", + textAlign: "center", + fontSize: { + xs: "0.6rem", + sm: "0.9rem", + }, +}; + // function for calculate links and size const calculateLinksAndSize = (dbStats: DbStatsItem[] | null) => { if (!dbStats) return { totalLinks: 0, totalSizeTB: "0.00" }; @@ -46,6 +76,34 @@ const StatisticsBanner: React.FC = () => { dispatch(fetchDbStats()); }, [dispatch]); + const StatItem = ({ + icon, + number, + label, + }: { + icon: React.ReactNode; + number: string; + label: string; + }) => ( + + {icon} + + {number} + {label} + + + ); + return ( { padding: "1rem", display: "flex", flexWrap: "wrap", - justifyContent: "center", + justifyContent: { + xs: "flex-start", + sm: "center", + }, gap: "2rem", }} > {/* Databases */} - - - - - {databaseCount.toLocaleString()} - - - Databases - - - - + } + number={databaseCount.toLocaleString()} + label="Databases" + /> {/* Datasets */} - - - - - {formatNumber(datasetStat?.num)} - - - Datasets - - - + } + number={formatNumber(datasetStat?.num)} + label="Datasets" + /> {/* Subjects */} - - - - - {formatNumber(subjectStat?.num)} - - - Subjects - - - + } + number={formatNumber(subjectStat?.num)} + label="Subjects" + /> {/* Links */} - - - - - {totalLinks.toLocaleString() ?? "-"} - - - Links - - - + } + number={formatNumber(totalLinks)} + label="Links" + /> {/* Size */} - - - - - {totalSizeTB ?? "-"} TB - - - Size - - - + } + number={`${totalSizeTB ?? "-"} TB`} + label="Size" + /> ); }; diff --git a/src/design/ReadMoreText.tsx b/src/design/ReadMoreText.tsx new file mode 100644 index 0000000..724434a --- /dev/null +++ b/src/design/ReadMoreText.tsx @@ -0,0 +1,44 @@ +import { Colors } from "./theme"; +import { Box, Typography, Button } from "@mui/material"; +import React, { useState } from "react"; + +const ReadMoreText: React.FC<{ text: string }> = ({ text }) => { + const [expanded, setExpanded] = useState(false); + + return ( + + + {text} + + + + + ); +}; + +export default ReadMoreText; diff --git a/src/design/theme.ts b/src/design/theme.ts index e786d3a..465682b 100644 --- a/src/design/theme.ts +++ b/src/design/theme.ts @@ -1,4 +1,5 @@ // import { orange, purple } from "@mui/material/colors"; +import { lightGreen } from "@mui/material/colors"; import { createTheme } from "@mui/material/styles"; const primary = { @@ -23,6 +24,7 @@ export const Colors = { error: "#D9534F", textPrimary: "#212121", textSecondary: "#494747", + lightGreen: "#16FDE2", green: "#02DEC4", darkGreen: "#49c6ae", yellow: "#FFDD31", diff --git a/src/pages/AboutPage.tsx b/src/pages/AboutPage.tsx index 7251d85..1e114bf 100644 --- a/src/pages/AboutPage.tsx +++ b/src/pages/AboutPage.tsx @@ -6,35 +6,25 @@ import { Button, Grid, Tooltip, + Divider, } from "@mui/material"; import { Colors } from "design/theme"; import React, { useRef } from "react"; - -const videoData = [ - { - src: "search.png", - alt: "search icon", - tip: "Search tutotial video", - video: "preview_video.mp4", - // ref: searchVideoRef, - }, - { src: "preview.png", alt: "preview icon", tip: "Preview tutotial video" }, - { src: "download.png", alt: "download icon", tip: "Download tutotial video" }, - { src: "api.png", alt: "api icon", tip: "Restful API tutotial video" }, -]; +import { Color } from "three"; const AboutPage: React.FC = () => { - const searchVideoRef = useRef(null); - const previewVideoRef = useRef(null); - const downloadVideoRef = useRef(null); - const apiVideoRef = useRef(null); + const searchVideoRef = useRef(null); + const previewVideoRef = useRef(null); + const downloadVideoRef = useRef(null); + const apiVideoRef = useRef(null); + const convertVideoRef = useRef(null); const videoData = [ { src: "search.png", alt: "search icon", tip: "Search tutotial video", - video: "preview_video.mp4", + video: "search_video.mp4", ref: searchVideoRef, }, { @@ -44,28 +34,66 @@ const AboutPage: React.FC = () => { video: "preview_video.mp4", ref: previewVideoRef, }, + { + src: "api.png", + alt: "api icon", + tip: "Rest API - Python tutotial video", + video: "python_api_video.mp4", + ref: apiVideoRef, + }, { src: "download.png", alt: "download icon", tip: "Download tutotial video", - video: "preview_video.mp4", + video: "download_video.mp4", ref: downloadVideoRef, }, { - src: "api.png", - alt: "api icon", - tip: "Restful API tutotial video", - video: "preview_video.mp4", - ref: searchVideoRef, + src: "convert.png", + alt: "convert icon", + tip: "Convert tutotial video", + video: "convert.mp4", + ref: convertVideoRef, }, ]; - return ( + const TutorialVideoItem = ({ + title, + videoUrl, + }: { + title: string; + videoUrl: string; + }) => ( + {/* + {title} + */} + + + ); + return ( + {/*section 1 */} { style={{ maxHeight: "500px", objectFit: "cover" }} > Your browser does not support the video tag. @@ -169,12 +197,40 @@ const AboutPage: React.FC = () => { justifyContent: "center", }} > - {videoData.map(({ src, alt, tip }) => ( - + {videoData.map(({ src, alt, tip, ref }) => ( + + // ref?.current?.scrollIntoView({ behavior: "smooth" }) + + // } + onClick={() => { + if (ref?.current) { + const offset = 80; // adjust this to match your fixed navbar height + const top = ref.current.offsetTop - offset; + window.scrollTo({ top, behavior: "smooth" }); + } + }} sx={{ width: { xs: "25%", @@ -201,7 +257,7 @@ const AboutPage: React.FC = () => { }} > { > Getting Started with NeuroJSON - - - + + + + + + + Preview + + + + + + + + + + + + + + + + + + + + + + + + Download + + + + + + + + + + + + + + + + + + + Convert And Upload + + + + + + + diff --git a/src/pages/DatabasePage.tsx b/src/pages/DatabasePage.tsx index 70923d8..859f2ba 100644 --- a/src/pages/DatabasePage.tsx +++ b/src/pages/DatabasePage.tsx @@ -1,4 +1,4 @@ -import { Box, Typography, Button, Container } from "@mui/material"; +import { Box, Typography, Button, Container, Avatar } from "@mui/material"; import { Colors } from "design/theme"; import { useAppDispatch } from "hooks/useAppDispatch"; import { useAppSelector } from "hooks/useAppSelector"; @@ -12,6 +12,7 @@ const DatabasePage: React.FC = () => { const navigate = useNavigate(); const dispatch = useAppDispatch(); const { registry } = useAppSelector(NeurojsonSelector); + console.log("registry", registry); useEffect(() => { dispatch(fetchRegistry()); @@ -41,7 +42,7 @@ const DatabasePage: React.FC = () => { } return ( - + Databases @@ -70,17 +71,23 @@ const DatabasePage: React.FC = () => { key={db.id} variant="outlined" sx={{ + position: "relative", // for overlay positioning padding: 3, textTransform: "none", fontWeight: 600, borderColor: Colors.lightGray, + // backgroundImage: db.logo ? `url(${db.logo})` : "none", + // backgroundSize: "cover", + // backgroundPosition: "center", color: Colors.lightGray, borderRadius: 2, transition: "all 0.3s ease", - height: "100px", + height: "150px", display: "flex", - flexDirection: "column", + flexDirection: "row", justifyContent: "center", + overflow: "hidden", // clip overlay inside + gap: 1, "&:hover": { borderColor: Colors.lightGray, backgroundColor: Colors.secondaryPurple, @@ -90,9 +97,61 @@ const DatabasePage: React.FC = () => { }} onClick={() => navigate(`${RoutesEnum.DATABASES}/${db.id}`)} > - - {db.name || "Unnamed Database"} - + {/* Logo as Avatar */} + {db.logo && ( + + )} + + {/* Overlay for fade/blur */} + {/* */} + + {/* Text goes above overlay */} + + + {db.fullname || "Unnamed Database"} + + {`(${db.name})`} + ); })} diff --git a/src/pages/DatasetDetailPage.tsx b/src/pages/DatasetDetailPage.tsx index 255541e..7b47878 100644 --- a/src/pages/DatasetDetailPage.tsx +++ b/src/pages/DatasetDetailPage.tsx @@ -17,6 +17,7 @@ import { } from "@mui/material"; import { TextField } from "@mui/material"; import LoadDatasetTabs from "components/DatasetDetailPage/LoadDatasetTabs"; +import ReadMoreText from "design/ReadMoreText"; import theme, { Colors } from "design/theme"; import { useAppDispatch } from "hooks/useAppDispatch"; import { useAppSelector } from "hooks/useAppSelector"; @@ -156,25 +157,12 @@ const DatasetDetailPage: React.FC = () => { const [jsonSize, setJsonSize] = useState(0); const [transformedDataset, setTransformedDataset] = useState(null); const [previewIndex, setPreviewIndex] = useState(0); + const aiSummary = datasetDocument?.[".datainfo"]?.AISummary ?? ""; // add spinner const [isPreviewLoading, setIsPreviewLoading] = useState(false); const [readyPreviewData, setReadyPreviewData] = useState(null); - // const onPreviewReady = (decodedData: any) => { - // console.log("βœ… Data is ready! Opening modal."); - // setReadyPreviewData(decodedData); // Store the final data - // setIsPreviewLoading(false); // Hide the spinner - // setPreviewOpen(true); // NOW open the modal - // }; - - // Dataset download button size calculation function - // const formatSize = (sizeInBytes: number): string => { - // if (sizeInBytes < 1024 * 1024) { - // return `${(sizeInBytes / 1024).toFixed(1)} KB`; - // } - // return `${(sizeInBytes / 1024 / 1024).toFixed(2)} MB`; - // }; const formatSize = (sizeInBytes: number): string => { if (sizeInBytes < 1024) { return `${sizeInBytes} Bytes`; @@ -302,16 +290,6 @@ const DatasetDetailPage: React.FC = () => { return internalLinks; }; - // const formatFileSize = (bytes: number): string => { - // if (bytes >= 1024 * 1024 * 1024) { - // return `${Math.floor(bytes / (1024 * 1024 * 1024))} GB`; - // } else if (bytes >= 1024 * 1024) { - // return `${Math.floor(bytes / (1024 * 1024))} MB`; - // } else { - // return `${Math.floor(bytes / 1024)} KB`; - // } - // }; - useEffect(() => { const fetchData = async () => { if (dbName && docId) { @@ -498,6 +476,10 @@ const DatasetDetailPage: React.FC = () => { "Is Internal:", isInternal ); + + // Clear any stale preview type from last run + delete (window as any).__previewType; + // fix spinner setIsPreviewLoading(true); // Show the spinner overlay setPreviewIndex(idx); @@ -515,10 +497,34 @@ const DatasetDetailPage: React.FC = () => { // }; const is2DPreviewCandidate = (obj: any): boolean => { - if (!obj || typeof obj !== "object") return false; - if (!obj._ArrayType_ || !obj._ArraySize_ || !obj._ArrayZipData_) + if (typeof window !== "undefined" && (window as any).__previewType) { + // console.log("preview type: 2d"); + return (window as any).__previewType === "2d"; + } + // if (window.__previewType) { + // console.log("work~~~~~~~"); + // return window.__previewType === "2d"; + // } + console.log("is 2d preview candidate !== 2d"); + console.log("obj", obj); + // if (typeof obj === "string" && obj.includes("db=optics-at-martinos")) { + // return false; + // } + // if (typeof obj === "string" && obj.endsWith(".jdb")) { + // return true; + // } + if (!obj || typeof obj !== "object") { + return false; + } + console.log("=======after first condition"); + if (!obj._ArrayType_ || !obj._ArraySize_ || !obj._ArrayZipData_) { + console.log("inside second condition"); return false; + } const dim = obj._ArraySize_; + console.log("array.isarray(dim)", Array.isArray(dim)); + console.log("dim.length", dim.length === 1 || dim.length === 2); + return ( Array.isArray(dim) && (dim.length === 1 || dim.length === 2) && @@ -534,6 +540,7 @@ const DatasetDetailPage: React.FC = () => { setPreviewOpen(true); } delete window.__onPreviewReady; + delete (window as any).__previewType; // for is2DPreviewCandidate }; // -----end @@ -622,8 +629,18 @@ const DatasetDetailPage: React.FC = () => { typeof dataOrUrl === "string" ? extractFileName(dataOrUrl) : ""; if (isPreviewableFile(fileName)) { (window as any).previewdataurl(dataOrUrl, idx); + const is2D = is2DPreviewCandidate(dataOrUrl); const panel = document.getElementById("chartpanel"); - if (panel) panel.style.display = "none"; // πŸ”’ Hide chart panel on 3D external + console.log("is2D", is2D); + console.log("panel", panel); + + if (is2D) { + console.log("πŸ“Š 2D data β†’ rendering inline with dopreview()"); + if (panel) panel.style.display = "block"; // πŸ”“ Show it! + setPreviewOpen(false); // β›” Don't open modal + } else { + if (panel) panel.style.display = "none"; // πŸ”’ Hide chart panel on 3D external + } //add spinner // setPreviewDataKey(dataOrUrl); // setPreviewOpen(true); @@ -937,6 +954,19 @@ const DatasetDetailPage: React.FC = () => { + {/* ai summary */} + {aiSummary && ( + + AI Summary + + )} + {aiSummary && } + { +// if (typeof obj !== "object" || obj === null) return obj; + +// const transformed: any = Array.isArray(obj) ? [] : {}; + +// for (const key in obj) { +// if (!Object.prototype.hasOwnProperty.call(obj, key)) continue; + +// const value = obj[key]; + +// // Match README, CHANGES, or file extensions +// const isLongTextKey = /^(README|CHANGES)$|\.md$|\.txt$|\.m$/i.test(key); + +// if (typeof value === "string" && isLongTextKey) { +// transformed[key] = `${value}`; +// } else if (typeof value === "object") { +// transformed[key] = transformJsonForDisplay(value); +// } else { +// transformed[key] = value; +// } +// } + +// return transformed; +// }; + +// const formatAuthorsWithDOI = ( +// authors: string[] | string, +// doi: string +// ): JSX.Element => { +// let authorText = ""; + +// if (Array.isArray(authors)) { +// if (authors.length === 1) { +// authorText = authors[0]; +// } else if (authors.length === 2) { +// authorText = authors.join(", "); +// } else { +// authorText = `${authors.slice(0, 2).join("; ")} et al.`; +// } +// } else { +// authorText = authors; +// } + +// let doiUrl = ""; +// if (doi) { +// if (/^[0-9]/.test(doi)) { +// doiUrl = `https://doi.org/${doi}`; +// } else if (/^doi\./.test(doi)) { +// doiUrl = `https://${doi}`; +// } else if (/^doi:/.test(doi)) { +// doiUrl = doi.replace(/^doi:/, "https://doi.org/"); +// } else { +// doiUrl = doi; +// } +// } + +// return ( +// <> +// {authorText} +// {doiUrl && ( +// +// (e.currentTarget.style.textDecoration = "underline") +// } +// onMouseLeave={(e) => (e.currentTarget.style.textDecoration = "none")} +// > +// {doiUrl} +// +// )} +// +// ); +// }; + +const UpdatedDatasetDetailPage: React.FC = () => { + const { dbName, docId } = useParams<{ dbName: string; docId: string }>(); + const navigate = useNavigate(); + const dispatch = useAppDispatch(); + const { + selectedDocument: datasetDocument, + loading, + error, + } = useAppSelector(NeurojsonSelector); + + const [externalLinks, setExternalLinks] = useState([]); + const [internalLinks, setInternalLinks] = useState([]); + // const [isExpanded, setIsExpanded] = useState(false); + const [isInternalExpanded, setIsInternalExpanded] = useState(true); + // const [searchTerm, setSearchTerm] = useState(""); + // const [matches, setMatches] = useState([]); + // const [highlightedIndex, setHighlightedIndex] = useState(-1); + const [downloadScript, setDownloadScript] = useState(""); + const [downloadScriptSize, setDownloadScriptSize] = useState(0); + const [totalFileSize, setTotalFileSize] = useState(0); + + const [previewIsInternal, setPreviewIsInternal] = useState(false); + const [isExternalExpanded, setIsExternalExpanded] = useState(true); + // const [expandedPaths, setExpandedPaths] = useState([]); + // const [originalTextMap, setOriginalTextMap] = useState< + // Map + // >(new Map()); + // const [jsonViewerKey, setJsonViewerKey] = useState(0); + const [jsonSize, setJsonSize] = useState(0); + // const [transformedDataset, setTransformedDataset] = useState(null); + const [previewIndex, setPreviewIndex] = useState(0); + const aiSummary = datasetDocument?.[".datainfo"]?.AISummary ?? ""; + + // 1) detect subjects at the top level, return true or false + const hasTopLevelSubjects = useMemo( + () => Object.keys(datasetDocument || {}).some((k) => /^sub-/i.test(k)), + [datasetDocument] + ); + + // 2) keep current subjects-only split, return subject objects list + const subjectsOnly = useMemo(() => { + const out: any = {}; + if (!datasetDocument) return out; + Object.keys(datasetDocument).forEach((k) => { + if (/^sub-/i.test(k)) out[k] = (datasetDocument as any)[k]; + }); + return out; + }, [datasetDocument]); + + // 3) link maps + const subjectLinks = useMemo( + () => externalLinks.filter((l) => /^\/sub-/i.test(l.path)), + [externalLinks] + ); + const subjectLinkMap = useMemo( + () => makeLinkMap(subjectLinks), + [subjectLinks] + ); + + // 4) build a folder/file tree with a fallback to the WHOLE doc when no subjects exist + const treeData = useMemo( + () => + hasTopLevelSubjects + ? buildTreeFromDoc(subjectsOnly, subjectLinkMap) + : buildTreeFromDoc(datasetDocument || {}, makeLinkMap(externalLinks)), + [ + hasTopLevelSubjects, + subjectsOnly, + subjectLinkMap, + datasetDocument, + externalLinks, + ] + ); + + // β€œrest” JSON only when we actually have subjects + const rest = useMemo(() => { + if (!datasetDocument || !hasTopLevelSubjects) return {}; + const r: any = {}; + Object.keys(datasetDocument).forEach((k) => { + if (!/^sub-/i.test(k)) r[k] = (datasetDocument as any)[k]; + }); + return r; + }, [datasetDocument, hasTopLevelSubjects]); + + // JSON panel should always render: + // - if we have subjects -> show "rest" (everything except sub-*) + // - if we don't have subjects -> show the whole document + const jsonPanelData = useMemo( + () => (hasTopLevelSubjects ? rest : datasetDocument || {}), + [hasTopLevelSubjects, rest, datasetDocument] + ); + + // 5) header title + counts also fall back + const treeTitle = hasTopLevelSubjects ? "Subjects" : "Files"; + + const { filesCount, totalBytes } = useMemo(() => { + const group = hasTopLevelSubjects ? subjectLinks : externalLinks; + let bytes = 0; + for (const l of group) { + const m = l.url.match(/size=(\d+)/); + if (m) bytes += parseInt(m[1], 10); + } + return { filesCount: group.length, totalBytes: bytes }; + }, [hasTopLevelSubjects, subjectLinks, externalLinks]); + + // add spinner + const [isPreviewLoading, setIsPreviewLoading] = useState(false); + const [readyPreviewData, setReadyPreviewData] = useState(null); + + const formatSize = (sizeInBytes: number): string => { + if (sizeInBytes < 1024) { + return `${sizeInBytes} Bytes`; + } else if (sizeInBytes < 1024 * 1024) { + return `${(sizeInBytes / 1024).toFixed(1)} KB`; + } else if (sizeInBytes < 1024 * 1024 * 1024) { + return `${(sizeInBytes / (1024 * 1024)).toFixed(2)} MB`; + } else if (sizeInBytes < 1024 * 1024 * 1024 * 1024) { + return `${(sizeInBytes / (1024 * 1024 * 1024)).toFixed(2)} GB`; + } else { + return `${(sizeInBytes / (1024 * 1024 * 1024 * 1024)).toFixed(2)} TB`; + } + }; + + // Recursive function to find `_DataLink_` + const extractDataLinks = (obj: any, path: string): ExternalDataLink[] => { + const links: ExternalDataLink[] = []; + + const traverse = ( + node: any, + currentPath: string, + parentKey: string = "" + ) => { + if (typeof node === "object" && node !== null) { + for (const key in node) { + if (key === "_DataLink_" && typeof node[key] === "string") { + let correctedUrl = node[key].replace(/:\$.*$/, ""); + const sizeMatch = node[key].match(/size=(\d+)/); + const size = sizeMatch + ? `${(parseInt(sizeMatch[1], 10) / 1024 / 1024).toFixed(2)} MB` + : "Unknown Size"; + + const parts = currentPath.split("/"); + const subpath = parts.slice(-3).join("/"); + const label = parentKey || "ExternalData"; + + links.push({ + name: `${label} (${size}) [/${subpath}]`, + size, + path: currentPath, // keep full JSON path for file placement + url: correctedUrl, + index: links.length, + }); + } else if (typeof node[key] === "object") { + const isMetaKey = key.startsWith("_"); + const newLabel = !isMetaKey ? key : parentKey; + traverse(node[key], `${currentPath}/${key}`, newLabel); + } + } + } + }; + + traverse(obj, path); + // return links; + const seenUrls = new Set(); + const uniqueLinks = links.filter((link) => { + if (seenUrls.has(link.url)) return false; + seenUrls.add(link.url); + return true; + }); + + return uniqueLinks; + }; + + const extractInternalData = (obj: any, path = ""): InternalDataLink[] => { + const internalLinks: InternalDataLink[] = []; + + if (obj && typeof obj === "object") { + if ( + obj.hasOwnProperty("MeshNode") && + (obj.hasOwnProperty("MeshSurf") || obj.hasOwnProperty("MeshElem")) + ) { + if ( + obj.MeshNode.hasOwnProperty("_ArrayZipData_") && + typeof obj.MeshNode["_ArrayZipData_"] === "string" + ) { + internalLinks.push({ + name: `JMesh`, + data: obj, + index: internalLinks.length, // maybe can be remove + arraySize: obj.MeshNode._ArraySize_, + }); + } + } else if (obj.hasOwnProperty("NIFTIData")) { + if ( + obj.NIFTIData.hasOwnProperty("_ArrayZipData_") && + typeof obj.NIFTIData["_ArrayZipData_"] === "string" + ) { + internalLinks.push({ + name: `JNIfTI`, + data: obj, + index: internalLinks.length, //maybe can be remove + arraySize: obj.NIFTIData._ArraySize_, + }); + } + } else if ( + obj.hasOwnProperty("_ArraySize_") && + !path.match("_EnumValue_$") + ) { + if ( + obj.hasOwnProperty("_ArrayZipData_") && + typeof obj["_ArrayZipData_"] === "string" + ) { + internalLinks.push({ + name: `JData`, + data: obj, + index: internalLinks.length, // maybe can be remove + arraySize: obj._ArraySize_, + }); + } + } else { + Object.keys(obj).forEach((key) => { + if (typeof obj[key] === "object") { + internalLinks.push( + ...extractInternalData( + obj[key], + `${path}.${key.replace(/\./g, "\\.")}` + ) + ); + } + }); + } + } + + return internalLinks; + }; + + useEffect(() => { + const fetchData = async () => { + if (dbName && docId) { + await dispatch(fetchDocumentDetails({ dbName, docId })); + } + }; + + fetchData(); + }, [dbName, docId, dispatch]); + + useEffect(() => { + if (datasetDocument) { + // Extract External Data & Assign `index` + console.log("datasetDocument", datasetDocument); + const links = extractDataLinks(datasetDocument, "").map( + (link, index) => ({ + ...link, + index, // Assign index correctly + }) + ); + + // Extract Internal Data & Assign `index` + const internalData = extractInternalData(datasetDocument).map( + (data, index) => ({ + ...data, + index, // Assign index correctly + }) + ); + + console.log(" Extracted external links:", links); + console.log(" Extracted internal data:", internalData); + + setExternalLinks(links); + setInternalLinks(internalData); + // const transformed = transformJsonForDisplay(datasetDocument); + // setTransformedDataset(transformed); + + // Calculate total file size from size= query param + let total = 0; + links.forEach((link) => { + const sizeMatch = link.url.match(/(?:[?&]size=)(\d+)/); + if (sizeMatch && sizeMatch[1]) { + total += parseInt(sizeMatch[1], 10); + } + }); + setTotalFileSize(total); + + let totalSize = 0; + + // 1️⃣ Sum external link sizes (from URL like ...?size=12345678) + links.forEach((link) => { + const sizeMatch = link.url.match(/size=(\d+)/); + if (sizeMatch) { + totalSize += parseInt(sizeMatch[1], 10); + } + }); + + // 2️⃣ Estimate internal size from _ArraySize_ (assume Float32 = 4 bytes) + internalData.forEach((link) => { + if (link.arraySize && Array.isArray(link.arraySize)) { + const count = link.arraySize.reduce((acc, val) => acc * val, 1); + totalSize += count * 4; + } + }); + + // setTotalFileSize(totalSize); + + // const minifiedBlob = new Blob([JSON.stringify(datasetDocument)], { + // type: "application/json", + // }); + // setJsonSize(minifiedBlob.size); + + const blob = new Blob([JSON.stringify(datasetDocument, null, 2)], { + type: "application/json", + }); + setJsonSize(blob.size); + + // // βœ… Construct download script dynamically + let script = `curl -L --create-dirs "https://neurojson.io:7777/${dbName}/${docId}" -o "${docId}.json"\n`; + + links.forEach((link) => { + const url = link.url; + // console.log("url", url); + const match = url.match(/file=([^&]+)/); + // console.log("match", match); + // console.log("match[1]", match?.[1]); + // try { + // const decoded = match?.[1] ? decodeURIComponent(match[1]) : "N/A"; + // console.log("decode", decoded); + // } catch (err) { + // console.warn("⚠️ Failed to decode match[1]:", match?.[1], err); + // } + + // const filename = match + // ? decodeURIComponent(match[1]) + // : `file-${link.index}`; + + const filename = match + ? (() => { + try { + return decodeURIComponent(match[1]); + } catch { + return match[1]; // fallback if decode fails + } + })() + : `file-${link.index}`; + // console.log("filename", filename); + + const outputPath = `$HOME/.neurojson/io/${dbName}/${docId}/${filename}`; + + script += `curl -L --create-dirs "${url}" -o "${outputPath}"\n`; + }); + setDownloadScript(script); + // βœ… Calculate and set script size + const scriptBlob = new Blob([script], { type: "text/plain" }); + setDownloadScriptSize(scriptBlob.size); + } + }, [datasetDocument, docId]); + + const [previewOpen, setPreviewOpen] = useState(false); + const [previewDataKey, setPreviewDataKey] = useState(null); + + // useEffect(() => { + // highlightMatches(searchTerm); + + // // Cleanup to reset highlights when component re-renders or unmounts + // return () => { + // document.querySelectorAll(".highlighted").forEach((el) => { + // const element = el as HTMLElement; + // const text = element.textContent || ""; + // element.innerHTML = text; + // element.classList.remove("highlighted"); + // }); + // }; + // }, [searchTerm, datasetDocument]); + + // useEffect(() => { + // if (!transformedDataset) return; + + // const spans = document.querySelectorAll(".string-value"); + + // spans.forEach((el) => { + // if (el.textContent?.includes('')) { + // // Inject as HTML so it renders code block correctly + // el.innerHTML = el.textContent ?? ""; + // } + // }); + // }, [transformedDataset]); + + const handleDownloadDataset = () => { + if (!datasetDocument) return; + const jsonData = JSON.stringify(datasetDocument); + const blob = new Blob([jsonData], { type: "application/json" }); + const link = document.createElement("a"); + link.href = URL.createObjectURL(blob); + link.download = `${docId}.json`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }; + + const handleDownloadScript = () => { + const blob = new Blob([downloadScript], { type: "text/plain" }); + const link = document.createElement("a"); + link.href = URL.createObjectURL(blob); + link.download = `${docId}.sh`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }; + + const handlePreview = ( + dataOrUrl: string | any, + idx: number, + isInternal: boolean = false + ) => { + console.log( + "🟒 Preview button clicked for:", + dataOrUrl, + "Index:", + idx, + "Is Internal:", + isInternal + ); + + // Clear any stale preview type from last run + delete (window as any).__previewType; + + // fix spinner + setIsPreviewLoading(true); // Show the spinner overlay + setPreviewIndex(idx); + setPreviewDataKey(dataOrUrl); + setPreviewIsInternal(isInternal); + + // setPreviewOpen(false); // IMPORTANT: Keep modal closed for now + + // This callback will be triggered by the legacy script when data is ready + // (window as any).__onPreviewReady = (decodedData: any) => { + // console.log("βœ… Data is ready! Opening modal."); + // setReadyPreviewData(decodedData); // Store the final data for the modal + // setIsPreviewLoading(false); // Hide the spinner + // setPreviewOpen(true); // NOW it's time to open the modal + // }; + + const is2DPreviewCandidate = (obj: any): boolean => { + if (typeof window !== "undefined" && (window as any).__previewType) { + // console.log("preview type: 2d"); + return (window as any).__previewType === "2d"; + } + // if (window.__previewType) { + // console.log("work~~~~~~~"); + // return window.__previewType === "2d"; + // } + console.log("is 2d preview candidate !== 2d"); + console.log("obj", obj); + // if (typeof obj === "string" && obj.includes("db=optics-at-martinos")) { + // return false; + // } + // if (typeof obj === "string" && obj.endsWith(".jdb")) { + // return true; + // } + if (!obj || typeof obj !== "object") { + return false; + } + console.log("=======after first condition"); + if (!obj._ArrayType_ || !obj._ArraySize_ || !obj._ArrayZipData_) { + console.log("inside second condition"); + return false; + } + const dim = obj._ArraySize_; + console.log("array.isarray(dim)", Array.isArray(dim)); + console.log("dim.length", dim.length === 1 || dim.length === 2); + + return ( + Array.isArray(dim) && + (dim.length === 1 || dim.length === 2) && + dim.every((v) => typeof v === "number" && v > 0) + ); + }; + // for add spinner ---- start + // When legacy preview is actually ready, turn off spinner & open modal + window.__onPreviewReady = () => { + setIsPreviewLoading(false); + // Only open modal for 3D data + if (!is2DPreviewCandidate(dataOrUrl)) { + setPreviewOpen(true); + } + delete window.__onPreviewReady; + delete (window as any).__previewType; // for is2DPreviewCandidate + }; + // -----end + + const extractFileName = (url: string): string => { + const match = url.match(/file=([^&]+)/); + // return match ? decodeURIComponent(match[1]) : url; + if (match) { + // Strip any trailing query parameters + const raw = decodeURIComponent(match[1]); + return raw.split("?")[0].split("&")[0]; + } + // fallback: try to get last path part if no 'file=' param + try { + const u = new URL(url); + const parts = u.pathname.split("/"); + return parts[parts.length - 1]; + } catch { + return url; + } + }; + + const fileName = + typeof dataOrUrl === "string" ? extractFileName(dataOrUrl) : ""; + console.log("πŸ” Extracted fileName:", fileName); + + const isPreviewableFile = (fileName: string): boolean => { + return /\.(nii\.gz|jdt|jdb|bmsh|jmsh|bnii)$/i.test(fileName); + }; + console.log("πŸ§ͺ isPreviewableFile:", isPreviewableFile(fileName)); + + // test for add spinner + // if (isInternal) { + // if (is2DPreviewCandidate(dataOrUrl)) { + // // inline 2D + // window.dopreview(dataOrUrl, idx, true); + // } else { + // // 3D + // window.previewdata(dataOrUrl, idx, true, []); + // } + // } else { + // // external + // window.previewdataurl(dataOrUrl, idx); + // } + + // for test so command out the below + // setPreviewIndex(idx); + // setPreviewDataKey(dataOrUrl); + // setPreviewIsInternal(isInternal); + // setPreviewOpen(true); + + if (isInternal) { + try { + if (!(window as any).intdata) { + (window as any).intdata = []; + } + if (!(window as any).intdata[idx]) { + (window as any).intdata[idx] = ["", "", null, `Internal ${idx}`]; + } + (window as any).intdata[idx][2] = JSON.parse(JSON.stringify(dataOrUrl)); + + const is2D = is2DPreviewCandidate(dataOrUrl); + + if (is2D) { + console.log("πŸ“Š 2D data β†’ rendering inline with dopreview()"); + (window as any).dopreview(dataOrUrl, idx, true); + const panel = document.getElementById("chartpanel"); + if (panel) panel.style.display = "block"; // πŸ”“ Show it! + setPreviewOpen(false); // β›” Don't open modal + // setPreviewLoading(false); // stop spinner + } else { + console.log("🎬 3D data β†’ rendering in modal"); + (window as any).previewdata(dataOrUrl, idx, true, []); + // add spinner + // setPreviewDataKey(dataOrUrl); + // setPreviewOpen(true); + // setPreviewIsInternal(true); + } + } catch (err) { + console.error("❌ Error in internal preview:", err); + // setPreviewLoading(false); // add spinner + } + } else { + // external + // if (/\.(nii\.gz|jdt|jdb|bmsh|jmsh|bnii)$/i.test(dataOrUrl)) { + const fileName = + typeof dataOrUrl === "string" ? extractFileName(dataOrUrl) : ""; + if (isPreviewableFile(fileName)) { + (window as any).previewdataurl(dataOrUrl, idx); + const is2D = is2DPreviewCandidate(dataOrUrl); + const panel = document.getElementById("chartpanel"); + console.log("is2D", is2D); + console.log("panel", panel); + + if (is2D) { + console.log("πŸ“Š 2D data β†’ rendering inline with dopreview()"); + if (panel) panel.style.display = "block"; // πŸ”“ Show it! + setPreviewOpen(false); // β›” Don't open modal + } else { + if (panel) panel.style.display = "none"; // πŸ”’ Hide chart panel on 3D external + } + //add spinner + // setPreviewDataKey(dataOrUrl); + // setPreviewOpen(true); + // setPreviewIsInternal(false); + } else { + console.warn("⚠️ Unsupported file format for preview:", dataOrUrl); + // setPreviewLoading(false); // add spinner + } + } + }; + + const handleClosePreview = () => { + console.log("πŸ›‘ Closing preview modal."); + setPreviewOpen(false); + setPreviewDataKey(null); + + // Cancel animation frame loop + if (typeof window.reqid !== "undefined") { + cancelAnimationFrame(window.reqid); + window.reqid = undefined; + } + + // Stop 2D chart if any + const panel = document.getElementById("chartpanel"); + if (panel) panel.style.display = "none"; + + // Remove canvas children + // const canvasDiv = document.getElementById("canvas"); + // if (canvasDiv) { + // while (canvasDiv.firstChild) { + // canvasDiv.removeChild(canvasDiv.firstChild); + // } + // } + + // Reset Three.js global refs + window.scene = undefined; + window.camera = undefined; + window.renderer = undefined; + }; + + // const handleSearch = (e: React.ChangeEvent) => { + // setSearchTerm(e.target.value); + // setHighlightedIndex(-1); + // highlightMatches(e.target.value); + // }; + + // const highlightMatches = (keyword: string) => { + // const spans = document.querySelectorAll( + // ".react-json-view span.string-value, .react-json-view span.object-key" + // ); + + // // Clean up all existing highlights + // spans.forEach((el) => { + // const element = el as HTMLElement; + // if (originalTextMap.has(element)) { + // element.innerHTML = originalTextMap.get(element)!; // Restore original HTML + // element.classList.remove("highlighted"); + // } + // }); + + // // Clear old state + // setMatches([]); + // setHighlightedIndex(-1); + // setExpandedPaths([]); + // setOriginalTextMap(new Map()); + + // if (!keyword.trim() || keyword.length < 3) return; + + // const regex = new RegExp(`(${keyword})`, "gi"); + // const matchedElements: HTMLElement[] = []; + // const matchedPaths: Set = new Set(); + // const newOriginalMap = new Map(); + + // spans.forEach((el) => { + // const element = el as HTMLElement; + // const original = element.innerHTML; + // const text = element.textContent || ""; + + // if (text.toLowerCase().includes(keyword.toLowerCase())) { + // newOriginalMap.set(element, original); // Store original HTML + // const highlighted = text.replace( + // regex, + // `$1` + // ); + // element.innerHTML = highlighted; + // matchedElements.push(element); + + // const parent = element.closest(".variable-row"); + // const path = parent?.getAttribute("data-path"); + // if (path) matchedPaths.add(path); + // } + // }); + + // // Update state + // setOriginalTextMap(newOriginalMap); + // setMatches(matchedElements); + // setExpandedPaths(Array.from(matchedPaths)); + // }; + + // const findNext = () => { + // if (matches.length === 0) return; + + // setHighlightedIndex((prevIndex) => { + // const nextIndex = (prevIndex + 1) % matches.length; + + // matches.forEach((match) => { + // match + // .querySelector("mark") + // ?.setAttribute("style", "background: yellow; color: black;"); + // }); + + // const current = matches[nextIndex]; + // current.scrollIntoView({ behavior: "smooth", block: "center" }); + + // current + // .querySelector("mark") + // ?.setAttribute("style", "background: orange; color: black;"); + + // return nextIndex; + // }); + // }; + + if (loading) { + return ( + + + + ); + } + + if (error) { + return ( + + + {error} + + + ); + } + console.log("datasetDocument", datasetDocument); + const onekey = datasetDocument + ? datasetDocument.hasOwnProperty("README") + ? "README" + : datasetDocument.hasOwnProperty("dataset_description.json") + ? "dataset_description.json" + : "_id" + : "_id"; + + return ( + <> + {/* πŸ”§ Inline CSS for string formatting */} + {/* */} + + + + + {/* βœ… Dataset Title (From dataset_description.json) */} + + {datasetDocument?.["dataset_description.json"]?.Name ?? + `Dataset: ${docId}`} + + + {/* βœ… Dataset Author (If Exists) */} + {datasetDocument?.["dataset_description.json"]?.Authors && ( + + {Array.isArray( + datasetDocument["dataset_description.json"].Authors + ) + ? datasetDocument["dataset_description.json"].Authors.join(", ") + : datasetDocument["dataset_description.json"].Authors} + + )} + + {/* βœ… Breadcrumb Navigation (🏠 Home β†’ Database β†’ Dataset) */} + + {/* 🏠 Home Icon Button */} + + + + Β» + + + {/* Database Name (Clickable) */} + + + + Β» + + + {/* Dataset Name (_id field) */} + + {docId} + + + + {/* ai summary */} + {aiSummary && } + + + + + + + {/* + + + */} + + + +
+ + {/* JSON Viewer (left panel) */} + + + {/* 1) SUBJECTS FILE BROWSER */} + {hasTopLevelSubjects && ( + + handlePreview(url, index, false)} + /> + + )} + + {/* 2) EVERYTHING ELSE AS JSON */} + + + + + + {/* = 3 ? false : 1} // πŸ” Expand during search + style={{ fontSize: "14px", fontFamily: "monospace" }} + /> */} + + + {/* Data panels (right panel) */} + + + {/* βœ… Collapsible header */} + setIsInternalExpanded(!isInternalExpanded)} + > + + Internal Data ({internalLinks.length} objects) + + {isInternalExpanded ? : } + + + + {/* βœ… Scrollable area */} + + {internalLinks.length > 0 ? ( + internalLinks.map((link, index) => ( + + + {link.name}{" "} + {link.arraySize + ? `[${link.arraySize.join("x")}]` + : ""} + + + + )) + ) : ( + + No internal data found. + + )} + + + + + {/* βœ… Header with toggle */} + setIsExternalExpanded(!isExternalExpanded)} + > + + External Data ({externalLinks.length} links) + + {isExternalExpanded ? : } + + + + {/* Scrollable card container */} + + {externalLinks.length > 0 ? ( + externalLinks.map((link, index) => { + const match = link.url.match(/file=([^&]+)/); + const fileName = match ? match[1] : ""; + const isPreviewable = + /\.(nii(\.gz)?|bnii|jdt|jdb|jmsh|bmsh)$/i.test( + fileName + ); + + return ( + + + {link.name} + + + + {isPreviewable && ( + + )} + + + ); + }) + ) : ( + + No external links found. + + )} + + + + + + + {/*
*/} + + + {/* + + {/* Global spinner while loading (before modal mounts) */} + + + + + {/* Preview Modal Component - Add Here */} + +
+ + ); +}; + +export default UpdatedDatasetDetailPage; diff --git a/src/utils/preview.js b/src/utils/preview.js index 4d496f1..3a44b5e 100644 --- a/src/utils/preview.js +++ b/src/utils/preview.js @@ -232,6 +232,7 @@ function drawpreview(cfg) { } // for spinner // --- Signal React that 3D preview is ready --- + window.__previewType = "3d"; if (typeof window.__onPreviewReady === "function") { window.__onPreviewReady(); } @@ -246,6 +247,7 @@ function previewdata(key, idx, isinternal, hastime) { isinternal, intdata: window.intdata, }); + console.log("key in previewdata", key); if (!hasthreejs) { $.when( $.getScript("https://mcx.space/cloud/js/OrbitControls.js"), @@ -255,9 +257,11 @@ function previewdata(key, idx, isinternal, hastime) { ).done(function () { hasthreejs = true; dopreview(key, idx, isinternal, hastime); + console.log("into the previewdata function if"); }); } else { dopreview(key, idx, isinternal, hastime); + console.log("into the previewdata function else"); } } @@ -281,8 +285,21 @@ function dopreview(key, idx, isinternal, hastime) { return; } } else { + // dataroot = key; + // console.log("into dopreview external data's dataroot", dataroot); + if (window.extdata && window.extdata[idx] && window.extdata[idx][2]) { - dataroot = window.extdata[idx][2]; + if (typeof key === "object") { + dataroot = key; + console.log("if key is object", typeof key); + } else { + dataroot = window.extdata[idx][2]; + console.log("type of key", typeof key); + } + + // dataroot = key; + + console.log("into dopreview external data's dataroot", dataroot); } else { console.error("❌ External data not ready for index", idx); return; @@ -302,7 +319,9 @@ function dopreview(key, idx, isinternal, hastime) { dataroot = window.extdata[idx][2]; } } else if (dataroot instanceof nj.NdArray) { + console.log("dataroot before ndim", dataroot); ndim = dataroot.shape.length; + console.log("ndim", ndim); } if (ndim < 3 && ndim > 0) { @@ -327,9 +346,16 @@ function dopreview(key, idx, isinternal, hastime) { '

Data preview

×
' ); if (dataroot instanceof nj.NdArray) { + console.log("dataroot", dataroot); if (dataroot.shape[0] > dataroot.shape[1]) dataroot = dataroot.transpose(); + console.log("is nj.NdArray:", dataroot instanceof nj.NdArray); + console.log("dtype:", dataroot.dtype); + console.log("shape:", dataroot.shape); + console.log("size:", dataroot.size); + let plotdata = dataroot.tolist(); + console.log("plotdata", plotdata); if (hastime.length == 0) { if (plotdata[0] instanceof Array) plotdata.unshift([...Array(plotdata[0].length).keys()]); @@ -357,12 +383,14 @@ function dopreview(key, idx, isinternal, hastime) { : hastime[i]; } let u = new uPlot(opts, plotdata, document.getElementById("plotchart")); + console.log("first u", u); } else { let u = new uPlot( opts, [[...Array(dataroot.length).keys()], dataroot], document.getElementById("plotchart") ); + console.log("second u", u); } // add spinner // --- NEW LOGIC for 2D plot --- @@ -375,6 +403,7 @@ function dopreview(key, idx, isinternal, hastime) { // for spinner // --- Signal React that 2D preview is ready --- + window.__previewType = "2d"; if (typeof window.__onPreviewReady === "function") { window.__onPreviewReady(); } @@ -1700,14 +1729,52 @@ function previewdataurl(url, idx) { console.warn("⚠️ Unsupported file format for preview:", url); return; } - // disable cache + // cached + // if (urldata.hasOwnProperty(url)) { + // if ( + // urldata[url] instanceof nj.NdArray || + // urldata[url].hasOwnProperty("MeshNode") + // ) { + // previewdata(urldata[url], idx, false); + // } + // return; + // } + if (urldata.hasOwnProperty(url)) { - if ( - urldata[url] instanceof nj.NdArray || - urldata[url].hasOwnProperty("MeshNode") - ) { - previewdata(urldata[url], idx, false); + const cached = urldata[url]; + + // fNIRS / time-series (cached) + if (cached?.data?.dataTimeSeries) { + let serieslabel = true; + if (cached.data.measurementList) { + serieslabel = Array(cached.data.measurementList.length) + .fill("") + .map( + (_, i) => + "S" + + cached.data.measurementList[i].sourceIndex + + "D" + + cached.data.measurementList[i].detectorIndex + ); + } + + const plotData2D = nj.concatenate( + cached.data.time.reshape(cached.data.time.size, 1), + cached.data.dataTimeSeries + ).T; + + previewdata(plotData2D, idx, false, serieslabel); // triggers __onPreviewReady + return; + } + + // Mesh/volume (cached) + if (cached instanceof nj.NdArray || cached?.MeshNode) { + previewdata(cached, idx, false); // triggers __onPreviewReady + return; } + + // Fallback: still try to preview whatever it is + previewdata(cached, idx, false); return; } @@ -1782,6 +1849,7 @@ function previewdataurl(url, idx) { } var plotdata = bjd; + console.log("plotdata", plotdata); if (linkpath.length > 1 && !linkpath[1].match(/^Mesh[NSEVT]/)) { let objpath = linkpath[1].split(/(?