Skip to content
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