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
81 changes: 59 additions & 22 deletions apps/website/public/assets/hero.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions apps/website/public/whitepaper-preview.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@

<!-- Cover -->
<div style="height:100vh;display:flex;flex-direction:column;justify-content:flex-end;padding:80px 80px 100px;background:linear-gradient(135deg, #fafbfc 0%, #eaf3ff 100%);page-break-after:always">
<div style="font-family:monospace;font-size:11px;text-transform:uppercase;letter-spacing:0.12em;color:#004090;font-weight:700;margin-bottom:24px">Threadplane · Enterprise Angular Agent UI</div>
<h1 style="font-family:'EB Garamond',serif;font-size:52px;font-weight:800;line-height:1.1;color:#1a1a2e;margin-bottom:20px">Agent<br>UI<br>for<br>Angular</h1>
<div style="font-family:monospace;font-size:11px;text-transform:uppercase;letter-spacing:0.12em;color:#004090;font-weight:700;margin-bottom:24px">Threadplane · Open source · Angular</div>
<h1 style="font-family:'EB Garamond',serif;font-size:52px;font-weight:800;line-height:1.1;color:#1a1a2e;margin-bottom:20px">Threadplane</h1>
<p style="font-family:'EB Garamond',serif;font-style:italic;font-size:20px;color:#555770;margin-bottom:40px">Production-ready chat, threads, and generative UI for AI agents</p>
<div style="font-size:13px;color:#8b8fa3;font-family:monospace">threadplane.ai · 2026</div>
</div>
Expand Down
Binary file modified apps/website/public/whitepaper.pdf
Binary file not shown.
2 changes: 1 addition & 1 deletion apps/website/scripts/generate-whitepaper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ const WHITEPAPERS: Record<string, WhitepaperConfig> = {
id: 'overview',
title: 'Threadplane',
subtitle: 'Production-ready chat, threads, and generative UI for AI agents',
eyebrow: 'Threadplane · Enterprise Angular Agent UI',
eyebrow: 'Threadplane · Open source · Angular',
coverGradient: 'linear-gradient(135deg, #fafbfc 0%, #eaf3ff 100%)',
outputPdf: 'apps/website/public/whitepaper.pdf',
outputHtml: 'apps/website/public/whitepaper-preview.html',
Expand Down
32 changes: 30 additions & 2 deletions apps/website/scripts/refresh-whitepaper-covers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,13 @@
* 2. Update the cover footer color (#888 → #8b8fa3 / textMuted).
* 3. Update TOC row colors (rgba(0,0,0,.06) → #e6e8ee / border;
* #444 → #555770 / textSecondary).
* 4. Write the updated HTML back.
* 5. Render to PDF using Puppeteer.
* 4. Replace retired positioning copy on the cover. The rendered artifacts
* outlive the generator config — the overview cover still read "Agent UI
* for Angular" under the eyebrow "Threadplane · Enterprise Angular Agent
* UI" long after both were retired, because regenerating means paying for
* LLM chapter prose nobody wanted to change.
* 5. Write the updated HTML back.
* 6. Render to PDF using Puppeteer.
*
* Usage:
* pnpm tsx apps/website/scripts/refresh-whitepaper-covers.ts
Expand Down Expand Up @@ -55,6 +60,25 @@ const PAPERS: Paper[] = [
},
];

/**
* Retired positioning, and what replaces it.
*
* Straight string swaps rather than regexes: these are exact phrases that
* shipped inside a cover, and a loose pattern here would quietly rewrite
* chapter prose further down the same file.
*
* The guide titles on the other three covers are deliberately absent. "The
* Enterprise Guide to Generative UI in Angular" is the name of a document,
* not a claim about what the product is, and renaming a book people have
* already downloaded buys nothing.
*/
const RETIRED_COPY: ReadonlyArray<readonly [string, string]> = [
['Threadplane · Enterprise Angular Agent UI', 'Threadplane · Open source · Angular'],
// The generator's config already says `title: 'Threadplane'`; only the
// rendered artifact still carries the old four-line stack.
['Agent<br>UI<br>for<br>Angular', 'Threadplane'],
];

const LEGACY_GRADIENT_RE =
/background:linear-gradient\(135deg,\s*#[0-9a-fA-F]+\s+0%,\s*#[0-9a-fA-F]+\s+45%,\s*#[0-9a-fA-F]+\s+70%,\s*#[0-9a-fA-F]+\s+100%\)/;

Expand All @@ -74,6 +98,10 @@ function refreshHtml(html: string, paper: Paper): string {
'border-bottom:1px solid #e6e8ee;font-size:15px;color:#555770',
);

for (const [from, to] of RETIRED_COPY) {
out = out.split(from).join(to);
}

return out;
}

Expand Down
40 changes: 40 additions & 0 deletions apps/website/src/lib/brand-assets.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { describe, expect, it } from 'vitest';
import { RETIRED_POSITIONING } from './public-copy-contract';

const REPO_ROOT = join(__dirname, '..', '..', '..', '..');

/**
* Artifacts that state the product's positioning but are not generated from
* `positioning.ts`. Each is a file a human has to remember to update, which
* is exactly why this test exists: the README banner and the whitepaper cover
* both went months asserting a tagline that had been replaced.
*/
const BRAND_ASSETS = [
'README.md',
'apps/website/public/assets/hero.svg',
'apps/website/public/whitepaper-preview.html',
'apps/website/scripts/generate-whitepaper.ts',
'libs/a2ui/README.md',
'libs/ag-ui/README.md',
'libs/chat/README.md',
'libs/langgraph/README.md',
'libs/middleware/README.md',
'libs/render/README.md',
'libs/telemetry/README.md',
];

describe('brand assets carry current positioning', () => {
it.each(BRAND_ASSETS)('%s states no retired positioning', (relative) => {
const text = readFileSync(join(REPO_ROOT, relative), 'utf8');
const found = RETIRED_POSITIONING.filter((phrase) => text.includes(phrase));
expect(found, `${relative} still says: ${found.join(', ')}`).toEqual([]);
});

it('names phrases to look for, so the scan cannot pass by being empty', () => {
// A guard whose list is empty passes every file and reports nothing. This
// is the mutation check the list itself cannot perform.
expect(RETIRED_POSITIONING.length).toBeGreaterThan(3);
});
});
22 changes: 22 additions & 0 deletions apps/website/src/lib/public-copy-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,28 @@ export const NARRATIVE_MENTIONS: ReadonlyArray<readonly [string, RegExp]> = [
['telemetry hooks aside', /telemetry hooks/iu],
];

/**
* Positioning we have retired, and must not still be asserting anywhere.
*
* Copy on the website is single-sourced through `positioning.ts` and guarded
* by its own spec. These are the places that are *not*: static SVG banners,
* generated PDF covers, and the package READMEs — artifacts that keep
* rendering happily long after the words on them stopped being true. The
* overview whitepaper cover carried "Agent UI for Angular" for months past
* that phrase's retirement, because regenerating it meant paying for LLM
* chapter prose nobody wanted to touch.
*
* Add a phrase here in the same change that retires it.
*/
export const RETIRED_POSITIONING: readonly string[] = [
'AI agent UI framework for Angular',
'Angular AI Agent UI Framework',
'Angular AI agent UI framework',
'Enterprise Angular Agent UI',
'Production-ready agent UI',
'thread plane for enterprise agents',
];

/** Routes retired in favour of the canonical policy. */
export const RETIRED_ROUTE_PATTERN = /\/docs\/telemetry|\/api\/markdown\/telemetry/u;

Expand Down
Loading