Skip to content
Open
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
15 changes: 15 additions & 0 deletions backend/app/controllers/vcelldb_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
fetch_biomodels,
fetch_simulation_details,
get_vcml_file,
get_bngl_file,
get_sbml_file,
get_diagram_url,
get_diagram_image,
Expand Down Expand Up @@ -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.
Expand Down
10 changes: 10 additions & 0 deletions backend/app/routes/vcelldb_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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):
"""
Expand Down
86 changes: 86 additions & 0 deletions backend/app/services/databases_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
"""
Expand Down
4 changes: 4 additions & 0 deletions frontend/app/search/[bmid]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -493,6 +494,9 @@ export default function BiomodelDetailPage() {
/>
</div>

{/* BNGL Visualization Section */}
<BnglVisualizerSection biomodelId={data.bmKey} />

{/* Description Section */}
<Collapsible className="mb-6" defaultOpen>
<CollapsibleTrigger asChild>
Expand Down
91 changes: 91 additions & 0 deletions frontend/components/BnglVisualizerSection.tsx
Original file line number Diff line number Diff line change
@@ -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<BnglVisualizerSectionProps> = ({
biomodelId,
}) => {
const [bnglData, setBnglData] = useState<string>("");
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState("");
const [isVisible, setIsVisible] = useState(false);
const iframeRef = useRef<HTMLIFrameElement>(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 (
<div className="bg-white border border-slate-200 rounded-lg shadow-sm overflow-hidden">
<div className="bg-slate-50 border-b border-slate-200 px-4 py-3">
<h3 className="text-lg font-semibold text-slate-900 flex items-center gap-2">
<Network className="h-4 w-4" />
BNGL Visualization
</h3>
</div>
<div className="p-4">
<div className="bg-slate-50 border border-slate-200 rounded-lg overflow-hidden">
<iframe
ref={iframeRef}
src="/bnglviz/index.html"
className="w-full h-[600px] border-0"
title="BNGL Visualization"
sandbox="allow-scripts allow-same-origin"
/>
</div>
</div>
</div>
);
};
Loading