Skip to content

Commit

Permalink
annotations page
Browse files Browse the repository at this point in the history
  • Loading branch information
magland committed Feb 26, 2025
1 parent bd20565 commit 23cad33
Show file tree
Hide file tree
Showing 4 changed files with 228 additions and 0 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# Changes

## February 26, 2025
- Added dedicated annotations page:
- Displays all user annotations in a compact table format
- Shows titles, content, types, targets, and tags
- Features clickable dandiset tags that navigate to the corresponding dandiset page
- Includes creation and update timestamps
- Requires Neurosift API key for access
- Added GitHub Actions workflow for pull requests that checks code formatting and build status
- Added annotations feature to Dandiset pages:
- Users can add, view, and expand markdown-formatted notes
Expand Down
5 changes: 5 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import OpenNeuroDatasetPage from "./pages/OpenNeuroDatasetPage/OpenNeuroDatasetP
import OpenNeuroPage from "./pages/OpenNeuroPage/OpenNeuroPage";
import SettingsPage from "./pages/SettingsPage/SettingsPage";
import EdfPage from "./pages/EdfPage/EdfPage";
import AnnotationsPage from "./pages/AnnotationsPage/AnnotationsPage";
import { sendUrlUpdate } from "./ai-integration/messaging/windowMessaging";

const theme = createTheme({
Expand Down Expand Up @@ -347,6 +348,10 @@ const AppContent = () => {
path="/edf"
element={<EdfPage width={width} height={mainHeight} />}
/>
<Route
path="/annotations"
element={<AnnotationsPage width={width} height={mainHeight} />}
/>
</Routes>
</div>

Expand Down
200 changes: 200 additions & 0 deletions src/pages/AnnotationsPage/AnnotationsPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import { FunctionComponent, useEffect, useState } from "react";
import {
Box,
Typography,
Paper,
Alert,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
Chip,
} from "@mui/material";
import { useNavigate } from "react-router-dom";
import { Annotation as BaseAnnotation } from "../../pages/DandisetPage/hooks/useAnnotations";

interface Annotation extends BaseAnnotation {
tags: string[];
}

const ANNOTATION_API_BASE_URL =
"https://neurosift-annotation-manager.vercel.app/api";
const NSJM_API_BASE_URL = "https://neurosift-job-manager.vercel.app/api";

type Props = {
width: number;
height: number;
};

const AnnotationsPage: FunctionComponent<Props> = ({ width, height }) => {
const [annotations, setAnnotations] = useState<Annotation[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const navigate = useNavigate();

const fetchAnnotations = async () => {
const apiKey = localStorage.getItem("neurosiftApiKey");
if (!apiKey) return;

setIsLoading(true);
setError(null);
try {
const userResponse = await fetch(
`${NSJM_API_BASE_URL}/users/by-api-key`,
{
headers: {
Authorization: `Bearer ${apiKey}`,
},
},
);

if (!userResponse.ok) {
throw new Error("Failed to get user info");
}

const { userId } = await userResponse.json();

const response = await fetch(
`${ANNOTATION_API_BASE_URL}/annotations?userId=${userId}`,
{
headers: {
Authorization: `Bearer ${apiKey}`,
},
},
);
if (!response.ok) {
throw new Error("Failed to fetch annotations");
}
const data = await response.json();
setAnnotations(data.annotations);
} catch (err) {
setError(
err instanceof Error ? err.message : "Failed to fetch annotations",
);
} finally {
setIsLoading(false);
}
};

useEffect(() => {
const apiKey = localStorage.getItem("neurosiftApiKey");
if (!apiKey) return;
fetchAnnotations();
}, []);

if (!localStorage.getItem("neurosiftApiKey")) {
return (
<Box sx={{ p: 3 }}>
<Alert severity="warning" sx={{ mb: 2 }}>
Please set your Neurosift API key in the{" "}
<span
style={{ cursor: "pointer", textDecoration: "underline" }}
onClick={() => navigate("/settings")}
>
settings page
</span>{" "}
to view your annotations.
</Alert>
</Box>
);
}

return (
<div style={{ position: "absolute", width, height, overflowY: "auto" }}>
<Box sx={{ p: 3 }}>
<Typography variant="h4" gutterBottom>
Your Annotations
</Typography>

{isLoading && <div>Loading...</div>}

{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}

<Paper sx={{ width: "100%", overflow: "hidden" }}>
<Table size="small" stickyHeader>
<TableHead>
<TableRow>
<TableCell>Title</TableCell>
<TableCell>Content</TableCell>
<TableCell>Type</TableCell>
<TableCell>Target</TableCell>
<TableCell>Tags</TableCell>
<TableCell>Created</TableCell>
<TableCell>Updated</TableCell>
</TableRow>
</TableHead>
<TableBody>
{annotations.map((annotation) => (
<TableRow key={annotation.id} hover>
<TableCell>{annotation.title}</TableCell>
<TableCell
sx={{
maxWidth: "300px",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{annotation.data.content}
</TableCell>
<TableCell>{annotation.type}</TableCell>
<TableCell>{annotation.targetType}</TableCell>
<TableCell>
<Box sx={{ display: "flex", gap: 0.5, flexWrap: "wrap" }}>
{annotation.tags.map((tag, index) => {
const isDandisetTag = tag.startsWith("dandiset:");
const chipProps = isDandisetTag
? {
onClick: () => {
const dandisetId = tag.split(":")[1];
navigate(`/dandiset/${dandisetId}`);
},
clickable: true,
}
: {};
return (
<Chip
key={index}
label={tag}
size="small"
variant="outlined"
sx={{
maxWidth: 150,
cursor: isDandisetTag ? "pointer" : "default",
}}
{...chipProps}
/>
);
})}
</Box>
</TableCell>
<TableCell>
{new Date(annotation.createdAt).toLocaleString()}
</TableCell>
<TableCell>
{annotation.updatedAt !== annotation.createdAt
? new Date(annotation.updatedAt).toLocaleString()
: "—"}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Paper>

{!isLoading && !error && annotations.length === 0 && (
<Typography align="center" sx={{ mt: 2 }}>
No annotations found.
</Typography>
)}
</Box>
</div>
);
};

export default AnnotationsPage;
17 changes: 17 additions & 0 deletions src/pages/HomePage/HomePage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Box, Button, Container, Paper, Typography, Link } from "@mui/material";
import { FunctionComponent, useEffect, useState } from "react";
import GitHubIcon from "@mui/icons-material/GitHub";
import EditNoteIcon from "@mui/icons-material/EditNote";
import { useNavigate } from "react-router-dom";
import ScrollY from "@components/ScrollY";

Expand Down Expand Up @@ -190,6 +191,22 @@ const HomePage: FunctionComponent<HomePageProps> = ({ width, height }) => {
<GitHubIcon />
<Typography variant="body2">Submit Feedback / Issues</Typography>
</Link>
<Typography
component="span"
onClick={() => navigate("/annotations")}
sx={{
display: "flex",
alignItems: "center",
gap: 1,
color: "text.secondary",
textDecoration: "none",
cursor: "pointer",
"&:hover": { color: "primary.main" },
}}
>
<EditNoteIcon />
<Typography variant="body2">Annotations</Typography>
</Typography>
</Box>
<Typography
variant="body2"
Expand Down

0 comments on commit 23cad33

Please sign in to comment.