diff --git a/packages/libs/wdk-client/src/Components/Display/CollapsibleSection.tsx b/packages/libs/wdk-client/src/Components/Display/CollapsibleSection.tsx index a8570755b2..92359fe13e 100644 --- a/packages/libs/wdk-client/src/Components/Display/CollapsibleSection.tsx +++ b/packages/libs/wdk-client/src/Components/Display/CollapsibleSection.tsx @@ -33,7 +33,7 @@ const buttonStyle: React.CSSProperties = { padding: 0, }; -function CollapsibleSection(props: Props) { +export function CollapsibleSection(props: Props) { const { className, id, diff --git a/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/records/AlphaFoldAttributeSection.tsx b/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/records/AlphaFoldAttributeSection.tsx index 5099634a84..620db4550b 100644 --- a/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/records/AlphaFoldAttributeSection.tsx +++ b/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/records/AlphaFoldAttributeSection.tsx @@ -1,21 +1,102 @@ import React, { useEffect, useRef, useState } from 'react'; + import { BlockRecordAttributeSection, Props, } from '@veupathdb/wdk-client/lib/Views/Records/RecordAttributes/RecordAttributeSection'; +import { DefaultSectionTitle } from '@veupathdb/wdk-client/lib/Views/Records/SectionTitle'; + +import { CollapsibleSection } from '@veupathdb/wdk-client/lib/Components/Display/CollapsibleSection'; + +function AlphaFoldErrorWrapper({ + children, + attribute: { name, displayName, help }, + isCollapsed, + onCollapsedChange, + title, +}: Props & { children: React.ReactNode }) { + const headerContent = title ?? ( + + ); + + return ( + +
{children}
+
+ ); +} + /* - * This component does two things: - * - * 1. It imports the required assets needed to render - * the web component. - * 2. It renders the attribute section as a block section. + * This component: + * 1. Pre-validates the AlphaFold data URL before rendering the web component + * 2. Imports the required assets for the pdbe-molstar viewer + * 3. Shows a friendly error if the AlphaFold structure file is not found */ export function AlphaFoldRecordSection(props: Props) { const areAssetsLoadingRef = useRef(false); + const [dataUrlStatus, setDataUrlStatus] = + useState<'loading' | 'valid' | 'invalid' | null>(null); + + // Get the attribute value (HTML containing the pdbe-molstar element) + const attributeName = props.attribute.name; + const attributeValue = props.record.attributes[attributeName]; + + // Extract the custom-data-url from the HTML + const extractDataUrl = (htmlString: string): string | null => { + if (typeof htmlString !== 'string') return null; + const match = htmlString.match(/custom-data-url=["']([^"']+)["']/); + return match ? match[1] : null; + }; + + const dataUrl = extractDataUrl(attributeValue + ''); + const hasDataUrl = dataUrl !== null && dataUrl !== ''; + + // Pre-validate the data URL + useEffect(() => { + if (!props.isCollapsed && hasDataUrl && dataUrlStatus === null) { + setDataUrlStatus('loading'); + + // Make a HEAD request to check if the file exists + if (dataUrl !== null) { + fetch(dataUrl, { method: 'HEAD' }) + .then((response) => { + if (response.ok) { + setDataUrlStatus('valid'); + } else { + console.warn( + `AlphaFold structure file not found: ${dataUrl} (${response.status})` + ); + setDataUrlStatus('invalid'); + } + }) + .catch((error) => { + console.warn( + `Failed to validate AlphaFold structure file: ${dataUrl}`, + error + ); + setDataUrlStatus('invalid'); + }); + } else { + console.error('URL is null, cannot fetch data.'); + } + } + }, [props.isCollapsed, hasDataUrl, dataUrl, dataUrlStatus]); + + // Load viewer assets only if data URL is valid useEffect(() => { - if (!props.isCollapsed && !areAssetsLoadingRef.current) { + if ( + !props.isCollapsed && + !areAssetsLoadingRef.current && + dataUrlStatus === 'valid' + ) { // Using dynamic import to lazy load these scripts // @ts-ignore import('../../../../../../vendored/pdbe-molstar-light-3.0.0.css'); @@ -23,7 +104,83 @@ export function AlphaFoldRecordSection(props: Props) { import('../../../../../../vendored/pdbe-molstar-component-3.0.0.js'); areAssetsLoadingRef.current = true; } - }, [props.isCollapsed]); + }, [props.isCollapsed, dataUrlStatus]); + + // Handle missing data URL + if (!hasDataUrl) { + return ( +
+

+ AlphaFold structure prediction not available for this gene. +

+
+ ); + } + + // Handle data URL validation in progress + if (dataUrlStatus === 'loading') { + return ( +
+

+ Loading structure data... +

+
+ ); + } + + // Handle invalid/not found data URL + if (dataUrlStatus === 'invalid') { + return ( + +
+
+

AlphaFold Structure Prediction Visualization not available

+

+ The predicted structure file could not be found. This may be + because: +

+
    +
  • The structure has not been predicted yet
  • +
  • The structure file is temporarily unavailable
  • +
  • + This gene/protein is not eligible for AlphaFold prediction +
  • +
+ {process.env.NODE_ENV !== 'production' && ( +
+ + Technical details + + + {dataUrl} + {' '} + returns 404 Not Found. +
+ )} +
+
+
+ ); + } + + // Render normally if data URL is valid return ( <>