Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -206,8 +206,10 @@ jobs:
with:
path: .next/cache
key: next-${{ runner.os }}-${{ hashFiles('bun.lock', 'next.config.ts') }}-${{ steps.next_cache.outputs.day }}
- name: Install the lockfile-pinned Chromium and Linux dependencies
run: node node_modules/playwright-core/cli.js install --with-deps chromium
# Both browser suites start their own server on a free port against the
# same build and use the runner's preinstalled Chrome. They run one after
# same build and use the lockfile-pinned Chromium. They run one after
# the other: the cross-tab contracts are timing-sensitive and a second
# server plus browser contexts on the same runner made them flake.
- name: Browser contracts against the production build
Expand Down
23 changes: 23 additions & 0 deletions app/messaging.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import snapshot from "../portfolio-messaging.generated.json";

if (snapshot.contract !== "hraness.product-messaging/v1" || snapshot.productId !== "aicharts") {
throw new Error("The website requires the canonical aicharts messaging snapshot.");
}

export const productMessaging = snapshot.messaging;
export const productName = productMessaging.names.name;
if (!snapshot.canonicalUrl.startsWith("https://")) throw new Error("The product origin must use HTTPS.");
export const productCanonicalUrl = snapshot.canonicalUrl as `https://${string}`;

/** Keep the site's selected siblings while deriving their copy and destinations. */
export function relatedProduct(id: string) {
const project = snapshot.projects.find((candidate) => candidate.id === id);
if (project === undefined) throw new Error(`Unknown portfolio product ${id}.`);
return { href: project.canonicalUrl, name: project.name, role: project.messaging.short };
}

export function registeredRelationship(a: string, b: string): string | null {
return snapshot.relations.find((relation) =>
(relation.source === a && relation.target === b) ||
(relation.source === b && relation.target === a))?.detail ?? null;
}
64 changes: 14 additions & 50 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import {
site,
} from "./site";

import { productMessaging, relatedProduct, registeredRelationship } from "./messaging";

import "@/styles/chart-home.css";

