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 (
<>