The frontend is a React 18 application built with TypeScript and Vite. It provides an interactive UI for managing photobooks, viewing images, exploring the Knowledge Graph, and managing person identities.
frontend/
├── index.html # HTML entry point
├── package.json # Dependencies and scripts
├── vite.config.ts # Vite configuration
├── tsconfig.json # TypeScript configuration
├── tailwind.config.js # Tailwind CSS configuration
├── postcss.config.js # PostCSS configuration
│
└── src/
├── main.tsx # React entry point
├── App.tsx # Main app with routing
├── index.css # Global styles and Tailwind imports
│
├── pages/
│ ├── LoginPage.tsx # Authentication
│ ├── RegisterPage.tsx # User registration
│ ├── DashboardPage.tsx # Main dashboard (images/photobooks)
│ ├── PersonsPage.tsx # Person management
│ ├── ObjectsPage.tsx # Object browser
│ ├── JobsDashboard.tsx # Background job monitoring
│ └── KGVisualizationPage.tsx # Knowledge Graph explorer
│
├── components/
│ ├── features/ # Domain-specific components
│ │ ├── ImageUploader.tsx # Drag-drop file upload
│ │ ├── ImageGallery.tsx # Image grid with filters
│ │ ├── ImageDetailModal.tsx # Full-size image viewer
│ │ ├── PhotobookCard.tsx # Photobook preview card
│ │ ├── CreatePhotobookModal.tsx # Photobook creation
│ │ ├── FaceClustersPanel.tsx # Person face clusters
│ │ └── ...
│ │
│ ├── ui/ # Reusable UI primitives
│ │ ├── Button.tsx
│ │ ├── Card.tsx
│ │ ├── Spinner.tsx
│ │ ├── Modal.tsx
│ │ └── ...
│ │
│ ├── LoadingScreen.tsx # Full-page loading state
│ └── AuthLayout.tsx # Auth page layout
│
├── context/
│ ├── AuthContext.tsx # Auth state provider
│ └── useAuth.ts # Auth hook
│
├── hooks/
│ ├── usePhotobooks.ts # Photobook CRUD hooks
│ ├── useImages.ts # Image query hooks
│ ├── usePersons.ts # Person/face hooks
│ └── ...
│
├── lib/
│ └── api/
│ ├── index.ts # API exports
│ ├── client.ts # Fetch wrapper with auth
│ ├── types.ts # TypeScript interfaces
│ ├── auth.ts # Auth API calls
│ ├── images.ts # Image API calls
│ ├── photobooks.ts # Photobook API calls
│ ├── persons.ts # Person API calls
│ └── kg.ts # Knowledge Graph API
│
└── test/ # Test utilities
Main landing page with two tabs:
- Photobooks: Grid of photobook cards with creation modal
- Images: Image gallery with upload, face filtering, and detail views
Features:
- Face cluster panel for filtering by person
- "Show All Faces" toggle to overlay face bounding boxes
- Image detail modal with full metadata and claims
Person identity management:
- List of all identified persons with face thumbnails
- Person details with all associated images
- Merge persons capability
- Rename person
- Unidentified faces queue for assignment
Browse detected objects across all images:
- Filter by object type
- View object instances with source images
- Click to see object in context
Interactive Knowledge Graph explorer using Sigma.js:
- Node filtering by type (IMAGE, PERSON, DETECTION, etc.)
- Edge filtering by claim status
- Confidence threshold slider
- Search nodes by label
- Click nodes/edges to view details
- ForceAtlas2 layout algorithm
Background job monitoring:
- Pending jobs queue
- Recent completed jobs
- Job status and results
- Cancel pending jobs
Server state is managed with React Query for caching, refetching, and optimistic updates:
// hooks/useImages.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { getImages, uploadImage, deleteImage } from '../lib/api/images'
export function useImages() {
return useQuery({
queryKey: ['images'],
queryFn: getImages,
})
}
export function useUploadImage() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: uploadImage,
onSuccess: () => {
queryClient.invalidateQueries(['images'])
},
})
}Authentication state via React Context:
// context/AuthContext.tsx
const AuthContext = createContext<AuthContextValue>(...)
export function AuthProvider({ children }) {
const [user, setUser] = useState<User | null>(null)
const [isLoading, setIsLoading] = useState(true)
// Check stored token on mount
useEffect(() => {
const token = localStorage.getItem('access_token')
if (token) {
fetchCurrentUser().then(setUser).finally(() => setIsLoading(false))
}
}, [])
return (
<AuthContext.Provider value={{ user, isAuthenticated: !!user, login, logout, isLoading }}>
{children}
</AuthContext.Provider>
)
}Fetch wrapper with auth headers and error handling:
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8000/api/v1'
export async function apiClient<T>(
path: string,
options: RequestInit = {}
): Promise<T> {
const token = localStorage.getItem('access_token')
const response = await fetch(`${API_BASE}${path}`, {
...options,
headers: {
'Content-Type': 'application/json',
...(token && { Authorization: `Bearer ${token}` }),
...options.headers,
},
})
if (!response.ok) {
throw new ApiError(response.status, await response.text())
}
return response.json()
}export interface Image {
id: string
filename: string
original_name: string
file_path: string
thumbnail_path: string | null
width: number
height: number
created_at: string
properties: ImageProperties
}
export interface Person {
id: string
name: string
face_count: number
image_count: number
representative_face: FaceData | null
}
export interface GraphNode {
id: string
type: KGNodeType
label: string
properties: Record<string, unknown>
created_at: string
}
export interface GraphEdge {
id: string
source: string
target: string
predicate: string
confidence: number
status: KGClaimStatus
source_type: string
literal_value: string | null
}Button with variants:
<Button variant="primary" leftIcon={<Plus />}>Create</Button>
<Button variant="secondary" onClick={handleCancel}>Cancel</Button>
<Button variant="danger" isLoading={isDeleting}>Delete</Button>Card with interactive state:
<Card variant="interactive" onClick={handleClick}>
<CardHeader>Title</CardHeader>
<CardBody>Content</CardBody>
</Card>Modal with animated backdrop:
<Modal isOpen={isOpen} onClose={handleClose} title="Edit Person">
<form onSubmit={handleSubmit}>
{/* form fields */}
</form>
</Modal>Page transitions and micro-interactions:
<AnimatePresence mode="wait">
<motion.div
key={activeTab}
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{ duration: 0.2 }}
>
{/* tab content */}
</motion.div>
</AnimatePresence>Built with Sigma.js and graphology:
import { SigmaContainer, useLoadGraph } from '@react-sigma/core'
import Graph from 'graphology'
import forceAtlas2 from 'graphology-layout-forceatlas2'
function GraphLoader({ nodes, edges }) {
const loadGraph = useLoadGraph()
useEffect(() => {
const graph = new Graph()
// Add nodes with styling
nodes.forEach(node => {
graph.addNode(node.id, {
label: node.label,
size: 8,
color: getNodeColor(node.type),
x: Math.random() * 100,
y: Math.random() * 100,
})
})
// Add edges
edges.forEach(edge => {
graph.addEdge(edge.source, edge.target, {
label: edge.predicate,
size: edge.confidence * 2,
})
})
// Apply force-directed layout
forceAtlas2.assign(graph, { iterations: 100 })
loadGraph(graph)
}, [nodes, edges])
return null
}Utility-first styling with custom theme:
<div className="min-h-screen bg-surface-50">
<header className="bg-white border-b border-stone-100">
<div className="max-w-7xl mx-auto px-6 py-4">
<h1 className="text-2xl font-bold text-stone-900">Title</h1>
</div>
</header>
</div>module.exports = {
theme: {
extend: {
colors: {
accent: {
50: '#fef2f2',
// ... color scale
600: '#dc2626',
},
surface: {
50: '#fafafa',
// ... gray scale
},
},
},
},
}cd frontend
npm install
npm run dev # Start dev server on port 5173npm run build # Output to dist/
npm run preview # Preview production buildCreate .env.local:
VITE_API_URL=http://localhost:8000/api/v1- Create page component in
src/pages/MyPage.tsx - Add route in
src/App.tsx:<Route path="/mypage" element={ <ProtectedRoute> <MyPage /> </ProtectedRoute> } />
- Add navigation link in header/sidebar
- Add types in
src/lib/api/types.ts - Create API function in
src/lib/api/myfeature.ts - Export from
src/lib/api/index.ts - Create React Query hook in
src/hooks/useMyFeature.ts
npm test # Run tests
npm run test:ui # Run with UI
npm run test:cov # With coverageTest utilities in src/test/:
renderWithProviders()- Wrap with QueryClient and RoutercreateMockImage()- Generate test image datamockApiResponse()- Mock fetch responses