diff --git a/backend/app/controllers/vcelldb_controller.py b/backend/app/controllers/vcelldb_controller.py index a95668a..a9563ea 100644 --- a/backend/app/controllers/vcelldb_controller.py +++ b/backend/app/controllers/vcelldb_controller.py @@ -6,6 +6,7 @@ fetch_biomodels, fetch_simulation_details, get_vcml_file, + get_bngl_file, get_sbml_file, get_diagram_url, get_diagram_image, @@ -71,6 +72,20 @@ async def get_vcml_controller(biomodel_id: str, truncate: bool = False) -> str: raise HTTPException(status_code=500, detail="Error fetching VCML URL.") +async def get_bngl_controller(biomodel_id: str) -> dict: + """ + Controller function to fetch the contents of the BNGL file for a biomodel. + Returns: + dict: Contains the BNGL data or empty if not rule-based. + """ + try: + bngl_content = await get_bngl_file(biomodel_id) + return {"data": bngl_content} + except Exception as e: + # For BNGL, we want to return empty data instead of error for non-rule-based models + return {"data": ""} + + async def get_sbml_controller(biomodel_id: str) -> str: """ Controller function to fetch the contents of the SBML file for a biomodel. diff --git a/backend/app/routes/vcelldb_router.py b/backend/app/routes/vcelldb_router.py index a4ad835..608d78f 100644 --- a/backend/app/routes/vcelldb_router.py +++ b/backend/app/routes/vcelldb_router.py @@ -5,6 +5,7 @@ get_biomodels_controller, get_simulation_details_controller, get_vcml_controller, + get_bngl_controller, get_sbml_controller, get_diagram_url_controller, get_diagram_image_controller, @@ -51,6 +52,15 @@ async def get_vcml(biomodel_id: str, truncate: bool = False): raise e +@router.get("/biomodel/{biomodel_id}/biomodel.bngl", response_model=dict) +async def get_bngl(biomodel_id: str): + """ + Endpoint to get BNGL file contents for a given biomodel. + Returns empty data if the model is not rule-based. + """ + return await get_bngl_controller(biomodel_id) + + @router.get("/biomodel/{biomodel_id}/biomodel.sbml", response_model=str) async def get_sbml(biomodel_id: str): """ diff --git a/backend/app/services/databases_service.py b/backend/app/services/databases_service.py index 5d94bf1..cc566e9 100644 --- a/backend/app/services/databases_service.py +++ b/backend/app/services/databases_service.py @@ -319,6 +319,92 @@ async def get_vcml_file( ) +@observe(name="GET_BNGL_FILE") +async def get_bngl_file( + biomodel_id: str, max_retries: int = 3 +) -> str: + """ + Fetches the BNGL file content for a given biomodel with retry logic. + + Args: + biomodel_id (str): ID of the biomodel. + max_retries (int): Maximum number of retry attempts. + Returns: + str: BNGL content of the biomodel, or empty string if not rule-based. + """ + logger.info(f"Fetching BNGL file for biomodel: {biomodel_id}") + + # Check connectivity first + if not await check_vcell_connectivity(): + logger.error( + "VCell API is not reachable. Please check your network connection and DNS settings." + ) + raise Exception( + "VCell API is not reachable. Please check your network connection and DNS settings." + ) + + for attempt in range(max_retries + 1): + try: + url = f"{VCELL_API_BASE_URL}/biomodel/{biomodel_id}/biomodel.bngl" + logger.info( + f"Requesting URL: {url} (attempt {attempt + 1}/{max_retries + 1})" + ) + + async with httpx.AsyncClient(timeout=30.0) as client: + response = await client.get(url) + logger.info(f"Response status: {response.status_code}") + logger.info(f"Response headers: {dict(response.headers)}") + + if response.status_code == 404: + logger.info(f"BNGL not available for biomodel {biomodel_id}") + return "" + + response.raise_for_status() + bngl_content = response.text.strip() + + # Check if content is empty or minimal (some models might return empty BNGL) + if not bngl_content or len(bngl_content) < 50: + logger.info(f"Empty or minimal BNGL content for biomodel {biomodel_id}") + return "" + + return bngl_content + + except httpx.HTTPStatusError as e: + if e.response.status_code == 404: + logger.info(f"BNGL not available for biomodel {biomodel_id} (404 error)") + return "" + logger.error( + f"HTTP error fetching BNGL file for biomodel {biomodel_id}: {e.response.status_code} - {e.response.text}" + ) + if attempt == max_retries: + raise e + logger.warning(f"Retrying in {2 ** attempt} seconds...") + await asyncio.sleep(2**attempt) + + except httpx.RequestError as e: + logger.error( + f"Request error fetching BNGL file for biomodel {biomodel_id}: {str(e)}" + ) + if attempt == max_retries: + raise e + logger.warning(f"Retrying in {2 ** attempt} seconds...") + await asyncio.sleep(2**attempt) + + except Exception as e: + logger.error( + f"Unexpected error fetching BNGL file for biomodel {biomodel_id}: {str(e)}" + ) + if attempt == max_retries: + raise e + logger.warning(f"Retrying in {2 ** attempt} seconds...") + await asyncio.sleep(2**attempt) + + # This should never be reached, but just in case + raise Exception( + f"Failed to fetch BNGL file for biomodel {biomodel_id} after {max_retries + 1} attempts" + ) + + @observe(name="GET_SBML_FILE") async def get_sbml_file(biomodel_id: str) -> str: """ diff --git a/frontend/app/search/[bmid]/page.tsx b/frontend/app/search/[bmid]/page.tsx index cc16d0f..4c2e110 100644 --- a/frontend/app/search/[bmid]/page.tsx +++ b/frontend/app/search/[bmid]/page.tsx @@ -10,6 +10,7 @@ import { } from "@/components/ui/collapsible"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { ChatBox } from "@/components/ChatBox"; +import { BnglVisualizerSection } from "@/components/BnglVisualizerSection"; import { User, Lock, @@ -493,6 +494,9 @@ export default function BiomodelDetailPage() { /> + {/* BNGL Visualization Section */} + + {/* Description Section */} diff --git a/frontend/components/BnglVisualizerSection.tsx b/frontend/components/BnglVisualizerSection.tsx new file mode 100644 index 0000000..50f489b --- /dev/null +++ b/frontend/components/BnglVisualizerSection.tsx @@ -0,0 +1,91 @@ +import React, { useEffect, useState, useRef } from "react"; +import { Network, Loader2 } from "lucide-react"; + +interface BnglVisualizerSectionProps { + biomodelId: string; +} + +export const BnglVisualizerSection: React.FC = ({ + biomodelId, +}) => { + const [bnglData, setBnglData] = useState(""); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(""); + const [isVisible, setIsVisible] = useState(false); + const iframeRef = useRef(null); + + useEffect(() => { + const fetchBnglData = async () => { + setIsLoading(true); + setError(""); + try { + const apiUrl = process.env.NEXT_PUBLIC_API_URL; + const res = await fetch(`${apiUrl}/biomodel/${biomodelId}/biomodel.bngl`); + + if (res.ok) { + const data = await res.json(); + if (data.data && data.data.trim()) { + setBnglData(data.data); + setIsVisible(true); + } else { + // Model is not rule-based, don't show the section + setIsVisible(false); + } + } else { + // Error fetching, don't show the section + setIsVisible(false); + } + } catch (err) { + // Error fetching, don't show the section + setIsVisible(false); + } finally { + setIsLoading(false); + } + }; + + if (biomodelId) { + fetchBnglData(); + } + }, [biomodelId]); + + useEffect(() => { + const handleMessage = (event: MessageEvent) => { + if (event.data === 'bnglviz-ready' && bnglData && iframeRef.current) { + // Send BNGL data to iframe + iframeRef.current.contentWindow?.postMessage(bnglData, '*'); + } + }; + + if (isVisible && bnglData) { + window.addEventListener('message', handleMessage); + return () => window.removeEventListener('message', handleMessage); + } + }, [bnglData, isVisible]); + + // Don't render anything if loading, error, or not visible + if (isLoading || error || !isVisible || !bnglData) { + return null; + } + + return ( +
+
+

+ + BNGL Visualization +

+
+
+
+