# AI Knowledge Map
A dotted AI orb pulses light down wires into five category cards; the active card fans rays onto scored results.
## 1. Setup
// npm install framer-motion @phosphor-icons/react
File: components-workspace/ai-knowledge-map/index.tsx · 'use client' · export default function AiKnowledgeMap(props: AiKnowledgeMapProps)
Requires Tailwind CSS v4 with a class-based dark variant (`@custom-variant dark (&:where(.dark, .dark *));`) and the Manrope font loaded by your app (the root falls back to the system sans without it).
```tsx
import { memo, useCallback, useEffect, useId, useMemo, useRef, useState, useSyncExternalStore } from 'react'
import type { CSSProperties, FocusEvent as ReactFocusEvent, KeyboardEvent as ReactKeyboardEvent } from 'react'
import { AnimatePresence, motion, useReducedMotion } from 'framer-motion'
import {
Buildings,
ChartLineUp,
WaveTriangle,
FileText,
Users,
ChartBar,
Link as LinkIcon,
CaretUp,
CaretDown,
type Icon,
} from '@phosphor-icons/react'
```
## 2. Constants
Everything above the component, verbatim and in source order, with comments removed. The helper components (OrbCanvas, ChainLayer, ChainCardFlash, Comet, QualityRing, NodeMeta, Pill, Meter, SoftGradient) are in block 4.
```tsx
export type KnowledgeMapPalette = {
ground: string
groundCenter: string
dominant: string
dominantBright: string
accent: string
accentBright: string
text: string
muted: string
hairline: string
cardFill: string
cardHoverFill: string
cardHoverBorder: string
cardActiveFrom: string
cardActiveMid: string
cardActiveTo: string
cardActiveBorder: string
cardActiveGlow: string
cardShadow: string
cardHoverShadow: string
iconTile: string
onAccent: string
connector: string
connectorActive: string
rayA: string
rayB: string
nodeA: string
nodeB: string
pillFill: string
pillBorder: string
coreCenter: string
coreOuter: string
coreDot: string
atmosphere: string
meterEmpty: string
scoreLow: string
scoreMid: string
scoreHigh: string
focusRing: string
}
type Pt = { x: number; y: number }
type Score = 0 | 1 | 2
export type KnowledgeMapNode = { title: string; gaps: number; score: Score; icon: Icon }
export type KnowledgeMapCategory = {
id: string
label: string
count: number
icon: Icon
nodes: KnowledgeMapNode[]
}
export const KNOWLEDGE_MAP_DARK: KnowledgeMapPalette = {
ground: '#08070C',
groundCenter: '#0F0D14',
dominant: '#8A8494',
dominantBright: '#F4F1F6',
accent: '#C8FF4D',
accentBright: '#C8FF4D',
text: '#F4F1F6',
muted: '#8A8494',
hairline: 'rgba(244,241,246,0.08)',
cardFill: '#131118',
cardHoverFill: '#1A1720',
cardHoverBorder: 'rgba(244,241,246,0.16)',
cardActiveFrom: 'rgba(200,255,77,0.08)',
cardActiveMid: '#16131C',
cardActiveTo: '#16131C',
cardActiveBorder: 'rgba(200,255,77,0.45)',
cardActiveGlow: 'inset 0 1px 0 rgba(244,241,246,0.08), 0 0 0 4px rgba(200,255,77,0.07), 0 4px 8px rgba(0,0,0,0.35), 0 16px 36px rgba(0,0,0,0.45)',
cardShadow: 'inset 0 1px 0 rgba(244,241,246,0.06), 0 1px 2px rgba(0,0,0,0.35), 0 6px 18px rgba(0,0,0,0.3)',
cardHoverShadow: 'inset 0 1px 0 rgba(244,241,246,0.08), 0 2px 4px rgba(0,0,0,0.4), 0 10px 26px rgba(0,0,0,0.38)',
iconTile: 'rgba(200,255,77,0.10)',
onAccent: '#08070C',
connector: 'rgba(244,241,246,0.08)',
connectorActive: 'rgba(244,241,246,0.45)',
rayA: 'rgba(244,241,246,0.45)',
rayB: 'rgba(200,255,77,0.45)',
nodeA: 'rgba(244,241,246,0.92)',
nodeB: 'rgba(200,255,77,0.92)',
pillFill: 'rgba(244,241,246,0.08)',
pillBorder: 'rgba(244,241,246,0.08)',
coreCenter: '#F4F1F6',
coreOuter: 'rgba(244,241,246,0.45)',
coreDot: '#F4F1F6',
atmosphere: 'rgba(244,241,246,0.08)',
meterEmpty: 'rgba(244,241,246,0.08)',
scoreLow: '#FF4A4A',
scoreMid: '#FFB020',
scoreHigh: '#C8FF4D',
focusRing: '#C8FF4D',
}
export const KNOWLEDGE_MAP_LIGHT: KnowledgeMapPalette = {
ground: '#F5F1F6',
groundCenter: '#FFFFFF',
dominant: '#6C6577',
dominantBright: '#16121A',
accent: '#5E7A12',
accentBright: '#5E7A12',
text: '#16121A',
muted: '#6C6577',
hairline: 'rgba(22,18,26,0.10)',
cardFill: '#FFFFFF',
cardHoverFill: '#FFFFFF',
cardHoverBorder: 'rgba(22,18,26,0.12)',
cardActiveFrom: 'rgba(94,122,18,0.09)',
cardActiveMid: '#FFFFFF',
cardActiveTo: '#FFFFFF',
cardActiveBorder: 'rgba(94,122,18,0.55)',
cardActiveGlow: 'inset 0 1px 0 #FFFFFF, 0 0 0 4px rgba(94,122,18,0.10), 0 4px 8px rgba(22,18,26,0.06), 0 16px 36px rgba(22,18,26,0.12)',
cardShadow: 'inset 0 1px 0 #FFFFFF, 0 1px 2px rgba(22,18,26,0.05), 0 6px 18px rgba(22,18,26,0.06)',
cardHoverShadow: 'inset 0 1px 0 #FFFFFF, 0 2px 4px rgba(22,18,26,0.07), 0 10px 26px rgba(22,18,26,0.10)',
iconTile: 'rgba(94,122,18,0.10)',
onAccent: '#FFFFFF',
connector: 'rgba(22,18,26,0.10)',
connectorActive: 'rgba(22,18,26,0.45)',
rayA: 'rgba(22,18,26,0.45)',
rayB: 'rgba(94,122,18,0.45)',
nodeA: 'rgba(22,18,26,0.92)',
nodeB: 'rgba(94,122,18,0.92)',
pillFill: 'rgba(22,18,26,0.10)',
pillBorder: 'rgba(22,18,26,0.10)',
coreCenter: '#FFFFFF',
coreOuter: 'rgba(22,18,26,0.10)',
coreDot: '#16121A',
atmosphere: 'rgba(22,18,26,0.10)',
meterEmpty: 'rgba(22,18,26,0.10)',
scoreLow: '#D92D20',
scoreMid: '#B25E02',
scoreHigh: '#5E7A12',
focusRing: '#5E7A12',
}
const NODE_ICONS: Icon[] = [FileText, Users, ChartBar, LinkIcon]
const SCORE_CYCLE: Score[] = [0, 2, 1, 2, 1, 2, 0, 1, 2, 2, 1, 0]
function makeNodes(titles: readonly string[]): KnowledgeMapNode[] {
return titles.map((title, i) => ({
title,
gaps: (i * 7 + 3) % 25,
score: SCORE_CYCLE[i % SCORE_CYCLE.length],
icon: NODE_ICONS[i % NODE_ICONS.length],
}))
}
const DOCUMENT_TITLES = [
'Master Services Agreement',
'Q3 Board Deck',
'Vendor Security Questionnaire',
'Annual Compliance Report',
'Product Roadmap Brief',
'Customer Onboarding Guide',
'Pricing Policy 2026',
'Incident Postmortem Log',
'Data Retention Policy',
'Sales Playbook',
'Employee Handbook',
'Partner Integration Spec',
'Quarterly Revenue Summary',
'Brand Guidelines',
] as const
const PEOPLE_TITLES = [
'Maya Lindqvist',
'Daniel Okafor',
'Priya Raman',
'Tomas Berger',
'Elena Moretti',
'Kenji Watanabe',
'Amara Nwosu',
'Lucas Ferreira',
'Sofia Kowalski',
'Noah Brennan',
'Leila Haddad',
'Mateo Alvarez',
'Hannah Schultz',
'Arjun Mehta',
'Chloe Dubois',
'Samuel Adeyemi',
'Ingrid Solberg',
'Rafael Costa',
'Yuki Tanaka',
'Olivia Grant',
] as const
const COMPANY_TITLES = [
'Northbridge Holdings Group',
'Meridian Data Services Ltd',
'Atlas Freight and Logistics',
'Continental Supply Partners',
'Harborview Asset Management',
'Sterling Payments Division',
'Lakeside Manufacturing Co',
'Ridgeway Insurance Brokers',
'Ashford Retail Collective',
'Vantage Point Advisory',
'Crescent Energy Trading',
'Pinnacle Health Networks',
'Redwood Property Trust',
'Solstice Media Holdings',
'Granite Peak Ventures',
'Fairmont Logistics Hub',
'Bluewater Marine Group',
'Copperline Industrial',
'Eastgate Distribution',
'Silverlake Analytics',
'Kestrel Aviation Services',
'Ironwood Capital Partners',
'Beacon Hill Consulting',
'Westfield Grain Exchange',
] as const
const METRIC_TITLES = [
'Average Resolution Latency',
'Monthly Recurring Retention',
'First Response Coverage',
'Pipeline Conversion Velocity',
'Order Fulfilment Accuracy',
'Support Deflection Rate',
'Asset Utilisation Index',
'Inventory Turnover Ratio',
'Net Expansion Momentum',
'Backlog Ageing Profile',
'Escalation Density Score',
'Onboarding Completion Rate',
'Forecast Deviation Margin',
'Service Uptime Continuity',
'Cost Per Resolved Case',
'Vendor Lead Time Spread',
'Quality Rejection Frequency',
'Renewal Intent Strength',
'Territory Coverage Balance',
'Capacity Headroom Estimate',
] as const
const ALERT_TITLES = [
'Unusual Access Pattern',
'Sustained Latency Drift',
'Duplicate Record Cluster',
'Sudden Volume Spike',
'Stalled Approval Queue',
'Repeated Retry Storm',
'Config Drift Detected',
'Expiring Credential Batch',
'Orphaned Dependency Chain',
'Unmapped Territory Gap',
'Silent Failure Streak',
'Seasonal Demand Inversion',
'Invoice Mismatch Surge',
'Dormant Account Wakeup',
] as const
export const KNOWLEDGE_MAP_CATEGORIES: KnowledgeMapCategory[] = [
{ id: 'documents', label: 'Documents', count: 14, icon: FileText, nodes: makeNodes(DOCUMENT_TITLES) },
{ id: 'people', label: 'People', count: 20, icon: Users, nodes: makeNodes(PEOPLE_TITLES) },
{ id: 'companies', label: 'Companies', count: 24, icon: Buildings, nodes: makeNodes(COMPANY_TITLES) },
{ id: 'metrics', label: 'Metrics', count: 20, icon: ChartLineUp, nodes: makeNodes(METRIC_TITLES) },
{ id: 'alerts', label: 'Alerts', count: 14, icon: WaveTriangle, nodes: makeNodes(ALERT_TITLES) },
]
const SCORE_WORDS = ['low', 'medium', 'high'] as const
type RailConfig = {
apex: number
bulge: number
bow: number
halfSpan: number
top: number
pitch: number
count: number
gapBefore: (k: number) => number
}
function railPoints(cfg: RailConfig): Pt[] {
const tAt = (y: number) => (cfg.halfSpan > 0 ? (y - cfg.bow) / cfg.halfSpan : 0)
const points: Pt[] = []
for (let k = 0; k < cfg.count; k++) {
const y = cfg.top + k * cfg.pitch + cfg.gapBefore(k)
const t = tAt(y)
points.push({ x: cfg.apex - cfg.bulge * t * t, y })
}
return points
}
function clipHeight(el: HTMLElement): number {
let node: HTMLElement | null = el.parentElement
while (node && node !== document.body) {
if (getComputedStyle(node).overflowY !== 'visible') return node.clientHeight
node = node.parentElement
}
return window.innerHeight
}
function rayPath(p0: Pt, p3: Pt, lead: number, leadK: number, arrive: number, arriveK: number): string {
const d = p3.y - p0.y
return `M ${p0.x} ${p0.y} C ${p0.x + lead} ${p0.y + leadK * d} ${p3.x - arrive} ${p3.y - arriveK * d} ${p3.x} ${p3.y}`
}
function compactRayPath(p0: Pt, p3: Pt, lead: number, arrive: number): string {
return `M ${p0.x} ${p0.y} C ${p0.x} ${p0.y + lead} ${p3.x} ${p3.y - arrive} ${p3.x} ${p3.y}`
}
function falloffFor(
y: number,
halfHeight: number,
outer: number,
mid: number,
band: number,
n: number,
minCount: number,
): number {
if (n < minCount || band <= 0 || halfHeight <= 0) return 1
const k = (Math.abs(y - halfHeight) / halfHeight - (1 - band)) / band
if (k <= 0) return 1
if (k >= 1) return outer
return k < 0.5 ? 1 + (mid - 1) * k * 2 : mid + (outer - mid) * (k - 0.5) * 2
}
function scoreColor(score: Score, p: KnowledgeMapPalette): string {
return score === 0 ? p.scoreLow : score === 1 ? p.scoreMid : p.scoreHigh
}
function highShare(nodes: KnowledgeMapNode[]): number {
return nodes.length ? Math.round((nodes.filter((n) => n.score === 2).length / nodes.length) * 100) : 0
}
const MAP_STYLES = `
.akm-card { background: var(--akm-bg); border: 1px solid var(--akm-border); box-shadow: var(--akm-shadow) }
.akm-arc { stroke: var(--akm-stroke); opacity: var(--akm-op) }
.akm-pct { color: var(--akm-ink) }
@media (hover: hover) {
.akm-card:not([aria-checked='true']):hover { background: var(--akm-hover-bg); border-color: var(--akm-hover-border); box-shadow: var(--akm-hover-shadow) }
.akm-card:hover .akm-arc { stroke: var(--akm-hot); opacity: 1 }
.akm-card:hover .akm-pct { color: var(--akm-hot-ink) }
}
@keyframes akm-dash { from { stroke-dashoffset: var(--akm-from) } to { stroke-dashoffset: var(--akm-to) } }
@keyframes akm-ring { 0% { transform: scale(0.25); opacity: 0 } 30% { opacity: var(--akm-a) } 100% { transform: scale(1); opacity: 0 } }
@keyframes akm-swell { 0%, 100% { opacity: 0 } 50% { opacity: var(--akm-a) } }
`
const cubic = (e: CubicEase) => `cubic-bezier(${e.join(',')})`
function useChainShot(bus: Set<() => void>, resetKey: string) {
const [shot, setShot] = useState<{ id: number; key: string } | null>(null)
const keyRef = useRef(resetKey)
useEffect(() => {
keyRef.current = resetKey
}, [resetKey])
useEffect(() => {
const fire = () => setShot((prev) => ({ id: (prev?.id ?? 0) + 1, key: keyRef.current }))
bus.add(fire)
return () => {
bus.delete(fire)
}
}, [bus])
return shot && shot.key === resetKey ? shot.id : 0
}
type ChainRay = { d: string; color: string; ring: string; x: number; y: number; delay: number }
export type CubicEase = readonly [number, number, number, number]
type IntroPlan = {
ease: CubicEase
core: number
caption: number
pill: number
connector: (i: number) => number
card: (i: number) => number
ray: (i: number) => number
dot: (i: number) => number
title: (i: number) => number
meta: (i: number) => number
}
export type KnowledgeMapLayout = 'fit' | 'follow' | 'bands'
const SOFT_STOPS: readonly [number, number][] = [
[0, 1],
[0.22, 0.7],
[0.44, 0.38],
[0.64, 0.16],
[0.82, 0.05],
[1, 0],
]
const TAU = Math.PI * 2
const ORB_REST_SWEEP = 0.5
const ORB_LAUNCH_LEAD = 0.35
const smoothstep = (v: number) => {
const c = Math.min(1, Math.max(0, v))
return c * c * (3 - 2 * c)
}
/**
* @typedef {object} AiKnowledgeMapProps
* @property {KnowledgeMapCategory[]} [categories=KNOWLEDGE_MAP_CATEGORIES] The category cards and the results each one fans out to.
* @property {number} [initialCategory=2] Index of the card that is active on mount.
* @property {(id: string, index: number) => void} [onCategoryChange] Called with the category id and index when the active card changes.
* @property {string} [title='Knowledge Index'] Heading above the orb.
* @property {number} [modelIndex=4] First number in the line under the heading, read as "Model 4 of 12".
* @property {number} [modelCount=12] Second number in that line.
* @property {number} [confidence=92] Percentage shown in the ring under the orb; its colour follows the score.
* @property {'fit' | 'follow' | 'bands'} [layout='follow'] How the result column sits: centred, following the active card, or grouped by score.
* @property {boolean} [pulse=true] Whether the orb sends light down the active wire to the results on every pass.
* @property {Partial<KnowledgeMapPalette>} [paletteDark] Colour overrides for the dark theme, merged over KNOWLEDGE_MAP_DARK.
* @property {Partial<KnowledgeMapPalette>} [paletteLight] Colour overrides for the light theme, merged over KNOWLEDGE_MAP_LIGHT.
* @property {string} [className] Extra classes merged onto the root element.
*/
export type AiKnowledgeMapProps = {
categories?: KnowledgeMapCategory[]
initialCategory?: number
onCategoryChange?: (id: string, index: number) => void
title?: string
modelIndex?: number
modelCount?: number
confidence?: number
layout?: KnowledgeMapLayout
pulse?: boolean
paletteDark?: Partial<KnowledgeMapPalette>
paletteLight?: Partial<KnowledgeMapPalette>
className?: string
}
const MAP = {
captionPrefix: 'Model',
captionMiddle: 'of',
coreStatLabel: 'Confidence',
nodePillLabel: 'GAPS',
stageWidth: 1440,
stageHeight: 900,
stageHeightMin: 660,
coreCx: 245,
coreCy: 0,
coreRadius: 64,
atmosphereScale: 3,
atmosphereFlatten: 0.88,
atmosphereAlpha: 1,
orbRings: 16,
orbEquatorDots: 40,
orbTilt: 0.32,
orbDotRadius: 1.4,
orbBackAlpha: 0.05,
orbRestAlpha: 0.34,
orbSweepMs: 3200,
orbSweepWidth: 0.3,
orbSweepBoost: 0.35,
orbSweepAlpha: 0.6,
orbSpinMs: 60000,
captionGap: 32,
statGap: 28,
cardLeftX: 448,
cardWidth: 264,
cardHeight: 96,
cardGap: 14,
cardRadius: 20,
cardPadding: 14,
connectorOriginX: 309,
connectorBundleSpread: 6,
connectorLeadX: 70,
connectorArriveX: 60,
connectorWidth: 1,
arcApexX: 1000,
arcBulge: 96,
arcSpan: 0,
arcInsetY: 28,
maxRowPitch: 104,
minRowPitch: 24,
bandGap: 22,
rowFontSize: 16,
denseFontSize: 14,
tightFontSize: 13,
densePitch: 34,
tightPitch: 28,
chevronSize: 12,
continuationOpacity: 0.55,
pageStep: 0,
pageMs: 260,
rayLeadX: 78,
rayLeadK: 0.16,
rayArriveX: 150,
rayArriveK: 0.1,
rayWidth: 1,
rayRaisedWidth: 2,
nodeDotRadius: 2.5,
nodeDotRaisedScale: 1.5,
nodeHaloScale: 2.8,
nodeHaloAlpha: 0.2,
labelOffsetX: 14,
metaColumnInset: 32,
titleMetaGap: 12,
metaIconSize: 14,
maxNodesLg: 0,
maxNodesMd: 0,
maxNodesSm: 7,
falloffOuter: 0.2,
falloffMid: 0.6,
falloffBand: 0.42,
falloffMinCount: 10,
dimFactor: 0.45,
metaOpacity: 0.7,
revealFullTitle: true,
compactBreakpoint: 900,
midBreakpoint: 1200,
compactStageWidth: 340,
compactStageHeight: 680,
compactCoreCx: 170,
compactCoreCy: 130,
compactCoreRadius: 36,
compactChipWidth: 148,
compactChipHeight: 72,
compactChipGap: 8,
compactChipRowY: 252,
compactSpineX: 30,
compactSpineBulge: 0,
compactListTop: 362,
compactRowHeight: 32,
compactRayLeadY: 40,
compactRayArriveY: 30,
compactLabelOffsetX: 16,
compactScaleMin: 0.8,
compactScaleMax: 1.5,
introCopyScale: 0.94,
metaDelayMs: 90,
ignitionEase: [0.22, 1, 0.36, 1] as CubicEase,
ignitionCaptionAt: 0.4,
ignitionPillAt: 0.6,
igniteMs: 1100,
connectorStartMs: 520,
connectorStaggerMs: 90,
connectorTravelMs: 620,
pulseLength: 0.04,
cardLightMs: 260,
cardLiftPx: 4,
cardSpringStiffness: 320,
cardSpringDamping: 26,
chargeMs: 480,
fanStartMs: 1420,
rayDrawMs: 640,
rayLandAt: 0.7,
rayStaggerMs: 34,
switchStaggerMs: 20,
switchTotalMs: 240,
fanTotalMs: 420,
chainPulseEvery: 1,
chainPulseTravelMs: 800,
chainPulseAlpha: 0.7,
chainPulseHead: 0.05,
chainPulseTail: 0.22,
chainPulseSpreadMs: 200,
dotFlashMs: 420,
labelDelayMs: 60,
labelMs: 520,
labelSlidePx: 10,
exitMs: 200,
hoverMs: 160,
dimMs: 200,
}
const ground = (over: Partial<KnowledgeMapPalette> | undefined, base: KnowledgeMapPalette) =>
`radial-gradient(140% 100% at 50% 50%, ${over?.groundCenter ?? base.groundCenter} 0%, ${over?.ground ?? base.ground} 72%)`
const EMPTY_CATEGORY: KnowledgeMapCategory = { id: 'empty', label: '', count: 0, icon: FileText, nodes: [] }
const noSubscribe = () => () => {}
```State
- every hook, handler, effect and disposal
- the animation loop, frame by frame
Tree
- the JSX, every className and inline style
Why · Remix · Check
- the mechanism, the tuning points, the checks
Unlock AI Knowledge Map
The full source and the remix prompt ship with Premium.