export const metadata = createPublicSiteMetadata(searchSite, { canonicalPath: "/" });
Expand Down Expand Up @@ -63,70 +65,32 @@ export default function Home() {
<HomeActivityFeed />
<section aria-labelledby="home-calculator-title" className="home-calculator" data-analytics-surface="home_calculator">
<div className="home-calculator__copy">
<h2 id="home-calculator-title">Subscription vs API vs GPUs</h2>
<h2 id="home-calculator-title">{productMessaging.headings["home-calculator"]}</h2>
<p>One maxed ChatGPT Pro seat implies a monthly token volume. The calculator prices it five ways: the subscription sticker, OpenAI and DeepSeek API rates, GPUs you buy, and GPUs you rent.</p>
</div>
<Link className="home-calculator__cta" href="/calculator">Open the calculator <span aria-hidden="true">↗</span></Link>
</section>
<MarketingRelated
groups={[
{
heading: "The agent platform",
heading: productMessaging.headings["home-platform-group"],
headingId: "related-tools",
summary: "The layer your agent runs through: sessions, accounts, web reads, and the models behind them.",
items: [
{
name: "Excalibur (xcb)",
href: "https://xcb.sh",
role: "Routes coding tasks across the Claude, Codex, and Devin plans you have",
relationship: "Excalibur measures subscription usage locally and, when you turn on exports, writes session files in the aicharts format. Automatic upload is not available.",
},
{
name: "Gobstopper",
href: "https://gobstopper.sh",
role: "Compacts long agent sessions into smaller copies, keeping every byte",
relationship: null,
},
{
name: "GhostGet",
href: "https://ghostget.com",
role: "Named web actions for AI agents: read pages, save media, use connected accounts",
relationship: null,
},
],
items: ["xcb", "gobstopper", "wrench"].map((id) => ({
...relatedProduct(id),
relationship: id === "xcb" ? registeredRelationship("aicharts", id) : null,
})),
},
{
heading: "The personal apps",
heading: productMessaging.headings["home-personal-group"],
headingId: "related-apps",
items: [
{
name: "PeopleBlade",
href: "https://peopleblade.com",
role: "Local personal CRM for everyone you know, built for your agent",
relationship: null,
},
{
name: "Soulscrape",
href: "https://soulscrape.com",
role: "Free agent skill that writes dated dossiers on people, sources cited",
relationship: null,
},
{
name: "Textbutler",
href: "https://textbutler.app",
role: "AI butler for the iMessage, WhatsApp, and Beeper chats you choose",
relationship: null,
},
{
name: "Wordcell",
href: "https://wordcell.io",
role: "Markdown knowledge base that gives agents the decisions behind code",
relationship: null,
},
],
items: ["peopleblade", "soulscrape", "message-like-me", "kb"].map((id) => ({
...relatedProduct(id),
relationship: null,
})),
},
]}
heading="From the same workshop."
heading={productMessaging.headings["home-related"]}
headingId="related-title"
label="Related"
summary="Other Hraness tools for people who work with AI agents."
Expand Down
14 changes: 8 additions & 6 deletions app/site.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { productMessaging } from "./messaging";
import { describe, expect, test } from "bun:test";

import packageJson from "../package.json";
Expand All @@ -16,19 +17,20 @@ import {

describe("aicharts public positioning", () => {
test("carries the canonical product messaging", () => {
// Canonical lines come from the portfolio messaging record (hraness/jungle
// a9988b903). The search title leads with the product name and names the
// comparison a searcher looks for; the tagline stays the visible heading.
// Search and visible copy share the checked portfolio snapshot.
expect(searchSite.title.startsWith(`${site.name}: `)).toBeTrue();
for (const fact of ["benchmark scores", "cost per task"]) {
expect(searchSite.title).toContain(fact);
}
expect(searchSite.title.length).toBeLessThanOrEqual(60);
expect(searchSite.title).not.toContain("&");
expect(site.tagline).toBe("See which model wins at each price.");
expect(site.category).toBe("AI model comparison charts");
expect(homeHeading).toBe(site.tagline);
expect(site.tagline).toBe(productMessaging.tagline);
expect(site.category).toBe(productMessaging.category);
expect(homeHeading).toBe(productMessaging.hero.heading);
expect(homeEyebrow).toBe(site.category);
expect(site.name).toBe(productMessaging.names.name);
expect(site.description).toBe(productMessaging.meta);
expect(homeLede).toBe(productMessaging.hero.summary);
// The meta description names the two product halves: published benchmark
// charts and the local token collector.
expect(site.description.startsWith(`${site.name} `)).toBeTrue();
Expand Down
31 changes: 15 additions & 16 deletions app/site.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,36 @@
// Canonical public identity lines come from the portfolio messaging record
// for `aicharts` (hraness/jungle registry, commit a9988b903).
import { productMessaging, productName, productCanonicalUrl } from "./messaging";

export const site = {
category: "AI model comparison charts",
description: "aicharts plots published AI benchmark scores against cost and tokens per task, and a local collector measures your own agents' token use.",
category: productMessaging.category,
description: productMessaging.meta,
domain: "aicharts.io",
emoji: "◉",
introduction: "Picking a model means guessing at a tradeoff between quality and price, because benchmark scores and prices live in different places. aicharts plots published benchmark scores against cost and tokens per task on one chart, so the strongest option at each budget is visible instead of implied. A local collector measures your own agents' token use, so the cost question covers your work, not only the models. aicharts is free and open source.",
name: "aicharts",
origin: "https://aicharts.io",
introduction: productMessaging.long,
name: productName,
origin: productCanonicalUrl,
palette: {
chromatic: { key: "#5e2e02", support: "#fefefd" },
tonal: { highlight: "#e1e0e0", shadow: "#291201" },
},
tagline: "See which model wins at each price.",
tagline: productMessaging.tagline,
} as const;

export const searchSite = {
description: site.description,
name: site.name,
origin: site.origin,
socialImage: {
alt: "aicharts: Model benchmark scores plotted against cost and tokens per task",
alt: `${productName}: ${productMessaging.short}`,
path: "/opengraph-image",
},
title: "aicharts: AI model benchmark scores vs cost per task",
title: productMessaging.headings["home-search-title"],
} as const;

export const homeEyebrow = site.category;
export const homeHeading = site.tagline;
export const homeLede =
"Benchmark scores plotted against cost and tokens per task, plus a local collector for your own agents' token use.";
export const homePrimaryAction = { href: "/#intelligence-index", label: "Browse the charts" } as const;
export const homeSecondaryAction = { href: "/usage", label: "Measure your agent" } as const;
export const homeHeading = productMessaging.hero.heading;
export const homeLede = productMessaging.hero.summary;
export const homePrimaryAction = { href: "/#intelligence-index", label: productMessaging.hero.primaryAction } as const;
export const homeSecondaryAction = { href: "/usage", label: productMessaging.hero.secondaryAction } as const;
export const homeTaskLinks = [
{ task: "coding", name: "Coding", description: "Build, debug, and work in a terminal." },
{ task: "reasoning", name: "Reasoning", description: "Solve unfamiliar problems." },
Expand All @@ -42,7 +41,7 @@ export const homeTaskLinks = [
] as const;
export { usageReleaseUrl } from "@/lib/usage-cli-release";

export const homeAboutHeading = "About aicharts";
export const homeAboutHeading = productMessaging.headings["home-about"];
/** The date the comparison facts below were last checked against each product's own pages. */
export const homeAlternativesCheckedOn = "2026-09-28";
export const homeAlternativesLead = "Other places to compare models:";
Expand Down
3 changes: 2 additions & 1 deletion app/social-image-site.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
import { formatIntelligenceCost, formatIntelligenceIndex } from "@/lib/index-model-pages";
import { MODEL_CARD_FALLBACK_CREATOR_SLUG } from "@/lib/model-card-route-status";

import { productMessaging } from "./messaging";
import { aichartsMarkDataUrl } from "./social-image-mark";
import { modelCardsEyebrow, site } from "./site";

Expand All @@ -16,7 +17,7 @@ import { modelCardsEyebrow, site } from "./site";
* routes add page copy only.
*/
export const aichartsSocialImageSite = defineSocialImageSite({
description: "Model benchmark scores plotted against cost and tokens per task.",
description: productMessaging.short,
domain: site.domain,
icon: { kind: "mark", src: aichartsMarkDataUrl },
// Names the cards set that must not break across lines.
Expand Down
Loading
Loading