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..49678f8 100644 --- a/src/pages/AboutPage.tsx +++ b/src/pages/AboutPage.tsx @@ -10,31 +10,18 @@ import { 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" }, -]; - 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 videoData = [ { src: "search.png", alt: "search icon", tip: "Search tutotial video", - video: "preview_video.mp4", + video: "search_video.mp4", ref: searchVideoRef, }, { @@ -44,28 +31,59 @@ 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, - }, ]; - 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 +187,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 +247,7 @@ const AboutPage: React.FC = () => { }} > { > Getting Started with NeuroJSON - - - + + + + + + + + + + + + + + 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(/(?