How automatable is your job? The Messiness Index scores jobs across 6 dimensions of task complexity. Higher messiness = harder to automate.
import React, { useState } from "react";
import { createRoot } from "react-dom/client";
interface JobAnalysis {
job: string;
scores: {
coordination: number;
knowledge: number;
exceptions: number;
feedback: number;
environment: number;
adversarial: number;
};
notes: string;
}
interface Insight {
jobs: JobAnalysis[];
}
const DIMENSIONS = [
{ key: "coordination", label: "Coord", full: "Coordination", desc: "Complexity involving people, synchronization, time pressure, and relationship depth." },
{ key: "knowledge", label: "Know", full: "Knowledge", desc: "Breadth and depth of domains touched and expertise required." },
{ key: "exceptions", label: "Except", full: "Exceptions", desc: "Frequency, cost, and novelty of unpredictable events or errors." },
{ key: "feedback", label: "Feed", full: "Feedback", desc: "Measurability, speed, and stakeholder agreement on success." },
{ key: "environment", label: "Env", full: "Environment", desc: "Physical variability, accessibility constraints, and required manual precision." },
{ key: "adversarial", label: "Adver", full: "Adversarial", desc: "Degree of active opposition, competitive stakes, and player adaptiveness." },
] as const;
type DimKey = typeof DIMENSIONS[number]["key"];
const SCORE_LABELS: Record<number, string> = {
0: "N/A",
1: "V.Low",
2: "Low",
3: "Medium",
4: "High",
5: "V.High",
};
function scoreColor(score: number): string {
if (score === 0) return "#333";
const colors = ["", "#2d5a27", "#4a7c3f", "#c9a227", "#c46210", "#a82a2a"];
return colors[score] || "#333";
}
function totalMessiness(scores: JobAnalysis["scores"]): number {
return Object.values(scores).reduce((sum, v) => sum + (v || 0), 0);
}
interface TooltipContent {
title: string;
body: React.ReactNode;
color: string;
}
interface TooltipState {
visible: boolean;
x: number;
y: number;
content: TooltipContent | null;
}
function Popup({ state, onClose }: { state: TooltipState; onClose: () => void }) {
if (!state.visible || !state.content) return null;
const { title, body, color } = state.content;
return (
<div
className="popup-container"
style={{ left: state.x, top: state.y, borderColor: color } as any}
>
<div className="popup-header">
<span className="popup-dot" style={{ backgroundColor: color }} />
<span className="popup-title">{title}</span>
<button className="popup-close" onClick={onClose}>×</button>
</div>
<div className="popup-body">{body}</div>
</div>
);
}
function App() {
const [insight, setInsight] = useState<Insight | null>(() => {
try { return tb.insight<Insight>(); } catch { return null; }
});
const [isGenerating, setIsGenerating] = useState(false);
const [tooltip, setTooltip] = useState<TooltipState>({ visible: false, x: 0, y: 0, content: null });
const [sortBy, setSortBy] = useState<DimKey | "total" | "name">("total");
const [sortAsc, setSortAsc] = useState(false);
const [expanded, setExpanded] = useState<Set<string>>(new Set());
const toggleExpand = (job: string) => {
setExpanded(prev => {
const next = new Set(prev);
if (next.has(job)) next.delete(job);
else next.add(job);
return next;
});
};
const handleAnalyze = async () => {
setIsGenerating(true);
try {
const result = await tb.infer<Insight>();
if (result) {
setInsight(result);
setTooltip(prev => ({ ...prev, visible: false }));
}
} finally {
setIsGenerating(false);
}
};
const triggerPopup = (e: React.MouseEvent, content: TooltipContent) => {
e.stopPropagation();
const popupWidth = 320;
const popupHeight = 240;
const padding = 15;
const edgeMargin = 10;
const vw = window.innerWidth;
const vh = window.innerHeight;
let x = e.clientX + padding;
let y = e.clientY + padding;
if (x + popupWidth > vw - edgeMargin) x = e.clientX - popupWidth - padding;
if (y + popupHeight > vh - edgeMargin) y = e.clientY - popupHeight - padding;
x = Math.max(edgeMargin, Math.min(x, vw - popupWidth - edgeMargin));
y = Math.max(edgeMargin, Math.min(y, vh - popupHeight - edgeMargin));
setTooltip({ visible: true, x, y, content });
};
const sortedJobs = insight?.jobs.slice().sort((a, b) => {
let cmp = 0;
if (sortBy === "name") {
cmp = a.job.localeCompare(b.job);
} else if (sortBy === "total") {
cmp = totalMessiness(b.scores) - totalMessiness(a.scores);
} else {
cmp = (b.scores[sortBy] || 0) - (a.scores[sortBy] || 0);
}
return sortAsc ? -cmp : cmp;
});
const handleSort = (key: DimKey | "total" | "name") => {
if (sortBy === key) {
setSortAsc(!sortAsc);
} else {
setSortBy(key);
setSortAsc(false);
}
};
return (
<div className="app" onClick={() => setTooltip(prev => ({ ...prev, visible: false }))}>
<Popup state={tooltip} onClose={() => setTooltip(prev => ({ ...prev, visible: false }))} />
<header>
<h1>How Automatable Is Your Job?</h1>
<p className="brand">The Messiness Index</p>
<p className="tagline">Higher messiness = harder to automate</p>
<button className={`analyze-btn ${isGenerating ? "loading" : ""}`} onClick={handleAnalyze}>
{isGenerating ? "Analyzing..." : "Analyze Jobs"}
</button>
</header>
{insight && (
<>
<div className="mobile-sort">
<label>Sort by</label>
<select
value={sortBy}
onChange={e => {
setSortBy(e.target.value as any);
setSortAsc(false);
}}
>
<option value="total">Total</option>
<option value="name">Name</option>
{DIMENSIONS.map(d => (
<option key={d.key} value={d.key}>{d.full}</option>
))}
</select>
<button className="sort-dir-btn" onClick={() => setSortAsc(!sortAsc)}>
{sortAsc ? "↑" : "↓"}
</button>
</div>
<div className="card-list">
{sortedJobs?.map(job => {
const total = totalMessiness(job.scores);
const isOpen = expanded.has(job.job);
return (
<div key={job.job} className={`card ${isOpen ? "open" : ""}`}>
<button className="card-header" onClick={() => toggleExpand(job.job)}>
<span className="card-job">{job.job}</span>
<span className="card-right">
<span className="card-total">{total}</span>
<span className={`card-chevron ${isOpen ? "open" : ""}`}>▾</span>
</span>
</button>
{isOpen && (
<div className="card-body">
<div className="card-scores">
{DIMENSIONS.map(d => {
const score = job.scores[d.key] ?? 0;
return (
<div key={d.key} className="card-score-row">
<span className="card-dim-label">{d.full}</span>
<span className="card-score-badge" style={{ background: scoreColor(score) }}>
{SCORE_LABELS[score]}
</span>
</div>
);
})}
</div>
{job.notes && <p className="card-notes">{job.notes}</p>}
</div>
)}
</div>
);
})}
</div>
<div className="table-wrap">
<table className="matrix">
<thead>
<tr>
<th className={`sortable ${sortBy === "name" ? "active" : ""}`} onClick={() => handleSort("name")}>
<div className="header-content">
<span className="header-spacer" />
<span className="dim-label">Job</span>
<div className="header-icons">
<span className="dim-info-spacer" />
<span className="sort-arrow-wrap">{sortBy === "name" && (sortAsc ? "↑" : "↓")}</span>
</div>
</div>
</th>
{DIMENSIONS.map(d => (
<th
key={d.key}
className={`sortable dim-header ${sortBy === d.key ? "active" : ""}`}
onClick={() => handleSort(d.key)}
>
<div className="header-content">
<span className="header-spacer" />
<span className="dim-label">{d.full}</span>
<div className="header-icons">
<button
className="dim-info-trigger"
onClick={(e) => triggerPopup(e, {
title: d.full,
body: <p>{d.desc}</p>,
color: "var(--accent)"
})}
>?</button>
<span className="sort-arrow-wrap">{sortBy === d.key && (sortAsc ? "↑" : "↓")}</span>
</div>
</div>
</th>
))}
<th
className={`sortable ${sortBy === "total" ? "active" : ""}`}
onClick={() => handleSort("total")}
>
<div className="header-content">
<span className="header-spacer" />
<span className="dim-label">Total</span>
<div className="header-icons">
<span className="dim-info-spacer" />
<span className="sort-arrow-wrap">{sortBy === "total" && (sortAsc ? "↑" : "↓")}</span>
</div>
</div>
</th>
</tr>
</thead>
<tbody>
{sortedJobs?.map(job => (
<tr key={job.job}>
<td
className="job-name clickable"
onClick={(e) => triggerPopup(e, {
title: job.job,
body: (
<div className="job-popup-content">
<p className="popup-notes">{job.notes}</p>
<div className="radar-wrap">
<RadarChart scores={job.scores} />
</div>
</div>
),
color: "var(--accent)"
})}
>
{job.job}
</td>
{DIMENSIONS.map(d => {
const score = job.scores[d.key] ?? 0;
return (
<td key={d.key} className="score-cell" style={{ background: scoreColor(score) }}>
{SCORE_LABELS[score]}
</td>
);
})}
<td className="total-cell">{totalMessiness(job.scores)}</td>
</tr>
))}
</tbody>
</table>
</div>
</>
)}
{!insight && (
<div className="empty">
<p>Add any jobs to the Data tab, then click "Analyze Jobs".</p>
<p className="hint">We'll score each one across 6 dimensions of task complexity.</p>
</div>
)}
</div>
);
}
function RadarChart({ scores }: { scores: JobAnalysis["scores"] }) {
const size = 200;
const cx = size / 2;
const cy = size / 2;
const r = 70;
const dims = DIMENSIONS.filter(d => scores[d.key] > 0);
if (dims.length < 3) return <div className="radar-empty">Not enough dimensions for radar</div>;
const points = dims.map((d, i) => {
const angle = (Math.PI * 2 * i) / dims.length - Math.PI / 2;
const val = scores[d.key] / 5;
return {
x: cx + Math.cos(angle) * r * val,
y: cy + Math.sin(angle) * r * val,
lx: cx + Math.cos(angle) * (r + 20),
ly: cy + Math.sin(angle) * (r + 20),
label: d.label,
};
});
const pathD = points.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x} ${p.y}`).join(" ") + " Z";
const gridLevels = [0.2, 0.4, 0.6, 0.8, 1];
return (
<svg width={size} height={size} className="radar">
{gridLevels.map(level => {
const gridPath = dims.map((_, i) => {
const angle = (Math.PI * 2 * i) / dims.length - Math.PI / 2;
const x = cx + Math.cos(angle) * r * level;
const y = cy + Math.sin(angle) * r * level;
return `${i === 0 ? "M" : "L"} ${x} ${y}`;
}).join(" ") + " Z";
return <path key={level} d={gridPath} className="radar-grid" />;
})}
{dims.map((_, i) => {
const angle = (Math.PI * 2 * i) / dims.length - Math.PI / 2;
return (
<line
key={i}
x1={cx}
y1={cy}
x2={cx + Math.cos(angle) * r}
y2={cy + Math.sin(angle) * r}
className="radar-axis"
/>
);
})}
<path d={pathD} className="radar-area" />
{points.map((p, i) => (
<text key={i} x={p.lx} y={p.ly} className="radar-label" textAnchor="middle" dominantBaseline="middle">
{p.label}
</text>
))}
</svg>
);
}
createRoot(document.getElementById("root")!).render(<App />);