Fixed initial load being slow
This commit is contained in:
@@ -54,7 +54,14 @@
|
|||||||
"Bash(npx eslint:*)",
|
"Bash(npx eslint:*)",
|
||||||
"Bash(git checkout:*)",
|
"Bash(git checkout:*)",
|
||||||
"mcp__plugin_playwright_playwright__browser_hover",
|
"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
@@ -4,11 +4,13 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<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>
|
<title>CVMIS: CHARLWOOD, A.</title>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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="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'">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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>
|
<script defer src="https://analytics.charlwood.xyz/script.js" data-website-id="075e79d5-433a-4192-91c0-0b5b9c4334ab"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
Binary file not shown.
+3
-4
@@ -56,10 +56,9 @@ function App() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initModel()
|
if (phase === 'login' || phase === 'pmr') {
|
||||||
}, [])
|
initModel()
|
||||||
|
}
|
||||||
useEffect(() => {
|
|
||||||
if (phase === 'pmr') {
|
if (phase === 'pmr') {
|
||||||
sessionStorage.setItem('portfolio-visited', String(Date.now()))
|
sessionStorage.setItem('portfolio-visited', String(Date.now()))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { motion } from 'framer-motion'
|
||||||
import Sidebar from './Sidebar'
|
import Sidebar from './Sidebar'
|
||||||
import { MobileBottomNav } from './MobileBottomNav'
|
import { MobileBottomNav } from './MobileBottomNav'
|
||||||
import { CommandPalette } from './CommandPalette'
|
|
||||||
import { DetailPanel } from './DetailPanel'
|
|
||||||
import { PatientSummaryTile } from './tiles/PatientSummaryTile'
|
import { PatientSummaryTile } from './tiles/PatientSummaryTile'
|
||||||
import { ParentSection } from './ParentSection'
|
import { ParentSection } from './ParentSection'
|
||||||
import CareerConstellation from './constellation/CareerConstellation'
|
|
||||||
import { TimelineInterventionsSubsection } from './TimelineInterventionsSubsection'
|
import { TimelineInterventionsSubsection } from './TimelineInterventionsSubsection'
|
||||||
import { RepeatMedicationsSubsection } from './RepeatMedicationsSubsection'
|
|
||||||
import { LastConsultationCard } from './LastConsultationCard'
|
import { LastConsultationCard } from './LastConsultationCard'
|
||||||
import { ChatWidget } from './ChatWidget'
|
|
||||||
import { MobileOverviewHeader } from './MobileOverviewHeader'
|
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 { useActiveSection } from '@/hooks/useActiveSection'
|
||||||
import { useIsMobileNav } from '@/hooks/useIsMobileNav'
|
import { useIsMobileNav } from '@/hooks/useIsMobileNav'
|
||||||
import { useDetailPanel } from '@/contexts/DetailPanelContext'
|
import { useDetailPanel } from '@/contexts/DetailPanelContext'
|
||||||
@@ -329,22 +330,26 @@ export function DashboardLayout() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div ref={constellationWrapperRef} className="pathway-graph-sticky">
|
<div ref={constellationWrapperRef} className="pathway-graph-sticky">
|
||||||
<CareerConstellation
|
<Suspense fallback={null}>
|
||||||
onRoleClick={handleRoleClick}
|
<CareerConstellation
|
||||||
onSkillClick={handleSkillClick}
|
onRoleClick={handleRoleClick}
|
||||||
onNodeHover={handleNodeHover}
|
onSkillClick={handleSkillClick}
|
||||||
highlightedNodeId={highlightedNodeId}
|
onNodeHover={handleNodeHover}
|
||||||
containerHeight={chronologyHeight}
|
highlightedNodeId={highlightedNodeId}
|
||||||
animationReady={constellationReady}
|
containerHeight={chronologyHeight}
|
||||||
globalFocusActive={globalFocusId !== null}
|
animationReady={constellationReady}
|
||||||
/>
|
globalFocusActive={globalFocusId !== null}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div data-tile-id="section-skills" style={{ marginTop: '22px' }}>
|
<div data-tile-id="section-skills" style={{ marginTop: '22px' }}>
|
||||||
<RepeatMedicationsSubsection onNodeHighlight={handleNodeHighlight} focusRelatedIds={focusRelatedIds} />
|
<Suspense fallback={null}>
|
||||||
|
<RepeatMedicationsSubsection onNodeHighlight={handleNodeHighlight} focusRelatedIds={focusRelatedIds} />
|
||||||
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
</ParentSection>
|
</ParentSection>
|
||||||
</div>
|
</div>
|
||||||
@@ -352,17 +357,23 @@ export function DashboardLayout() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Command palette overlay */}
|
{/* Command palette overlay */}
|
||||||
<CommandPalette
|
<Suspense fallback={null}>
|
||||||
isOpen={commandPaletteOpen}
|
<CommandPalette
|
||||||
onClose={handlePaletteClose}
|
isOpen={commandPaletteOpen}
|
||||||
onAction={handlePaletteAction}
|
onClose={handlePaletteClose}
|
||||||
/>
|
onAction={handlePaletteAction}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
|
|
||||||
{/* Detail panel */}
|
{/* Detail panel */}
|
||||||
<DetailPanel />
|
<Suspense fallback={null}>
|
||||||
|
<DetailPanel />
|
||||||
|
</Suspense>
|
||||||
|
|
||||||
{/* Floating chat widget */}
|
{/* Floating chat widget */}
|
||||||
<ChatWidget onAction={handlePaletteAction} />
|
<Suspense fallback={null}>
|
||||||
|
<ChatWidget onAction={handlePaletteAction} />
|
||||||
|
</Suspense>
|
||||||
|
|
||||||
{/* Mobile bottom navigation */}
|
{/* Mobile bottom navigation */}
|
||||||
<MobileBottomNav
|
<MobileBottomNav
|
||||||
|
|||||||
@@ -3,15 +3,6 @@
|
|||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
/* Premium UI fonts — Elvaro Grotesque (primary) */
|
/* 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-face {
|
||||||
font-family: 'Elvaro Grotesque';
|
font-family: 'Elvaro Grotesque';
|
||||||
src: url('/Fonts/Elvaro Grotesque Sans Family/WOFF/TBJElvaro-Regular.woff2') format('woff2'),
|
src: url('/Fonts/Elvaro Grotesque Sans Family/WOFF/TBJElvaro-Regular.woff2') format('woff2'),
|
||||||
@@ -48,34 +39,7 @@
|
|||||||
font-display: swap;
|
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 */
|
/* 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-face {
|
||||||
font-family: 'Interval Mono';
|
font-family: 'Interval Mono';
|
||||||
src: url('/Fonts/IntervalMono/WOFF/TBJInterval-Regular.woff2') format('woff2'),
|
src: url('/Fonts/IntervalMono/WOFF/TBJInterval-Regular.woff2') format('woff2'),
|
||||||
@@ -85,15 +49,6 @@
|
|||||||
font-display: swap;
|
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) */
|
/* Premium UI fonts — Blumir (alternative) */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Blumir';
|
font-family: 'Blumir';
|
||||||
|
|||||||
@@ -1,18 +1,15 @@
|
|||||||
import { env, pipeline, type FeatureExtractionPipeline } from '@xenova/transformers'
|
let extractor: import('@xenova/transformers').FeatureExtractionPipeline | null = null
|
||||||
|
|
||||||
// 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 loading = false
|
let loading = false
|
||||||
|
|
||||||
export async function initModel(): Promise<void> {
|
export async function initModel(): Promise<void> {
|
||||||
if (extractor || loading) return
|
if (extractor || loading) return
|
||||||
loading = true
|
loading = true
|
||||||
try {
|
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 {
|
} catch {
|
||||||
// Silently swallow — model unavailable, semantic search won't activate
|
// Silently swallow — model unavailable, semantic search won't activate
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -9,6 +9,20 @@ export default defineConfig({
|
|||||||
'@': path.resolve(__dirname, './src'),
|
'@': 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: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': 'http://localhost:3000',
|
'/api': 'http://localhost:3000',
|
||||||
|
|||||||
Reference in New Issue
Block a user