// File: page.tsx // Path: apps/web/app/page.tsx // Project: AI Risk Index — airiskindex.io // Author: Simon-Pierre Boucher // Contact: contact@spboucher.ai // Copyright © 2026 Simon-Pierre Boucher. All rights reserved. // // Description: Home page: hero, live search, ranking (most/least exposed), group averages. import Link from "next/link"; import { prisma } from "@airiskindex/db"; import { OccupationSearch } from "@/components/occupation-search"; import { ScoreBar } from "@/components/score-marks"; import { SOC_MAJOR_GROUPS } from "@/lib/soc-groups"; export const dynamic = "force-dynamic"; const CONCEPTS = [ { name: "Exposure", description: "AI is technically capable of performing the task. High exposure alone does not mean job loss — it means the occupation's tasks are changing.", }, { name: "Substitution", description: "AI actually replaces the human performing the task, once cost, adoption velocity and real-world barriers are accounted for. This is the headline score.", }, { name: "Augmentation", description: "AI assists the human, increasing productivity. For most occupations today, measured usage is augmentation, not replacement.", }, ] as const; interface RankRow { occupationCode: string; substitution: number; substitutionLow: number; substitutionHigh: number; exposure: number; augmentation: number; occupation: { code: string; title: string }; } async function loadHome() { try { const [occupations, tasks, run] = await Promise.all([ prisma.occupation.count(), prisma.task.count(), prisma.scoreRun.findFirst({ orderBy: { createdAt: "desc" } }), ]); if (!run) return { occupations, tasks, run: null, scored: 0, top: [], bottom: [], groups: [] }; const [scored, top, bottom, groups] = await Promise.all([ prisma.occupationScore.count({ where: { runId: run.id } }), prisma.occupationScore.findMany({ where: { runId: run.id }, orderBy: { substitution: "desc" }, take: 15, include: { occupation: { select: { code: true, title: true } } }, }), prisma.occupationScore.findMany({ where: { runId: run.id }, orderBy: { substitution: "asc" }, take: 15, include: { occupation: { select: { code: true, title: true } } }, }), prisma.$queryRaw>` SELECT left("occupationCode", 2) AS prefix, avg(substitution)::float AS avg, count(*) AS n FROM "OccupationScore" WHERE "runId" = ${run.id} GROUP BY 1 HAVING count(*) >= 3 ORDER BY 2 DESC `, ]); return { occupations, tasks, run, scored, top, bottom, groups }; } catch { return { occupations: 0, tasks: 0, run: null, scored: 0, top: [], bottom: [], groups: [] }; } } function RankList({ rows, startRank }: { rows: RankRow[]; startRank?: number }): JSX.Element { return (
    {rows.map((row, index) => (
  1. {startRank !== undefined && ( {startRank + index} )} {row.occupation.title} {row.substitution.toFixed(0)}
    CI {row.substitutionLow.toFixed(0)}–{row.substitutionHigh.toFixed(0)} · exposure{" "} {row.exposure.toFixed(0)} · augmentation {row.augmentation.toFixed(0)}
  2. ))}
); } export default async function HomePage(): Promise { const { occupations, tasks, run, scored, top, bottom, groups } = await loadHome(); return (

How is your occupation exposed to AI — task by task?

Every occupation is scored from its individual tasks by a multi-model rater panel — three sub-scores, confidence intervals from rater disagreement, versioned methodology, public API. Built for adaptation planning, not headlines.

{[ [occupations.toLocaleString("en-US"), "occupations (O*NET 30.3)"], [tasks.toLocaleString("en-US"), "task statements"], [scored.toLocaleString("en-US"), "occupations scored"], [run ? run.indexVersion : "—", "methodology version"], ].map(([value, label]) => (

{value}

{label}

))}
{run && top.length > 0 && ( <>

Substitution ranking

run {run.indexVersion} · {run.createdAt.toISOString().slice(0, 10)} ·{" "} full index — all {scored.toLocaleString("en-US")} ranked →

Composite substitution pressure, 0–100. The whisker marks the confidence interval from rater disagreement — a wide band is a claim we hold loosely.

Most exposed

Least exposed

{groups.length > 0 && (

By occupation group

Mean substitution score across scored occupations in each SOC major group.

{groups.map((group) => (
{SOC_MAJOR_GROUPS[group.prefix] ?? group.prefix} {Number(group.n)} occupations {group.avg.toFixed(0)}
)} )}

Three scores, never collapsed into one

{CONCEPTS.map((concept) => (

{concept.name}

{concept.description}

))}
); }