/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
/**
* Variant B — "Stage"
*
* Cinematic, dark-by-default, demo-driven. The point of this variant is to
* make people feel the agent driving the model. So the hero is an animated
* IFC wireframe that progressively colorises itself as a fake transcript
* scrolls underneath ("agent: viewer_isolate IfcWall …"). Big confident
* type, generous breathing room, and recipes shown as a horizontally
* scrolling carousel of stylised agent conversations.
*
* Typography: Instrument Serif (italic, for the flex character) carries
* display + numerals. Bricolage Grotesque (variable) does the body work.
* JetBrains Mono for code. The chartreuse accent (#d6ff3f) is a nod to
* construction-safety hi-vis — distinctive on a black field, never seen on
* a generic SaaS landing.
*
* The variant explicitly forces dark on its own subtree without flipping
* the global .dark class, so the rest of the SPA isn’t affected when the
* user navigates away.
*/
import {
type CSSProperties,
type ReactNode,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import {
ArrowDown,
ArrowLeft,
ArrowUpRight,
Check,
ChevronRight,
Copy,
Play,
Sparkles,
Sun,
Terminal,
} from 'lucide-react';
import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog';
import { cn } from '@/lib/utils';
import { HeroScene, HERO_STEPS, HERO_STEP_MS, type HeroStep } from './HeroScene';
import {
CATALOG,
CATEGORY_BLURBS,
CATEGORY_ORDER,
CLIENTS,
EXAMPLES,
FAMILY_ACCENT,
MCP_VERSION,
RECIPES,
catalogStats,
exampleCall,
makeConfigSnippet,
makeDeepLink,
paramsFor,
toolsByCategory,
type ParamRow,
} from './data';
import type { CatalogTool, McpClient, McpClientId, ToolCategory } from './types';
import { scrollToAnchor, useCopyToClipboard, useDocumentMeta, useFonts } from './use-mcp-page';
const NIGHT = '#0a0a0c';
const NIGHT_2 = '#121215';
const PAPER = '#ede4d3';
const PAPER_DIM = '#9c9486';
const ACCENT = '#d6ff3f'; // hi-vis chartreuse
const ACCENT_2 = '#ff5cdc'; // magenta for hover/active
const RULE = 'rgba(237, 228, 211, 0.10)';
const stage: CSSProperties = {
background: NIGHT,
color: PAPER,
fontFamily: '"Bricolage Grotesque", "Inter Tight", system-ui, sans-serif',
fontFeatureSettings: '"ss01" 1, "ss02" 1, "cv11" 1',
};
const display: CSSProperties = {
fontFamily: '"Instrument Serif", "Newsreader", Georgia, serif',
fontWeight: 400,
fontStyle: 'normal',
};
const mono: CSSProperties = {
fontFamily: '"JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace',
};
export function McpLanding(): ReactNode {
useFonts(
'https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Bricolage+Grotesque:opsz,wght@12..96,300;12..96,400;12..96,500;12..96,600;12..96,700&family=JetBrains+Mono:wght@400;500;600&display=swap',
);
useDocumentMeta('@ifc-lite/mcp — drive an IFC from any LLM', NIGHT);
return (
);
}
// ── backdrop ────────────────────────────────────────────────────────────────
function BackdropGrain(): ReactNode {
// SVG fractal noise gives the dark field a subtle grain — keeps the page
// from looking like flat black, especially on OLEDs.
return (
);
}
// ── top bar ─────────────────────────────────────────────────────────────────
function TopBar(): ReactNode {
return (
{/* Brand also acts as the back-to-viewer affordance, but the
Viewer link in the nav makes that explicit so it doesn't
rely on users guessing. */}
ifc-lite
/ mcp · {MCP_VERSION}
{stats.total} typed tools that let any LLM agent query, validate, mutate, and
visualise real IFC building models. The same toolkit your engineers ship with, in a
chat.
);
}
/**
* Hero stage — a real Three.js building (HeroScene.tsx) driven by twelve
* distinct agent-transcript steps. Each step:
*
* • mutates the WebGL scene (colour / isolation / section / new entity / camera),
* • prints its tool-call line under the canvas,
* • optionally overlays a UI badge or panel (audit score, count histogram,
* bSDD pset list, BCF pin, describe-selection card).
*
* The 12-step loop covers 7 of the MCP categories (Discovery, Query,
* Validation, Mutation, BCF, bSDD, Viewer) so a viewer instantly sees the
* surface is much wider than "colour the walls".
*/
function WireframeStage(): ReactNode {
const [step, setStep] = useState(0);
// Pin position in container-local pixels, fed by HeroScene every rAF.
const [pinFrame, setPinFrame] = useState<{ x: number; y: number; visible: boolean } | null>(null);
useEffect(() => {
const t = setTimeout(() => setStep((s) => (s + 1) % HERO_STEPS.length), HERO_STEP_MS);
return () => clearTimeout(t);
}, [step]);
const current = HERO_STEPS[step];
return (
{/* faint grid behind the canvas, masked outward so the building feels lit */}
{/* WebGL canvas */}
);
}
/**
* HeroOverlay — sparse, iconic UI per step. Each one shows the smallest
* possible "proof of action" so the canvas stays the hero. No prose, no
* tool names duplicated from the transcript bar — just the result.
*
* Pointer-events are off everywhere so OrbitControls never lose clicks.
*/
function HeroOverlay({
step,
pinFrame,
}: {
step: HeroStep;
pinFrame: { x: number; y: number; visible: boolean } | null;
}): ReactNode {
if (!step.overlay) return null;
const o = step.overlay;
const chrome =
'absolute z-20 rounded-md border px-3 py-2.5 backdrop-blur-md pointer-events-none';
const chromeStyle: CSSProperties = {
borderColor: RULE,
background: 'rgba(18,18,21,0.82)',
color: PAPER,
...mono,
};
// ── Audit: huge score in display serif, single sparkline-y bar.
if (o.kind === 'audit') {
const pct = Math.max(0, Math.min(100, o.score));
return (
{o.score}
/ 100
{o.note}
);
}
// ── Counts: a tiny histogram. Tall numerals, faint type labels, a bar
// proportional to the largest row. Three rows max.
if (o.kind === 'counts') {
const max = Math.max(...o.rows.map((r) => r.n), 1);
return (
{o.rows.map((row) => (
Ifc{row.type}
{row.n}
))}
);
}
// ── Psets (bSDD): an alphanumeric data tag — Pset header rules + a few
// canonical properties with their datatypes. Reads as a real spec
// sheet, not a vague label list.
if (o.kind === 'psets') {
// Hard-coded sample property rows per Pset so the page renders even
// before the live MCP tools/call response is wired up. Order kept
// deterministic so the text doesn’t reflow between renders.
const SAMPLE_ROWS: Record> = {
Pset_WallCommon: [
{ k: 'FireRating', v: 'EI60', t: 'string' },
{ k: 'IsExternal', v: 'true', t: 'boolean' },
{ k: 'LoadBearing', v: 'false', t: 'boolean' },
{ k: 'AcousticRating', v: 'R45', t: 'string' },
],
Qto_WallBaseQuantities: [
{ k: 'Length', v: '5.20', t: 'm' },
{ k: 'Height', v: '3.00', t: 'm' },
{ k: 'Volume', v: '3.74', t: 'm³' },
],
Pset_ConcreteElementGeneral: [
{ k: 'StrengthClass', v: 'C30/37', t: 'string' },
{ k: 'AssemblyPlace', v: 'SITE', t: 'enum' },
],
};
return (
);
}
// ── BCF pin caption — the pin itself lives in WebGL (a Sprite anchored
// to the wall), so this overlay is just the small alphanumeric label
// that follows the pin's projected screen position. Hidden if we
// don't have a fresh projection yet.
if (o.kind === 'pin') {
if (!pinFrame || !pinFrame.visible) return null;
return (
{o.ref}
);
}
// ── Inspect card: a hairline frame, ref + at most two evidence lines.
if (o.kind === 'card') {
return (
}
/>
{/* Full-bleed scroller wrapper so fades + spacers can sit outside the
1280-max content column. The cards align to the same gutter as the
section header by padding the scroller with a calc() that mirrors
the centred content width. */}
{/* edge fades — purely cosmetic, must not eat clicks */}
{/* pagination dots */}
{RECIPES.length} recipes · scroll →
{/* One dot per page (not per recipe), so as cards-per-page changes
with viewport width the active highlight remains reachable. */}
{Array.from({ length: scrollState.pages }, (_, i) => (
))}