Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions src/components/SearchPage/DatasetCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ interface DatasetCardProps {
value: {
name?: string;
readme?: string;
aisummary?: string | Record<string, string>;
modality?: string[];
subj?: string[];
info?: {
Expand Down Expand Up @@ -94,15 +95,30 @@ const containsKeyword = (text?: string, kw?: string) => {
return words.some((w) => t.includes(w));
};

/** AISummary can be a plain string OR a sectioned object
* ({Introduction, Methods, Results, Conclusion}). Flatten to one string so we
* can search/snippet it — mirrors the detail page's handling. */
const flattenAiSummary = (s: any): string | undefined => {
if (!s) return undefined;
if (typeof s === "string") return s;
if (typeof s === "object")
return Object.values(s).filter(Boolean).join(" ");
return undefined;
};

/** Find a short snippet in secondary fields if not already visible */
function findMatchSnippet(
v: any,
kw?: string
): { label: string; html: string } | null {
if (!kw) return null;

// Which fields to scan (can add/remove fields here)
// Which fields to scan (can add/remove fields here).
// "AI Summary" is first so a topic-word hit in the generated summary is the
// explanation shown (its text lives in the dbinfo view's `aisummary` field).
const CANDIDATE_FIELDS: Array<[string, (v: any) => string | undefined]> = [
["AI Summary", (v) => flattenAiSummary(v?.aisummary)],
["Description", (v) => v?.info?.Description],
["Acknowledgements", (v) => v?.info?.Acknowledgements],
[
"Funding",
Expand Down Expand Up @@ -377,7 +393,8 @@ const DatasetCard: React.FC<DatasetCardProps> = ({
paragraph
sx={{ textOverflow: "ellipsis" }}
>
<strong>Summary:</strong> {highlightKeyword(readme, keyword)}
<strong>README:</strong> {highlightKeyword(readme, keyword)}
{readme.length >= 256 ? "…" : ""}
</Typography>
)}
</Stack>
Expand Down
3 changes: 2 additions & 1 deletion src/components/User/Dashboard/DatasetOrganizer/LLMPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1590,6 +1590,7 @@ const LLMPanel: React.FC<LLMPanelProps> = ({
</MenuItem>
<MenuItem value="mri">MRI</MenuItem>
<MenuItem value="nirs">NIRS</MenuItem>
<MenuItem value="eeg">EEG</MenuItem>
<MenuItem value="mixed">Mixed</MenuItem>
</Select>
{modalityError && (
Expand All @@ -1605,7 +1606,7 @@ const LLMPanel: React.FC<LLMPanelProps> = ({

<TextField
label="Describe your dataset (optional)"
placeholder='e.g. "DICOM files from 2 subjects, one male one female"'
placeholder='e.g. "DICOM files from 2 subjects, one male one female" or "EDF recordings from 5 subjects, rest and task runs"'
value={describeText}
onChange={(e) => setDescribeText(e.target.value)}
size="small"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,19 @@ export const categorizeFile = (file: FileItem): string => {
)
return "nirs";

// eeg — mirrors EEG_EXT = {'.edf', '.vhdr', '.set', '.bdf'}
if ([".edf", ".vhdr", ".set", ".bdf"].some((e) => name.endsWith(e)))
return "eeg";

// eeg_aux — mirrors EEG_AUX_EXT {'.vmrk', '.eeg', '.fdt'}
// + EEG_EVENT_EXT {'.event', '.events', '.evt', '.mrk'}
if (
[".vmrk", ".eeg", ".fdt", ".event", ".events", ".evt", ".mrk"].some((e) =>
name.endsWith(e)
)
)
return "eeg_aux";

// mri — mirrors MRI_EXT = {'.nii', '.dcm'} + .nii.gz
if (
name.endsWith(".nii.gz") ||
Expand Down Expand Up @@ -140,6 +153,13 @@ export const detectModality = (files: FileItem[]): string => {
files.some((f) => f.name.endsWith(".snirf"))
)
return "nirs";
// eeg — fileType keys from fileProcessors.ts getFileType()
if (
counts.eegEdf > 0 ||
counts.eegBrainvision > 0 ||
counts.eegEeglab > 0
)
return "eeg";
return "mixed";
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ export const getFileType = (name: string): string => {
matlab: ["mat"],
dicom: ["dcm"],
nirs: ["nirs"],
eegEdf: ["edf", "bdf"],
eegBrainvision: ["vhdr", "vmrk", "eeg"],
eegEeglab: ["set", "fdt"],
};

for (const [type, extensions] of Object.entries(fileTypes)) {
Expand Down
40 changes: 40 additions & 0 deletions src/components/User/Dashboard/DatasetOrganizer/utils/llm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,15 @@ fNIRS FORMATS (modality: nirs):
• Homer3 (.nirs) → convert_to: snirf
• MATLAB (.mat) → convert_to: snirf

EEG FORMATS (modality: eeg):
• EDF/EDF+ (.edf) → format_ready: true (copy directly)
• BrainVision (.vhdr) → format_ready: true (copy directly)
• EEGLAB (.set) → format_ready: true (copy directly)
• Biosemi (.bdf) → format_ready: true (copy directly)
CRITICAL: EEG files are NEVER converted. Always format_ready: true, convert_to: none.
CRITICAL: EEG bids_template MUST end with '_eeg.<original_ext>' (e.g. '_eeg.edf').
NEVER use NIfTI suffixes (T1w, T2w, bold) for EEG data.

═══════════════════════════════════════════════════════════════════════
SUBJECT IDENTIFICATION — MOST IMPORTANT STEP
═══════════════════════════════════════════════════════════════════════
Expand Down Expand Up @@ -495,6 +504,23 @@ For MRI: use acq- to distinguish different scan series from same subject.
VHFCT1mm-Ankle.dcm → acq-ankle_T1w
VHFCT1mm-Head.dcm → acq-head_T1w

For EEG: infer task and run from filename suffixes or directory names.
RULE 1 — If each subject has multiple EEG files, each file is a separate scan.
Identify what differs between files of the same subject (suffix, keyword, directory).
Map each variant to a distinct task- or run- label from user description.
If task labels cannot be inferred, use run-1, run-2, run-N.
RULE 2 — Create one mapping entry per unique file variant across subjects.
RULE 3 — BIDS directory for EEG is always 'eeg/', never 'anat/' or 'nirs/'.
RULE 4 — BIDS filename suffix is always '_eeg' + original extension.

EEG FILENAME EXAMPLES (CRITICAL — follow exactly):
✓ sub-01_task-rest_eeg.edf
✓ sub-01_task-arithmetic_eeg.edf
✓ sub-01_run-1_eeg.edf
✗ sub-01_T1w.nii.gz ← NEVER for EEG
✗ sub-01_unknown.nii.gz ← NEVER for EEG
✗ sub-01_bold.nii.gz ← NEVER for EEG

═══════════════════════════════════════════════════════════════════════
OUTPUT FORMAT
═══════════════════════════════════════════════════════════════════════
Expand Down Expand Up @@ -527,6 +553,20 @@ mappings:
- match_pattern: '.*'
bids_template: 'sub-X_task-walking_nirs.snirf'

# EEG example — when each subject has ONE edf file:
- modality: eeg
match: ['**/*.edf']
exclude: []
format_ready: true
convert_to: none
filename_rules:
- match_pattern: '.*'
bids_template: 'sub-X_task-rest_eeg.edf'

# EEG example — when each subject has MULTIPLE edf files (different tasks/runs):
# Create one mapping entry per task/run, use match_pattern to distinguish them.
# The match_pattern must be derived from what actually differs in the filenames.

OUTPUT: Raw YAML only (no markdown, no explanation)`;

export const PROMPT_MAT_SNIRF_MAPPING = `You are an fNIRS data format expert.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,10 @@ export interface BuildBidsPlanResult {
// Mirrors planner.py _DATA_EXTS, evidence.py TRIO_NAMES
// ============================================================================

// Mirrors planner.py _DATA_EXTS — EEG primary formats (edf/vhdr/set/bdf)
// are data files; EEG aux (.vmrk/.eeg/.fdt) are handled separately, not here.
const DATA_EXTENSIONS =
// /\.(snirf|nii|nii\.gz|dcm|mat|nirs|jnii|bnii|h5|hdf5|edf|bdf)$/i;
/\.(snirf|nii|nii\.gz|dcm|mat|nirs|jnii|bnii)$/i;
/\.(snirf|nii|nii\.gz|dcm|mat|nirs|jnii|bnii|edf|vhdr|set|bdf)$/i;

const TRIO_FILENAMES = new Set([
"dataset_description.json",
Expand Down
3 changes: 3 additions & 0 deletions src/redux/projects/types/projects.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ export interface FileItem {
| "matlab"
| "dicom"
| "nirs"
| "eegEdf"
| "eegBrainvision"
| "eegEeglab"
| "array"
| "other";
content?: string;
Expand Down
Loading
Loading