glam/frontend/src/App.tsx
kempersc 1fb924c412 feat: add ontology mappings to LinkML schema and enhance entity resolution
Schema enhancements (443 files):
- Add class_uri with proper ontology references (schema:, prov:, skos:, rico:)
- Add close_mappings, related_mappings per Rule 50 convention
- Replace stub hc: slot_uri with standard predicates (dcterms:identifier, skos:prefLabel)
- Improve descriptions with ontology mapping rationale
- Add prefixes blocks to all schema modules

Entity Resolution improvements:
- Add entity_resolution module with email semantics parsing
- Enhance build_entity_resolution.py with email-based matching signals
- Extend Entity Review API with filtering by signal types and count
- Add candidates caching and indexing for performance
- Add ReviewLoginPage component

New rules and documentation:
- Add Rule 51: No Hallucinated Ontology References
- Add .opencode/rules/no-hallucinated-ontology-references.md
- Add .opencode/rules/slot-ontology-mapping-reference.md
- Add adms.ttl and dqv.ttl ontology files

Frontend ontology support:
- Add RiC-O_1-1.rdf and schemaorg.owl to public/ontology
2026-01-13 13:51:02 +01:00

268 lines
8.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Main Application Component with Routing and Authentication
*
* © 2025 Netwerk Digitaal Erfgoed & TextPast. All rights reserved.
*/
import React, { Suspense, useEffect, useState, useCallback } from 'react';
import {
createBrowserRouter,
RouterProvider,
Navigate,
} from 'react-router-dom';
import { AuthProvider } from './contexts/AuthContext';
import { LanguageProvider } from './contexts/LanguageContext';
import { UIStateProvider } from './contexts/UIStateContext';
import { ProtectedRoute } from './components/ProtectedRoute';
import { Layout } from './components/layout/Layout';
import { preloadInstitutions } from './hooks/useGeoApiInstitutions';
import {
lazyWithRetry,
RouterErrorBoundary,
LazyLoadErrorBoundary
} from './components/common/LazyLoadError';
import { startVersionCheck } from './lib/version-check';
import { RefreshCw } from 'lucide-react';
// Eagerly loaded pages (small, frequently accessed)
import { LoginPage } from './pages/LoginPage';
import { Settings } from './pages/Settings';
import ProjectPlanPage from './pages/ProjectPlanPage';
// Lazy loaded pages with automatic retry on chunk load failures
// These use lazyWithRetry() to handle deployment-related chunk errors gracefully
// Map page - imports maplibre-gl (~1MB)
const NDEMapPage = lazyWithRetry(() => import('./pages/NDEMapPageMapLibre'));
// Visualize page - imports mermaid, d3, elkjs (~1.5MB combined)
const Visualize = lazyWithRetry(() => import('./pages/Visualize').then(m => ({ default: m.Visualize })));
// LinkML viewer - large schema parsing
const LinkMLViewerPage = lazyWithRetry(() => import('./pages/LinkMLViewerPage'));
// Ontology viewer - imports visualization libraries
const OntologyViewerPage = lazyWithRetry(() => import('./pages/OntologyViewerPage'));
// Data mappings page - custodian data mappings explorer
const DataMapPage = lazyWithRetry(() => import('./pages/DataMapPage'));
// Query builder - medium complexity
const QueryBuilderPage = lazyWithRetry(() => import('./pages/QueryBuilderPage'));
// Database page
const Database = lazyWithRetry(() => import('./pages/Database').then(m => ({ default: m.Database })));
// Stats page
const NDEStatsPage = lazyWithRetry(() => import('./pages/NDEStatsPage'));
// Conversation page
const ConversationPage = lazyWithRetry(() => import('./pages/ConversationPage'));
// Institution browser page
const InstitutionBrowserPage = lazyWithRetry(() => import('./pages/InstitutionBrowserPage'));
// Entity resolution review page
const EntityReviewPage = lazyWithRetry(() => import('./pages/EntityReviewPage'));
// Review login page (separate auth from main app)
const ReviewLoginPage = lazyWithRetry(() => import('./pages/ReviewLoginPage'));
import './App.css';
// Loading fallback component for lazy-loaded pages
const PageLoader = () => (
<div className="flex items-center justify-center h-64">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-4"></div>
<p className="text-gray-600 dark:text-gray-400">Loading...</p>
</div>
</div>
);
// Wrap lazy components with Suspense and error boundary for chunk load errors
const withSuspense = (Component: React.LazyExoticComponent<React.ComponentType>) => (
<LazyLoadErrorBoundary>
<Suspense fallback={<PageLoader />}>
<Component />
</Suspense>
</LazyLoadErrorBoundary>
);
// Create router configuration with protected and public routes
// Documentation routes (linkml, datamap, ontology, browse) are PUBLIC for embedding
// Other pages require authentication via ProtectedRoute
const router = createBrowserRouter([
{
path: '/login',
element: <LoginPage />,
errorElement: <RouterErrorBoundary />,
},
{
// Dedicated login for review page (separate credentials)
path: '/review-login',
element: withSuspense(ReviewLoginPage),
errorElement: <RouterErrorBoundary />,
},
// ========================================================================
// PUBLIC ROUTES - No authentication required
// These can be embedded in iframes from other domains (e.g., archief.support)
// ========================================================================
{
path: '/linkml',
element: <Layout />,
errorElement: <RouterErrorBoundary />,
children: [
{
index: true,
element: withSuspense(LinkMLViewerPage),
},
],
},
{
path: '/datamap',
element: <Layout />,
errorElement: <RouterErrorBoundary />,
children: [
{
index: true,
element: withSuspense(DataMapPage),
},
],
},
{
path: '/ontology',
element: <Layout />,
errorElement: <RouterErrorBoundary />,
children: [
{
index: true,
element: withSuspense(OntologyViewerPage),
},
],
},
{
path: '/browse',
element: <Layout />,
errorElement: <RouterErrorBoundary />,
children: [
{
index: true,
element: withSuspense(InstitutionBrowserPage),
},
],
},
// ========================================================================
// PROTECTED ROUTES - Authentication required
// ========================================================================
{
path: '/',
element: (
<ProtectedRoute>
<Layout />
</ProtectedRoute>
),
errorElement: <RouterErrorBoundary />,
children: [
{
// Home page redirects to LinkML viewer
index: true,
element: <Navigate to="/linkml" replace />,
},
{
path: 'visualize',
element: withSuspense(Visualize),
},
{
path: 'database',
element: withSuspense(Database),
},
{
path: 'settings',
element: <Settings />,
},
{
// Roadmap page (formerly Home/Project Plan)
path: 'roadmap',
element: <ProjectPlanPage />,
},
{
path: 'query-builder',
element: withSuspense(QueryBuilderPage),
},
{
// Redirect old UML viewer route to unified visualize page
path: 'uml-viewer',
element: <Navigate to="/visualize" replace />,
},
{
path: 'map',
element: withSuspense(NDEMapPage),
},
{
path: 'stats',
element: withSuspense(NDEStatsPage),
},
{
path: 'conversation',
element: withSuspense(ConversationPage),
},
{
path: 'review',
element: withSuspense(EntityReviewPage),
},
],
},
]);
function App() {
const [showUpdateBanner, setShowUpdateBanner] = useState(false);
// Preload institution data on app initialization
// This starts the fetch early so data is ready when user navigates to Map or Browse
useEffect(() => {
// Small delay to let the app initialize first
const timer = setTimeout(() => {
preloadInstitutions();
}, 500);
return () => clearTimeout(timer);
}, []);
// Check for new versions periodically
useEffect(() => {
const cleanup = startVersionCheck(() => {
setShowUpdateBanner(true);
});
return cleanup;
}, []);
const handleRefresh = useCallback(() => {
// Clear caches and reload
if ('caches' in window) {
caches.keys().then(names => {
names.forEach(name => caches.delete(name));
});
}
window.location.reload();
}, []);
return (
<LanguageProvider>
<AuthProvider>
<UIStateProvider>
{/* New version available banner */}
{showUpdateBanner && (
<div className="fixed top-0 left-0 right-0 z-[9999] bg-blue-600 text-white px-4 py-2 text-center text-sm flex items-center justify-center gap-3">
<span>A new version is available.</span>
<button
onClick={handleRefresh}
className="inline-flex items-center gap-1.5 px-3 py-1 bg-white text-blue-600 rounded font-medium hover:bg-blue-50 transition-colors"
>
<RefreshCw className="w-3.5 h-3.5" />
Refresh
</button>
<button
onClick={() => setShowUpdateBanner(false)}
className="text-white/80 hover:text-white ml-2"
aria-label="Dismiss"
>
×
</button>
</div>
)}
<RouterProvider router={router} />
</UIStateProvider>
</AuthProvider>
</LanguageProvider>
);
}
export default App;