Fixed initial load being slow

This commit is contained in:
2026-02-19 21:38:39 +00:00
parent edc1327987
commit e452b66a7f
8 changed files with 69 additions and 84 deletions
+8 -1
View File
@@ -54,7 +54,14 @@
"Bash(npx eslint:*)",
"Bash(git checkout:*)",
"mcp__plugin_playwright_playwright__browser_hover",
"mcp__plugin_playwright_playwright__browser_run_code"
"mcp__plugin_playwright_playwright__browser_run_code",
"WebFetch(domain:pagespeed.web.dev)",
"WebFetch(domain:www.googleapis.com)",
"Bash(find:*)",
"Bash(du -sh:*)",
"Bash(sudo apt-get install:*)",
"Bash(pdftotext:*)",
"Bash(pdftoppm:*)"
]
}
}
+4 -2
View File
@@ -4,11 +4,13 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Andy Charlwood — Deputy Head of Population Health & Data Analysis. Interactive CV and portfolio showcasing pharmacist expertise, data analytics, and population health management.">
<title>CVMIS: CHARLWOOD, A.</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;700&family=Plus+Jakarta+Sans:wght@400;500;600;700&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;700&family=Plus+Jakarta+Sans:wght@400;500;600;700&family=Inter:wght@400;500;600&display=swap" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;700&family=Plus+Jakarta+Sans:wght@400;500;600;700&family=Inter:wght@400;500;600&display=swap"></noscript>
<link rel="preload" href="/Fonts/Elvaro Grotesque Sans Family/WOFF/TBJElvaro-Regular.woff2" as="font" type="font/woff2" crossorigin>
<script defer src="https://analytics.charlwood.xyz/script.js" data-website-id="075e79d5-433a-4192-91c0-0b5b9c4334ab"></script>
</head>
<body>
BIN
View File
Binary file not shown.
+3 -4
View File
@@ -56,10 +56,9 @@ function App() {
})
useEffect(() => {
initModel()
}, [])
useEffect(() => {
if (phase === 'login' || phase === 'pmr') {
initModel()
}
if (phase === 'pmr') {
sessionStorage.setItem('portfolio-visited', String(Date.now()))
}
+34 -23
View File
@@ -1,17 +1,18 @@
import { useState, useEffect, useCallback, useRef, useMemo } from 'react'
import { useState, useEffect, useCallback, useRef, useMemo, lazy, Suspense } from 'react'
import { motion } from 'framer-motion'
import Sidebar from './Sidebar'
import { MobileBottomNav } from './MobileBottomNav'
import { CommandPalette } from './CommandPalette'
import { DetailPanel } from './DetailPanel'
import { PatientSummaryTile } from './tiles/PatientSummaryTile'
import { ParentSection } from './ParentSection'
import CareerConstellation from './constellation/CareerConstellation'
import { TimelineInterventionsSubsection } from './TimelineInterventionsSubsection'
import { RepeatMedicationsSubsection } from './RepeatMedicationsSubsection'
import { LastConsultationCard } from './LastConsultationCard'
import { ChatWidget } from './ChatWidget'
import { MobileOverviewHeader } from './MobileOverviewHeader'
const CommandPalette = lazy(() => import('./CommandPalette').then(m => ({ default: m.CommandPalette })))
const DetailPanel = lazy(() => import('./DetailPanel').then(m => ({ default: m.DetailPanel })))
const CareerConstellation = lazy(() => import('./constellation/CareerConstellation'))
const RepeatMedicationsSubsection = lazy(() => import('./RepeatMedicationsSubsection').then(m => ({ default: m.RepeatMedicationsSubsection })))
const ChatWidget = lazy(() => import('./ChatWidget').then(m => ({ default: m.ChatWidget })))
import { useActiveSection } from '@/hooks/useActiveSection'
import { useIsMobileNav } from '@/hooks/useIsMobileNav'
import { useDetailPanel } from '@/contexts/DetailPanelContext'
@@ -329,22 +330,26 @@ export function DashboardLayout() {
</div>
</div>
<div ref={constellationWrapperRef} className="pathway-graph-sticky">
<CareerConstellation
onRoleClick={handleRoleClick}
onSkillClick={handleSkillClick}
onNodeHover={handleNodeHover}
highlightedNodeId={highlightedNodeId}
containerHeight={chronologyHeight}
animationReady={constellationReady}
globalFocusActive={globalFocusId !== null}
/>
<Suspense fallback={null}>
<CareerConstellation
onRoleClick={handleRoleClick}
onSkillClick={handleSkillClick}
onNodeHover={handleNodeHover}
highlightedNodeId={highlightedNodeId}
containerHeight={chronologyHeight}
animationReady={constellationReady}
globalFocusActive={globalFocusId !== null}
/>
</Suspense>
</div>
</div>
<div data-tile-id="section-skills" style={{ marginTop: '22px' }}>
<RepeatMedicationsSubsection onNodeHighlight={handleNodeHighlight} focusRelatedIds={focusRelatedIds} />
<Suspense fallback={null}>
<RepeatMedicationsSubsection onNodeHighlight={handleNodeHighlight} focusRelatedIds={focusRelatedIds} />
</Suspense>
</div>
</ParentSection>
</div>
@@ -352,17 +357,23 @@ export function DashboardLayout() {
</div>
{/* Command palette overlay */}
<CommandPalette
isOpen={commandPaletteOpen}
onClose={handlePaletteClose}
onAction={handlePaletteAction}
/>
<Suspense fallback={null}>
<CommandPalette
isOpen={commandPaletteOpen}
onClose={handlePaletteClose}
onAction={handlePaletteAction}
/>
</Suspense>
{/* Detail panel */}
<DetailPanel />
<Suspense fallback={null}>
<DetailPanel />
</Suspense>
{/* Floating chat widget */}
<ChatWidget onAction={handlePaletteAction} />
<Suspense fallback={null}>
<ChatWidget onAction={handlePaletteAction} />
</Suspense>
{/* Mobile bottom navigation */}
<MobileBottomNav
-45
View File
@@ -3,15 +3,6 @@
@tailwind utilities;
/* Premium UI fonts — Elvaro Grotesque (primary) */
@font-face {
font-family: 'Elvaro Grotesque';
src: url('/Fonts/Elvaro Grotesque Sans Family/WOFF/TBJElvaro-Light.woff2') format('woff2'),
url('/Fonts/Elvaro Grotesque Sans Family/WOFF/TBJElvaro-Light.woff') format('woff');
font-weight: 300;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Elvaro Grotesque';
src: url('/Fonts/Elvaro Grotesque Sans Family/WOFF/TBJElvaro-Regular.woff2') format('woff2'),
@@ -48,34 +39,7 @@
font-display: swap;
}
@font-face {
font-family: 'Elvaro Grotesque';
src: url('/Fonts/Elvaro Grotesque Sans Family/WOFF/TBJElvaro-ExtraBold.woff2') format('woff2'),
url('/Fonts/Elvaro Grotesque Sans Family/WOFF/TBJElvaro-ExtraBold.woff') format('woff');
font-weight: 800;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Elvaro Grotesque';
src: url('/Fonts/Elvaro Grotesque Sans Family/WOFF/TBJElvaro-Black.woff2') format('woff2'),
url('/Fonts/Elvaro Grotesque Sans Family/WOFF/TBJElvaro-Black.woff') format('woff');
font-weight: 900;
font-style: normal;
font-display: swap;
}
/* Monospace — Interval Mono */
@font-face {
font-family: 'Interval Mono';
src: url('/Fonts/IntervalMono/WOFF/TBJInterval-Light.woff2') format('woff2'),
url('/Fonts/IntervalMono/WOFF/TBJInterval-Light.woff') format('woff');
font-weight: 300;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Interval Mono';
src: url('/Fonts/IntervalMono/WOFF/TBJInterval-Regular.woff2') format('woff2'),
@@ -85,15 +49,6 @@
font-display: swap;
}
@font-face {
font-family: 'Interval Mono';
src: url('/Fonts/IntervalMono/WOFF/TBJInterval-Bold.woff2') format('woff2'),
url('/Fonts/IntervalMono/WOFF/TBJInterval-Bold.woff') format('woff');
font-weight: 700;
font-style: normal;
font-display: swap;
}
/* Premium UI fonts — Blumir (alternative) */
@font-face {
font-family: 'Blumir';
+6 -9
View File
@@ -1,18 +1,15 @@
import { env, pipeline, type FeatureExtractionPipeline } from '@xenova/transformers'
// Serve model files from /models/ (Vite serves public/ at root)
env.localModelPath = '/models/'
env.allowRemoteModels = false
env.useBrowserCache = false
let extractor: FeatureExtractionPipeline | null = null
let extractor: import('@xenova/transformers').FeatureExtractionPipeline | null = null
let loading = false
export async function initModel(): Promise<void> {
if (extractor || loading) return
loading = true
try {
extractor = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2') as FeatureExtractionPipeline
const { env, pipeline } = await import('@xenova/transformers')
env.localModelPath = '/models/'
env.allowRemoteModels = false
env.useBrowserCache = false
extractor = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2') as import('@xenova/transformers').FeatureExtractionPipeline
} catch {
// Silently swallow — model unavailable, semantic search won't activate
} finally {
+14
View File
@@ -9,6 +9,20 @@ export default defineConfig({
'@': path.resolve(__dirname, './src'),
},
},
build: {
rollupOptions: {
output: {
manualChunks: {
'vendor-react': ['react', 'react-dom'],
'vendor-d3': ['d3'],
'vendor-motion': ['framer-motion'],
'vendor-search': ['fuse.js'],
'vendor-markdown': ['react-markdown'],
'vendor-carousel': ['embla-carousel-react', 'embla-carousel-autoplay'],
},
},
},
},
server: {
proxy: {
'/api': 'http://localhost:3000',