diff --git a/frontend/public/schemas/20251121/linkml/manifest.json b/frontend/public/schemas/20251121/linkml/manifest.json index d1c57fc498..0f62481474 100644 --- a/frontend/public/schemas/20251121/linkml/manifest.json +++ b/frontend/public/schemas/20251121/linkml/manifest.json @@ -1,5 +1,5 @@ { - "generated": "2026-01-04T22:18:27.627Z", + "generated": "2026-01-05T08:03:59.341Z", "version": "1.0.0", "categories": [ { diff --git a/frontend/src/components/uml/UMLVisualization.css b/frontend/src/components/uml/UMLVisualization.css index 315ffa98d3..250a7631b1 100644 --- a/frontend/src/components/uml/UMLVisualization.css +++ b/frontend/src/components/uml/UMLVisualization.css @@ -153,6 +153,35 @@ background: #f9fafb; } +.uml-visualization__search-count { + padding: 0.5rem 0.85rem; + font-size: 0.7rem; + color: #6b7280; + background: #f3f4f6; + border-bottom: 1px solid #e5e7eb; + text-align: center; +} + +.uml-visualization__search-more-btn { + display: block; + width: 100%; + padding: 0.6rem 0.85rem; + font-size: 0.75rem; + color: #0a3dfa; + font-weight: 500; + text-align: center; + background: #f9fafb; + border: none; + border-top: 1px solid #e5e7eb; + cursor: pointer; + transition: all 0.15s; +} + +.uml-visualization__search-more-btn:hover { + background: #eff6ff; + color: #0832d1; +} + .uml-visualization__zoom { font-size: 0.6875rem; color: #666; @@ -738,4 +767,21 @@ border-top-color: #374151; background: #111827; } + + .uml-visualization__search-count { + color: #9ca3af; + background: #111827; + border-bottom-color: #374151; + } + + .uml-visualization__search-more-btn { + color: #818cf8; + background: #111827; + border-top-color: #374151; + } + + .uml-visualization__search-more-btn:hover { + background: #1e3a5f; + color: #a5b4fc; + } } diff --git a/frontend/src/components/uml/UMLVisualization.tsx b/frontend/src/components/uml/UMLVisualization.tsx index d64b3cf62b..a31e99a2d8 100644 --- a/frontend/src/components/uml/UMLVisualization.tsx +++ b/frontend/src/components/uml/UMLVisualization.tsx @@ -290,6 +290,7 @@ const UMLVisualizationInner: React.FC = ({ const [searchQuery, setSearchQuery] = useState(''); const [searchResults, setSearchResults] = useState([]); const [showSearchResults, setShowSearchResults] = useState(false); + const [visibleResultsCount, setVisibleResultsCount] = useState(10); const [showProvenanceLinks, setShowProvenanceLinks] = useState(false); // Provenance links hidden by default const [showLegend, setShowLegend] = useState(false); // Cardinality legend dropdown const [edgeTooltip, setEdgeTooltip] = useState<{ label: string; x: number; y: number; bidirectional: boolean; containerWidth: number; containerHeight: number } | null>(null); @@ -2275,6 +2276,7 @@ const UMLVisualizationInner: React.FC = ({ // Search handler - filter nodes based on query const handleSearch = (query: string) => { setSearchQuery(query); + setVisibleResultsCount(10); // Reset visible count on new search if (query.trim() === '') { setSearchResults([]); setShowSearchResults(false); @@ -2359,7 +2361,12 @@ const UMLVisualizationInner: React.FC = ({ /> {showSearchResults && searchResults.length > 0 && (
- {searchResults.slice(0, 10).map(node => ( + {searchResults.length > visibleResultsCount && ( +
+ Showing {visibleResultsCount} of {searchResults.length} results +
+ )} + {searchResults.slice(0, visibleResultsCount).map(node => ( ))} - {searchResults.length > 10 && ( + {searchResults.length > visibleResultsCount && visibleResultsCount < 50 && ( + + )} + {visibleResultsCount >= 50 && searchResults.length > 50 && (
- +{searchResults.length - 10} more results + +{searchResults.length - 50} more results (max 50 shown)
)}
diff --git a/frontend/src/lib/ontology/ontology-loader.ts b/frontend/src/lib/ontology/ontology-loader.ts index ff6401af07..1354bdb496 100644 --- a/frontend/src/lib/ontology/ontology-loader.ts +++ b/frontend/src/lib/ontology/ontology-loader.ts @@ -621,6 +621,8 @@ function parseTurtleOntology(content: string): ParsedOntology { let currentTriples: Array<{ predicate: string; object: string }> = []; let lastPredicate: string | null = null; // Track last predicate for comma continuations let blankNodeDepth = 0; // Track depth of blank node blocks to skip + let inMultiLineString = false; // Track if we're inside a multi-line triple-quoted string + let multiLineQuoteChar = ''; // The quote character(s) for the multi-line string // First pass: extract prefixes for (const line of lines) { @@ -634,18 +636,76 @@ function parseTurtleOntology(content: string): ParsedOntology { } // Second pass: parse triples + let lineNum = 0; for (const line of lines) { + lineNum++; const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('@prefix') || trimmed.startsWith('PREFIX')) { continue; } - // Track blank node depth across lines + // Handle multi-line triple-quoted strings + // If we're inside a multi-line string, check if this line closes it + let lineToProcess = trimmed; // The portion of the line to process for brackets + if (inMultiLineString) { + const closeIndex = trimmed.indexOf(multiLineQuoteChar); + if (closeIndex !== -1) { + // Found the closing triple-quote, process rest of line after it + inMultiLineString = false; + const afterQuote = trimmed.substring(closeIndex + 3); + // Count brackets in the part after the closing quotes + for (const char of afterQuote) { + if (char === '[') blankNodeDepth++; + else if (char === ']') blankNodeDepth--; + } + // If the line after closing quote is just "] ;" or similar, skip it + if (afterQuote.trim().match(/^[\]\s;,.]*$/)) { + continue; + } + // IMPORTANT: Only process the part AFTER the closing quote for bracket detection + // Otherwise we'll re-detect the opening """ and incorrectly enter multi-line mode + lineToProcess = afterQuote; + } else { + // Still inside the multi-line string, skip this line entirely + continue; + } + } + + // Track blank node depth across lines BEFORE processing + // We need to track this first to know if we should skip this line + const prevBlankNodeDepth = blankNodeDepth; + // Count opening and closing brackets (outside quotes) + // IMPORTANT: Use lineToProcess (not trimmed) to avoid re-detecting """ after closing a multi-line string let inQuotes = false; - for (const char of trimmed) { - if (char === '"' || char === "'") { - inQuotes = !inQuotes; + let quoteChar = ''; + for (let i = 0; i < lineToProcess.length; i++) { + const char = lineToProcess[i]; + if (!inQuotes && (char === '"' || char === "'")) { + // Check for triple-quoted strings + if (lineToProcess.substring(i, i + 3) === '"""' || lineToProcess.substring(i, i + 3) === "'''") { + // Check if the closing triple-quote is on the same line + const restOfLine = lineToProcess.substring(i + 3); + const closeIndex = restOfLine.indexOf(lineToProcess.substring(i, i + 3)); + if (closeIndex === -1) { + // Triple-quote opens but doesn't close on this line - multi-line string + inMultiLineString = true; + multiLineQuoteChar = lineToProcess.substring(i, i + 3); + break; // Stop processing brackets on this line + } else { + // Triple-quote opens and closes on same line, skip to after closing + i += 3 + closeIndex + 2; // +3 for opening, +closeIndex to reach close, +2 to skip close + } + } else { + inQuotes = true; + quoteChar = char; + } + } else if (inQuotes) { + // Check for end of single-quoted string + if (quoteChar.length === 1 && char === quoteChar && lineToProcess[i - 1] !== '\\') { + inQuotes = false; + quoteChar = ''; + } } else if (!inQuotes) { if (char === '[') blankNodeDepth++; else if (char === ']') blankNodeDepth--; @@ -653,12 +713,16 @@ function parseTurtleOntology(content: string): ParsedOntology { } // Skip lines that are entirely within a blank node block - // (but process lines that start or end a blank node block for proper depth tracking) - if (blankNodeDepth > 0 && !trimmed.includes('[') && !trimmed.includes(']')) { + // A line is "inside" a blank node if: + // 1. We started inside (prevBlankNodeDepth > 0) and we're still inside (blankNodeDepth > 0) + // 2. OR we're on a line that only closes brackets + if (prevBlankNodeDepth > 0 && blankNodeDepth >= 0 && !trimmed.match(/^[a-zA-Z_]/)) { + // We're inside a blank node block and this line doesn't start a new subject continue; } - // Also skip lines that only close a blank node - if (trimmed === ']' || trimmed === '] ;' || trimmed === '] ,' || trimmed === '] .') { + + // Skip lines that only contain blank node closure + if (trimmed.match(/^[\]\s;,.]+$/)) { continue; } diff --git a/frontend/src/pages/OntologyViewerPage.css b/frontend/src/pages/OntologyViewerPage.css index 63d8772fa8..1f651ac3fd 100644 --- a/frontend/src/pages/OntologyViewerPage.css +++ b/frontend/src/pages/OntologyViewerPage.css @@ -760,72 +760,6 @@ font-size: 1rem; } -/* UML Large Diagram Warning */ -.ontology-viewer__uml-warning { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 1rem; - padding: 3rem 2rem; - min-height: 400px; - background: var(--surface-color, #fff); - border-radius: 8px; - text-align: center; -} - -.ontology-viewer__uml-warning-icon { - font-size: 3rem; - line-height: 1; -} - -.ontology-viewer__uml-warning h3 { - margin: 0; - font-size: 1.5rem; - font-weight: 600; - color: var(--text-primary, #212121); -} - -.ontology-viewer__uml-warning-stats { - margin: 0; - font-size: 1.125rem; - color: var(--text-secondary, #757575); -} - -.ontology-viewer__uml-warning-stats strong { - color: var(--primary-color, #1976d2); - font-weight: 600; -} - -.ontology-viewer__uml-warning-text { - margin: 0; - max-width: 400px; - font-size: 0.95rem; - color: var(--text-secondary, #757575); - line-height: 1.5; -} - -.ontology-viewer__uml-warning-button { - margin-top: 0.5rem; - padding: 0.75rem 1.5rem; - background: var(--primary-color, #1976d2); - color: white; - border: none; - border-radius: 6px; - font-size: 1rem; - font-weight: 500; - cursor: pointer; - transition: background 0.2s ease, transform 0.1s ease; -} - -.ontology-viewer__uml-warning-button:hover { - background: var(--primary-dark, #1565c0); -} - -.ontology-viewer__uml-warning-button:active { - transform: scale(0.98); -} - /* Responsive Design */ @media (max-width: 1024px) { .ontology-viewer-page { diff --git a/frontend/src/pages/OntologyViewerPage.tsx b/frontend/src/pages/OntologyViewerPage.tsx index 9a7e5d74b1..dce4c2ac60 100644 --- a/frontend/src/pages/OntologyViewerPage.tsx +++ b/frontend/src/pages/OntologyViewerPage.tsx @@ -47,16 +47,6 @@ import { UMLVisualization, type UMLDiagram, type UMLNode, type UMLLink } from '. import './OntologyViewerPage.css'; import '../styles/collapsible.css'; -/** - * Performance thresholds for UML diagram rendering. - * Large ontologies (Schema.org, CIDOC-CRM, RiC-O) can freeze the browser. - * Show warning and require user confirmation before rendering large diagrams. - */ -const UML_LARGE_DIAGRAM_THRESHOLD = { - nodes: 50, // Number of classes - links: 100, // Number of relationships -}; - /** * Normalize URI for comparison by handling http vs https variants. * Schema.org in particular uses https internally but is often referenced via http. @@ -371,12 +361,7 @@ const TEXT = { collapseSidebar: { nl: 'Zijbalk inklappen', en: 'Collapse sidebar' }, expandSidebar: { nl: 'Zijbalk uitklappen', en: 'Expand sidebar' }, - // UML diagram performance warnings - largeDiagramDetected: { nl: 'Grote Ontologie Gedetecteerd', en: 'Large Ontology Detected' }, - largeDiagramStats: { nl: 'klassen en', en: 'classes and' }, - largeDiagramRelationships: { nl: 'relaties', en: 'relationships' }, - largeDiagramWarning: { nl: 'Het renderen kan enkele seconden duren en kan uw browser vertragen.', en: 'Rendering may take several seconds and could slow your browser.' }, - loadDiagramAnyway: { nl: 'Toch Laden', en: 'Load Diagram Anyway' }, + // UML diagram umlLoading: { nl: 'UML-diagram genereren...', en: 'Generating UML diagram...' }, }; @@ -753,9 +738,6 @@ const OntologyViewerPage: React.FC = () => { // State for sidebar collapse const [sidebarCollapsed, setSidebarCollapsed] = useState(false); - // State for UML large diagram confirmation (performance optimization) - const [umlLoadConfirmed, setUmlLoadConfirmed] = useState(false); - // Track if we need to scroll after loading completes const pendingScrollRef = useRef(null); @@ -825,10 +807,6 @@ const OntologyViewerPage: React.FC = () => { useEffect(() => { if (!selectedOntology) return; - // Reset UML load confirmation when ontology changes - // (user must re-confirm for each large ontology) - setUmlLoadConfirmed(false); - const loadSelectedOntology = async () => { setIsLoading(true); setError(null); @@ -1540,10 +1518,6 @@ const OntologyViewerPage: React.FC = () => { /** * Render UML diagram view of the ontology - * - * Performance optimization: Large ontologies (Schema.org, CIDOC-CRM, RiC-O) - * can freeze the browser. We show a warning and require user confirmation - * before rendering large diagrams. */ const renderUMLView = () => { if (!ontology || !umlDiagram) return null; @@ -1557,31 +1531,6 @@ const OntologyViewerPage: React.FC = () => { ); } - // Check if this is a large diagram that could freeze the browser - const isLargeDiagram = - umlDiagram.nodes.length > UML_LARGE_DIAGRAM_THRESHOLD.nodes || - umlDiagram.links.length > UML_LARGE_DIAGRAM_THRESHOLD.links; - - // Show warning for large diagrams - require user confirmation - if (isLargeDiagram && !umlLoadConfirmed) { - return ( -
-
⚠️
-

{t('largeDiagramDetected')}

-

- {umlDiagram.nodes.length} {t('largeDiagramStats')} {umlDiagram.links.length} {t('largeDiagramRelationships')} -

-

{t('largeDiagramWarning')}

- -
- ); - } - return (