diff --git a/README.md b/README.md index 35c026eb2..1c8ba69b7 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@

Angular Agent Framework — agent UI primitives for Angular

@@ -109,7 +109,7 @@ That's it. `chat.messages()` and `chat.status()` are Angular Signals. Bind them

Angular Agent Framework architecture: Angular Component → agent() → StreamManager Bridge → LangGraph Platform, with signals returned reactively

diff --git a/apps/cockpit/src/app/layout.tsx b/apps/cockpit/src/app/layout.tsx index 4a7a8cf3b..df98aef26 100644 --- a/apps/cockpit/src/app/layout.tsx +++ b/apps/cockpit/src/app/layout.tsx @@ -6,17 +6,17 @@ import { AnalyticsBootstrap } from '../components/analytics-bootstrap'; import './cockpit.css'; export const metadata = { - title: 'Cockpit — Angular Agent Framework', - description: 'The live reference app for the Angular Agent Framework. Real LangGraph + AG-UI agents through the Angular surface you’ll ship.', + title: 'Cockpit — Agent UI for Angular', + description: 'The live reference app for Agent UI for Angular. Real LangGraph + AG-UI agents through the Angular surface you’ll ship.', openGraph: { - title: 'Cockpit — Angular Agent Framework', + title: 'Cockpit — Agent UI for Angular', description: 'The live reference app for the framework. Real LangGraph + AG-UI agents through the same Angular surface you’ll ship.', type: 'website', siteName: 'Cockpit', }, twitter: { card: 'summary_large_image', - title: 'Cockpit — Angular Agent Framework', + title: 'Cockpit — Agent UI for Angular', description: 'The live reference app for the framework. Real LangGraph + AG-UI agents through the Angular surface you’ll ship.', }, }; diff --git a/apps/cockpit/src/app/opengraph-image.tsx b/apps/cockpit/src/app/opengraph-image.tsx index 5a5be8d96..5aae5efe1 100644 --- a/apps/cockpit/src/app/opengraph-image.tsx +++ b/apps/cockpit/src/app/opengraph-image.tsx @@ -13,7 +13,7 @@ import { ImageResponse } from 'next/og'; import { darkOverrides } from '@ngaf/design-tokens'; export const runtime = 'edge'; -export const alt = 'Cockpit — the live reference app for the Angular Agent Framework'; +export const alt = 'Cockpit — the live reference app for Agent UI for Angular'; export const size = { width: 1200, height: 630 }; export const contentType = 'image/png'; @@ -71,7 +71,7 @@ export default async function OpenGraphImage() { marginBottom: 24, }} > - Cockpit · Angular Agent Framework + Cockpit · Agent UI for Angular {/* Headline — Inter Bold (cockpit chrome is sans-serif Linear-style) */} diff --git a/apps/cockpit/tsconfig.json b/apps/cockpit/tsconfig.json index ec30f9d83..4cfa2565c 100644 --- a/apps/cockpit/tsconfig.json +++ b/apps/cockpit/tsconfig.json @@ -15,10 +15,64 @@ "@ngaf/design-tokens": ["../../libs/design-tokens/src/index.ts"], "@ngaf/ui-react": ["../../libs/ui-react/src/index.ts"], "@ngaf/cockpit-registry": ["../../libs/cockpit-registry/src/index.ts"], - "@ngaf/telemetry/shared": ["../../libs/telemetry/src/shared/public-api.ts"], - "@ngaf/telemetry/browser": ["../../libs/telemetry/src/browser/public-api.ts"] + "@ngaf/telemetry/shared": [ + "../../libs/telemetry/src/shared/public-api.ts" + ], + "@ngaf/telemetry/browser": [ + "../../libs/telemetry/src/browser/public-api.ts" + ] } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] + "exclude": ["node_modules"], + "references": [ + { + "path": "../../cockpit/deep-agents/sandboxes/python" + }, + { + "path": "../../cockpit/deep-agents/skills/python" + }, + { + "path": "../../cockpit/deep-agents/subagents/python" + }, + { + "path": "../../cockpit/deep-agents/filesystem/python" + }, + { + "path": "../../cockpit/deep-agents/planning/python" + }, + { + "path": "../../cockpit/deep-agents/memory/python" + }, + { + "path": "../../cockpit/langgraph/deployment-runtime/python" + }, + { + "path": "../../cockpit/langgraph/time-travel/python" + }, + { + "path": "../../cockpit/langgraph/subgraphs/python" + }, + { + "path": "../../cockpit/langgraph/durable-execution/python" + }, + { + "path": "../../cockpit/langgraph/memory/python" + }, + { + "path": "../../cockpit/langgraph/interrupts/python" + }, + { + "path": "../../cockpit/langgraph/persistence/python" + }, + { + "path": "../../cockpit/langgraph/streaming/python" + }, + { + "path": "../../libs/design-tokens" + }, + { + "path": "../../libs/telemetry" + } + ] } diff --git a/apps/minting-service/tsconfig.app.json b/apps/minting-service/tsconfig.app.json index 2a61e4be9..9e04ba83b 100644 --- a/apps/minting-service/tsconfig.app.json +++ b/apps/minting-service/tsconfig.app.json @@ -11,5 +11,17 @@ "emitDeclarationOnly": false }, "include": ["src/**/*.ts", "handlers/**/*.ts", "scripts/**/*.ts"], - "exclude": ["src/**/*.spec.ts", "handlers/**/*.spec.ts", "scripts/**/*.spec.ts"] + "exclude": [ + "src/**/*.spec.ts", + "handlers/**/*.spec.ts", + "scripts/**/*.spec.ts" + ], + "references": [ + { + "path": "../../libs/licensing/tsconfig.lib.json" + }, + { + "path": "../../libs/db/tsconfig.lib.json" + } + ] } diff --git a/apps/website/content/AGENTS.md.template b/apps/website/content/AGENTS.md.template index 778772e04..33070ccb0 100644 --- a/apps/website/content/AGENTS.md.template +++ b/apps/website/content/AGENTS.md.template @@ -1,4 +1,4 @@ -# Angular Agent Framework v@VERSION@ +# Agent UI for Angular v@VERSION@ Agent UI primitives and LangGraph streaming adapters for Angular. diff --git a/apps/website/content/CLAUDE.md.template b/apps/website/content/CLAUDE.md.template index 778772e04..33070ccb0 100644 --- a/apps/website/content/CLAUDE.md.template +++ b/apps/website/content/CLAUDE.md.template @@ -1,4 +1,4 @@ -# Angular Agent Framework v@VERSION@ +# Agent UI for Angular v@VERSION@ Agent UI primitives and LangGraph streaming adapters for Angular. diff --git a/apps/website/e2e/website.spec.ts b/apps/website/e2e/website.spec.ts index c028bfc9b..a35ec5e44 100644 --- a/apps/website/e2e/website.spec.ts +++ b/apps/website/e2e/website.spec.ts @@ -117,7 +117,7 @@ test('docs pages render canonical and social metadata', async ({ page }) => { ); await expect(page.locator('meta[property="og:title"]')).toHaveAttribute( 'content', - 'Streaming - Agent Docs - Angular Agent Framework', + 'Streaming - Agent Docs - Agent UI for Angular', ); await expect(page.locator('meta[property="og:url"]')).toHaveAttribute( 'content', @@ -125,7 +125,7 @@ test('docs pages render canonical and social metadata', async ({ page }) => { ); await expect(page.locator('meta[name="twitter:title"]')).toHaveAttribute( 'content', - 'Streaming - Agent Docs - Angular Agent Framework', + 'Streaming - Agent Docs - Agent UI for Angular', ); }); diff --git a/apps/website/emails/email-wrapper.ts b/apps/website/emails/email-wrapper.ts index 5fcb20f05..44c5772e4 100644 --- a/apps/website/emails/email-wrapper.ts +++ b/apps/website/emails/email-wrapper.ts @@ -13,12 +13,12 @@ export function wrapEmail(opts: {
-
🛩️ Angular Agent Framework
+
🛩️ Agent UI for Angular
${opts.body}
-

Angular Agent Framework — Signal-native streaming for LangGraph.

+

Agent UI for Angular — Signal-native streaming for LangGraph.

${opts.showUnsubscribe ? '

Unsubscribe

' : ''}
diff --git a/apps/website/emails/lead-notification.ts b/apps/website/emails/lead-notification.ts index 0be62275f..9a9583256 100644 --- a/apps/website/emails/lead-notification.ts +++ b/apps/website/emails/lead-notification.ts @@ -1,18 +1,19 @@ import { wrapEmail, esc } from './email-wrapper'; interface LeadNotificationProps { - name: string; + name?: string; email: string; - company: string; - message: string; + company?: string; + message?: string; ts: string; } export function leadNotificationHtml({ name, email, company, message, ts }: LeadNotificationProps): string { + const displayName = name && name.length > 0 ? name : 'No name provided'; return wrapEmail({ body: `

New Lead

-

${esc(name)}

+

${esc(displayName)}

${esc(email)}${company ? ` — ${esc(company)}` : ''}

${message ? `

${esc(message)}

` : ''}
diff --git a/apps/website/emails/newsletter-welcome.ts b/apps/website/emails/newsletter-welcome.ts index 08c7624ac..bd87a1cd7 100644 --- a/apps/website/emails/newsletter-welcome.ts +++ b/apps/website/emails/newsletter-welcome.ts @@ -3,7 +3,7 @@ import { wrapEmail } from './email-wrapper'; export function newsletterWelcomeHtml(): string { return wrapEmail({ body: ` -

Welcome to Angular Agent Framework updates

+

Welcome to Agent UI for Angular updates

You'll receive updates on new capabilities, production patterns, and Angular agent best practices. We keep it focused and infrequent — no spam.

Explore the Docs `, diff --git a/apps/website/lib/resend.ts b/apps/website/lib/resend.ts index 086b9d8fc..a95e9f2bb 100644 --- a/apps/website/lib/resend.ts +++ b/apps/website/lib/resend.ts @@ -11,7 +11,7 @@ function getResend(): Resend | null { } export const AUDIENCE_ID = process.env.RESEND_AUDIENCE_ID || ''; -export const FROM = process.env.RESEND_FROM || 'Angular Agent Framework '; +export const FROM = process.env.RESEND_FROM || 'Agent UI for Angular '; export const NOTIFY_TO = process.env.RESEND_NOTIFY_TO || 'hello@cacheplane.io'; /** Send an email via Resend. No-ops when API key is missing. */ diff --git a/apps/website/public/AGENTS.md b/apps/website/public/AGENTS.md index e84f1c29a..b42a35792 100644 --- a/apps/website/public/AGENTS.md +++ b/apps/website/public/AGENTS.md @@ -1,4 +1,4 @@ -# Angular Agent Framework v0.0.32 +# Agent UI for Angular v0.0.32 Agent UI primitives and LangGraph streaming adapters for Angular. diff --git a/apps/website/public/CLAUDE.md b/apps/website/public/CLAUDE.md index e84f1c29a..b42a35792 100644 --- a/apps/website/public/CLAUDE.md +++ b/apps/website/public/CLAUDE.md @@ -1,4 +1,4 @@ -# Angular Agent Framework v0.0.32 +# Agent UI for Angular v0.0.32 Agent UI primitives and LangGraph streaming adapters for Angular. diff --git a/apps/website/public/assets/hero.svg b/apps/website/public/assets/hero.svg index 9f5cc199a..b64ae1feb 100644 --- a/apps/website/public/assets/hero.svg +++ b/apps/website/public/assets/hero.svg @@ -24,7 +24,7 @@ font-style="italic" fill="#8B96C8" opacity="0.9" - >The Angular Agent Framework for LangGraph + >Agent UI for Angular — for LangGraph diff --git a/apps/website/scripts/generate-narrative-docs.ts b/apps/website/scripts/generate-narrative-docs.ts index f22479a98..60be5c204 100644 --- a/apps/website/scripts/generate-narrative-docs.ts +++ b/apps/website/scripts/generate-narrative-docs.ts @@ -10,7 +10,7 @@ const API_DOCS_ROOT = 'apps/website/content/docs'; const TOPICS = [ { slug: 'introduction', - prompt: 'Write an introduction to the Angular Agent Framework library. Explain what it does, who it is for, and why it exists. Include a minimal getting-started example.', + prompt: 'Write an introduction to the Agent UI for Angular library. Explain what it does, who it is for, and why it exists. Include a minimal getting-started example.', }, { slug: 'streaming', @@ -36,7 +36,7 @@ async function generateDoc(slug: string, prompt: string, apiDocsJson: string): P max_tokens: 2048, messages: [{ role: 'user', - content: `You are writing documentation for the Angular Agent Framework library. + content: `You are writing documentation for the Agent UI for Angular library. Here is the TypeDoc API reference JSON:\n\n${apiDocsJson}\n\n${prompt} Write clean, developer-friendly MDX documentation. Use precise, no-fluff prose. Include code examples. Start with a single # heading.`, diff --git a/apps/website/scripts/generate-whitepaper.ts b/apps/website/scripts/generate-whitepaper.ts index 01deec9db..10876c695 100644 --- a/apps/website/scripts/generate-whitepaper.ts +++ b/apps/website/scripts/generate-whitepaper.ts @@ -11,7 +11,7 @@ if (!process.env['ANTHROPIC_API_KEY'] && loadEnvFile && fs.existsSync('.env')) { const client = new Anthropic(); const MODEL = process.env['ANTHROPIC_MODEL'] ?? 'claude-opus-4-5'; -const CURRENT_API_CONTEXT = `You are writing public technical whitepapers for Cacheplane Angular Agent Framework. +const CURRENT_API_CONTEXT = `You are writing public technical whitepapers for Cacheplane Agent UI for Angular. Use only the current API surface: - Package names are @ngaf/langgraph, @ngaf/render, @ngaf/chat, and @ngaf/ag-ui. diff --git a/apps/website/src/app/api/email-preview/route.ts b/apps/website/src/app/api/email-preview/route.ts index bae3cc39a..64a6b1847 100644 --- a/apps/website/src/app/api/email-preview/route.ts +++ b/apps/website/src/app/api/email-preview/route.ts @@ -16,7 +16,7 @@ const TEMPLATES: Record { subject: string; html: string }> = { html: whitepaperDownloadHtml('Brian'), }), 'newsletter-welcome': () => ({ - subject: 'Welcome to Angular Agent Framework updates', + subject: 'Welcome to Agent UI for Angular updates', html: newsletterWelcomeHtml(), }), 'lead-notification': () => ({ diff --git a/apps/website/src/app/api/leads/route.ts b/apps/website/src/app/api/leads/route.ts index b2cab92de..27a905593 100644 --- a/apps/website/src/app/api/leads/route.ts +++ b/apps/website/src/app/api/leads/route.ts @@ -19,8 +19,8 @@ export async function POST(req: NextRequest) { const company = sanitize(body.company, 200); const message = sanitize(body.message, 2000); - if (!name || !email) { - return NextResponse.json({ error: 'name and email required' }, { status: 400 }); + if (!email) { + return NextResponse.json({ error: 'email required' }, { status: 400 }); } const ts = new Date().toISOString(); @@ -40,7 +40,7 @@ export async function POST(req: NextRequest) { sendEmail({ from: FROM, to: NOTIFY_TO, - subject: `New lead: ${name}${company ? ` at ${company}` : ''}`, + subject: `New lead: ${name || email}${company ? ` at ${company}` : ''}`, html: leadNotificationHtml({ name, email, company, message, ts }), }), addToAudience(email, name), diff --git a/apps/website/src/app/api/newsletter/route.ts b/apps/website/src/app/api/newsletter/route.ts index cdc085bb3..f733fa696 100644 --- a/apps/website/src/app/api/newsletter/route.ts +++ b/apps/website/src/app/api/newsletter/route.ts @@ -26,7 +26,7 @@ export async function POST(req: NextRequest) { sendEmail({ from: FROM, to: email, - subject: 'Welcome to Angular Agent Framework updates', + subject: 'Welcome to Agent UI for Angular updates', html: newsletterWelcomeHtml(), }), addToAudience(email), diff --git a/apps/website/src/app/contact/page.tsx b/apps/website/src/app/contact/page.tsx new file mode 100644 index 000000000..d036c8c10 --- /dev/null +++ b/apps/website/src/app/contact/page.tsx @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT +import type { Metadata } from 'next'; +import React, { Suspense } from 'react'; +import { tokens } from '@ngaf/design-tokens'; +import { Container } from '../../components/ui/Container'; +import { Section } from '../../components/ui/Section'; +import { Eyebrow } from '../../components/ui/Eyebrow'; +import { ContactForm } from '../../components/contact/ContactForm'; +import { GitHubStarsPill } from '../../components/contact/GitHubStarsPill'; +import { SlaCard } from '../../components/contact/SlaCard'; +import { AltChannelRow } from '../../components/contact/AltChannelRow'; + +export const metadata: Metadata = { + title: 'Talk to an engineer — Cacheplane', + description: + "Tell us what you're shipping. We'll reply within one business day — usually with code, not a calendar invite.", + openGraph: { + title: 'Talk to an engineer — Cacheplane', + description: "Tell us what you're shipping. We'll reply within one business day.", + type: 'website', + }, +}; + +export default function ContactPage() { + return ( +
+ +
+ Contact +

+ Talk to an engineer. +

+

+ Tell us what you're shipping. We'll reply within one business day — usually with code, not a calendar invite. +

+
+ +
+ + + +
+ + +
+
+
+
+ ); +} diff --git a/apps/website/src/app/docs/[library]/[section]/[slug]/page.tsx b/apps/website/src/app/docs/[library]/[section]/[slug]/page.tsx index 960903cf5..5a0a9e5f0 100644 --- a/apps/website/src/app/docs/[library]/[section]/[slug]/page.tsx +++ b/apps/website/src/app/docs/[library]/[section]/[slug]/page.tsx @@ -45,8 +45,8 @@ export function generateStaticParams() { export async function generateMetadata({ params }: DocsRouteProps): Promise { const { library, section, slug } = await params; return getDocMetadata(library, section, slug) ?? { - title: 'Docs - Angular Agent Framework', - description: 'Angular Agent Framework documentation', + title: 'Docs - Agent UI for Angular', + description: 'Agent UI for Angular documentation', }; } diff --git a/apps/website/src/app/docs/page.tsx b/apps/website/src/app/docs/page.tsx index 83d2f87c4..c3d878a67 100644 --- a/apps/website/src/app/docs/page.tsx +++ b/apps/website/src/app/docs/page.tsx @@ -9,8 +9,8 @@ import { docsConfig } from '../../lib/docs-config'; import { createPageMetadata } from '../../lib/site-metadata'; export const metadata = createPageMetadata({ - title: 'Documentation — Angular Agent Framework', - description: 'Learn the framework. Library guides, API reference, and production patterns for Angular Agent Framework.', + title: 'Documentation — Agent UI for Angular', + description: 'Learn the framework. Library guides, API reference, and production patterns for Agent UI for Angular.', pathname: '/docs', type: 'website', }); @@ -72,7 +72,7 @@ export default function DocsLandingPage() { maxWidth: '52ch', }} > - Angular Agent Framework is a suite of MIT-licensed libraries for building AI agent interfaces. Pick a library to get started. + Agent UI for Angular is a suite of MIT-licensed libraries for building AI agent interfaces. Pick a library to get started.

diff --git a/apps/website/src/app/layout.tsx b/apps/website/src/app/layout.tsx index 67fd26f71..17321ba24 100644 --- a/apps/website/src/app/layout.tsx +++ b/apps/website/src/app/layout.tsx @@ -25,13 +25,13 @@ const mono = JetBrains_Mono({ export const metadata: Metadata = { metadataBase: new URL(SITE_ORIGIN), - title: 'Angular Agent Framework — Signal-Native Streaming for Angular + LangGraph', + title: 'Agent UI for Angular — Signal-Native Streaming for Angular + LangGraph', description: 'The enterprise Angular agent framework for LangChain. Signal-native streaming, thread persistence, and production patterns for Angular 20+.', icons: { icon: 'data:image/svg+xml,🛩️', }, openGraph: { - title: 'Angular Agent Framework', + title: 'Agent UI for Angular', description: 'Signal-native streaming for LangGraph — production patterns your Angular team can own.', type: 'website', siteName: SITE_NAME, @@ -40,7 +40,7 @@ export const metadata: Metadata = { }, twitter: { card: 'summary_large_image', - title: 'Angular Agent Framework', + title: 'Agent UI for Angular', description: 'Signal-native streaming for LangGraph — production patterns your Angular team can own.', images: [DEFAULT_SOCIAL_IMAGE], }, diff --git a/apps/website/src/app/llms-full.txt/route.ts b/apps/website/src/app/llms-full.txt/route.ts index 5031170c3..1dd2d4655 100644 --- a/apps/website/src/app/llms-full.txt/route.ts +++ b/apps/website/src/app/llms-full.txt/route.ts @@ -48,7 +48,7 @@ function loadAllPrompts(): string { export async function GET() { const sections = [ - '# Angular Agent Framework — Full Reference\n\nSee /llms.txt for a compact summary.\n', + '# Agent UI for Angular — Full Reference\n\nSee /llms.txt for a compact summary.\n', '## API Reference (TypeDoc)\n\n' + loadApiDocs(), '## Prompt Recipes\n\n' + loadAllPrompts(), [ diff --git a/apps/website/src/app/llms.txt/route.ts b/apps/website/src/app/llms.txt/route.ts index acb3c6cb6..8f4bbbf06 100644 --- a/apps/website/src/app/llms.txt/route.ts +++ b/apps/website/src/app/llms.txt/route.ts @@ -16,9 +16,9 @@ function loadVersion(): string { function buildLlmsTxt(): string { const version = loadVersion(); return [ - `# Angular Agent Framework v${version}`, + `# Agent UI for Angular v${version}`, '', - "Angular Agent Framework is a runtime-neutral chat UI SDK for Angular. The @ngaf/chat library provides streaming chat primitives bound to a runtime-neutral 'Agent' contract; runtime adapters (@ngaf/langgraph, @ngaf/ag-ui) translate between the contract and the actual backend.", + "Agent UI for Angular is a runtime-neutral chat UI SDK for Angular. The @ngaf/chat library provides streaming chat primitives bound to a runtime-neutral 'Agent' contract; runtime adapters (@ngaf/langgraph, @ngaf/ag-ui) translate between the contract and the actual backend.", '', '## Packages', '- @ngaf/chat — chat UI primitives (messages, input, tool calls, interrupt, debug, etc.) consuming the Agent contract', diff --git a/apps/website/src/app/not-found.tsx b/apps/website/src/app/not-found.tsx index 849f79bf0..86fd6774b 100644 --- a/apps/website/src/app/not-found.tsx +++ b/apps/website/src/app/not-found.tsx @@ -5,7 +5,7 @@ import { Eyebrow } from '../components/ui/Eyebrow'; import { Button } from '../components/ui/Button'; export const metadata = { - title: 'Page not found — Angular Agent Framework', + title: 'Page not found — Agent UI for Angular', description: 'The page you were looking for doesn’t exist.', }; diff --git a/apps/website/src/app/opengraph-image.tsx b/apps/website/src/app/opengraph-image.tsx index 0566e8060..12b95d05b 100644 --- a/apps/website/src/app/opengraph-image.tsx +++ b/apps/website/src/app/opengraph-image.tsx @@ -9,7 +9,7 @@ import { ImageResponse } from 'next/og'; // Node runtime (not edge) so we can read the bundled Garamond TTF off disk. export const runtime = 'nodejs'; -export const alt = 'Angular Agent Framework — Signal-native streaming for Angular + LangGraph'; +export const alt = 'Agent UI for Angular — Signal-native streaming for Angular + LangGraph'; export const size = { width: 1200, height: 630 }; export const contentType = 'image/png'; @@ -98,7 +98,7 @@ export default async function OpenGraphImage() { marginBottom: 28, }} > - Angular Agent Framework · MIT + Agent UI for Angular · MIT
{/* Headline — EB Garamond serif matches marketing-site h1 */} diff --git a/apps/website/src/app/page.tsx b/apps/website/src/app/page.tsx index dcab018d7..ce911effc 100644 --- a/apps/website/src/app/page.tsx +++ b/apps/website/src/app/page.tsx @@ -61,7 +61,7 @@ export default async function HomePage() { body="Server-emitted JSON specs become Angular components you already own. Vercel json-render and Google A2UI both supported, with per-component fallback and a readiness gate." bullets={[ 'Per-component fallback API + readiness gate', - 'A2UI v1 protocol + Vercel json-render adapter', + 'A2UI v0.9-compatible protocol + Vercel json-render adapter', 'Renders into your existing component library', 'Server-side schema, client-side trust', ]} diff --git a/apps/website/src/app/pilot-to-prod/page.tsx b/apps/website/src/app/pilot-to-prod/page.tsx index f7c1db928..d62a364c7 100644 --- a/apps/website/src/app/pilot-to-prod/page.tsx +++ b/apps/website/src/app/pilot-to-prod/page.tsx @@ -12,7 +12,7 @@ import { Promises } from '../../components/landing/Promises'; import { FinalCTA } from '../../components/landing/FinalCTA'; export const metadata = { - title: 'Pilot to Production — Angular Agent Framework', + title: 'Pilot to Production — Agent UI for Angular', description: 'Close the last-mile gap. The 3-month pilot engagement is included with every app deployment license. We work alongside your Angular team to ship your first agent to production.', }; diff --git a/apps/website/src/app/pricing/page.tsx b/apps/website/src/app/pricing/page.tsx index 8a4893010..e26cccb8e 100644 --- a/apps/website/src/app/pricing/page.tsx +++ b/apps/website/src/app/pricing/page.tsx @@ -4,11 +4,12 @@ import { Section } from '../../components/ui/Section'; import { Eyebrow } from '../../components/ui/Eyebrow'; import { PricingGrid } from '../../components/pricing/PricingGrid'; import { CompareTable } from '../../components/pricing/CompareTable'; +import { CompatibilityMatrix } from '../../components/pricing/CompatibilityMatrix'; import { LeadForm } from '../../components/pricing/LeadForm'; import { FinalCTA } from '../../components/landing/FinalCTA'; export const metadata = { - title: 'Pricing — Angular Agent Framework', + title: 'Pricing — Agent UI for Angular', description: 'Simple, transparent pricing. MIT-licensed libraries are free forever. Enterprise contracts available.', }; @@ -50,6 +51,33 @@ export default function PricingPage() { +
+ + Compatibility +

+ Angular version support +

+

+ We ship against the versions our CI tests. Other versions may work but aren't guaranteed. +

+ +
+
diff --git a/apps/website/src/app/render/page.tsx b/apps/website/src/app/render/page.tsx index 330ae4db0..b4f1b8d7c 100644 --- a/apps/website/src/app/render/page.tsx +++ b/apps/website/src/app/render/page.tsx @@ -57,7 +57,7 @@ export default async function RenderPage() {
MIT Vercel json-render - Google A2UI v1 + Google A2UI v0.9-compatible
@@ -70,13 +70,13 @@ export default async function RenderPage() { body="The agent emits structured UI as JSON. @ngaf/render maps each spec node to one of your Angular components — so the design system stays yours, and the agent gets to assemble it." bullets={[ 'Vercel json-render adapter', - 'Google A2UI v1 protocol', + 'Google A2UI v0.9-compatible protocol', 'Component registry — declare once, use everywhere', 'Server schema, client validation', ]} supportingCards={[ { title: 'json-render', description: 'Vercel adapter.' }, - { title: 'A2UI v1', description: 'Google protocol.' }, + { title: 'A2UI v0.9-compatible', description: 'Google protocol.' }, { title: 'registry', description: 'Spec → component.' }, ]} cta={{ label: 'See @ngaf/render docs', href: '/docs/render/getting-started/introduction' }} diff --git a/apps/website/src/app/solutions/page.tsx b/apps/website/src/app/solutions/page.tsx index 486281a22..fa71b130d 100644 --- a/apps/website/src/app/solutions/page.tsx +++ b/apps/website/src/app/solutions/page.tsx @@ -10,8 +10,8 @@ import { FinalCTA } from '../../components/landing/FinalCTA'; import { SOLUTIONS } from '../../lib/solutions-data'; export const metadata = { - title: 'Solutions — Angular Agent Framework', - description: 'See how Angular Agent Framework solves enterprise challenges — compliance, analytics, and customer support.', + title: 'Solutions — Agent UI for Angular', + description: 'See how Agent UI for Angular solves enterprise challenges — compliance, analytics, and customer support.', }; export default function SolutionsIndexPage() { diff --git a/apps/website/src/components/contact/AltChannelRow.tsx b/apps/website/src/components/contact/AltChannelRow.tsx new file mode 100644 index 000000000..fae491942 --- /dev/null +++ b/apps/website/src/components/contact/AltChannelRow.tsx @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +import React from 'react'; +import { tokens } from '@ngaf/design-tokens'; + +const linkStyle: React.CSSProperties = { + color: tokens.colors.accent, + textDecoration: 'none', + fontSize: tokens.typography.body.size, + fontFamily: tokens.typography.body.family, +}; + +export function AltChannelRow() { + return ( +

+ docs + · + GitHub issues + · + Discord +

+ ); +} diff --git a/apps/website/src/components/contact/ContactForm.spec.tsx b/apps/website/src/components/contact/ContactForm.spec.tsx new file mode 100644 index 000000000..a8e4a949c --- /dev/null +++ b/apps/website/src/components/contact/ContactForm.spec.tsx @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: MIT +// @vitest-environment jsdom +import React from 'react'; +import { describe, expect, it, vi, beforeEach } from 'vitest'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; + +const trackMock = vi.hoisted(() => vi.fn()); +const fetchMock = vi.hoisted(() => vi.fn()); + +vi.mock('../../lib/analytics/client', () => ({ track: trackMock })); +vi.mock('next/navigation', () => ({ + useSearchParams: () => new URLSearchParams('?source=home_hero&track=enterprise'), +})); +vi.mock('../ui/Button', () => ({ + Button: ({ + children, + type, + disabled, + onClick, + }: { + children: React.ReactNode; + type?: 'submit' | 'button' | 'reset'; + disabled?: boolean; + onClick?: () => void; + }) => ( + + ), +})); + +beforeEach(() => { + trackMock.mockClear(); + fetchMock.mockReset(); + vi.stubGlobal('fetch', fetchMock); + Object.defineProperty(document, 'referrer', { + value: 'https://cacheplane.ai/pricing', + configurable: true, + }); +}); + +describe('ContactForm', () => { + it('submits with email only and fires lead_form_submit + lead_form_success', async () => { + fetchMock.mockResolvedValue({ ok: true }); + const { ContactForm } = await import('./ContactForm'); + render(); + + fireEvent.change(screen.getByLabelText(/email/i), { + target: { value: 'jane@acme.com' }, + }); + fireEvent.click(screen.getByRole('button', { name: /send/i })); + + await waitFor(() => expect(fetchMock).toHaveBeenCalled()); + const body = JSON.parse(fetchMock.mock.calls[0][1].body); + expect(body.email).toBe('jane@acme.com'); + expect(body.source_page).toBe('home_hero'); + expect(body.track).toBe('enterprise'); + expect(body.referrer_host).toBe('cacheplane.ai'); + + expect(trackMock).toHaveBeenCalledWith( + 'marketing:lead_form_submit', + expect.objectContaining({ surface: 'contact' }), + ); + expect(trackMock).toHaveBeenCalledWith( + 'marketing:lead_form_success', + expect.objectContaining({ surface: 'contact' }), + ); + }); + + it('submits with all optional fields populated', async () => { + fetchMock.mockResolvedValue({ ok: true }); + const { ContactForm } = await import('./ContactForm'); + render(); + + fireEvent.change(screen.getByLabelText(/email/i), { target: { value: 'jane@acme.com' } }); + fireEvent.change(screen.getByLabelText(/name/i), { target: { value: 'Jane Smith' } }); + fireEvent.change(screen.getByLabelText(/company/i), { target: { value: 'Acme' } }); + fireEvent.change(screen.getByLabelText(/message/i), { target: { value: 'Hi' } }); + fireEvent.click(screen.getByRole('button', { name: /send/i })); + + await waitFor(() => expect(fetchMock).toHaveBeenCalled()); + const body = JSON.parse(fetchMock.mock.calls[0][1].body); + expect(body).toMatchObject({ + email: 'jane@acme.com', + name: 'Jane Smith', + company: 'Acme', + message: 'Hi', + }); + }); + + it('fires lead_form_fail on non-2xx', async () => { + fetchMock.mockResolvedValue({ ok: false, status: 500 }); + const { ContactForm } = await import('./ContactForm'); + render(); + + fireEvent.change(screen.getByLabelText(/email/i), { target: { value: 'jane@acme.com' } }); + fireEvent.click(screen.getByRole('button', { name: /send/i })); + + await waitFor(() => + expect(trackMock).toHaveBeenCalledWith( + 'marketing:lead_form_fail', + expect.objectContaining({ surface: 'contact' }), + ), + ); + }); +}); diff --git a/apps/website/src/components/contact/ContactForm.tsx b/apps/website/src/components/contact/ContactForm.tsx new file mode 100644 index 000000000..0b0bb2dd4 --- /dev/null +++ b/apps/website/src/components/contact/ContactForm.tsx @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: MIT +'use client'; + +import React, { useState } from 'react'; +import { useSearchParams } from 'next/navigation'; +import { tokens } from '@ngaf/design-tokens'; +import { Button } from '../ui/Button'; +import { track } from '../../lib/analytics/client'; +import { analyticsEvents } from '../../lib/analytics/events'; + +type Status = 'idle' | 'sending' | 'sent' | 'error'; + +function sanitizeReferrerHost(): string | undefined { + if (typeof document === 'undefined' || !document.referrer) return undefined; + try { + return new URL(document.referrer).hostname; + } catch { + return undefined; + } +} + +export function ContactForm() { + const params = useSearchParams(); + const [status, setStatus] = useState('idle'); + const [email, setEmail] = useState(''); + const [name, setName] = useState(''); + const [company, setCompany] = useState(''); + const [message, setMessage] = useState(''); + + const sourcePage = params.get('source') ?? 'contact_direct'; + const trackParam = (params.get('track') ?? 'enterprise') as string; + const ctaId = params.get('cta_id') ?? undefined; + const paper = params.get('paper') ?? undefined; + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + if (!email) return; + setStatus('sending'); + track(analyticsEvents.marketingLeadFormSubmit, { + surface: 'contact', + source_section: 'contact-form', + }); + try { + const res = await fetch('/api/leads', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email, + name: name || undefined, + company: company || undefined, + message: message || undefined, + source_page: sourcePage, + track: trackParam, + cta_id: ctaId, + paper, + referrer_host: sanitizeReferrerHost(), + }), + }); + if (res.ok) { + track(analyticsEvents.marketingLeadFormSuccess, { + surface: 'contact', + source_section: 'contact-form', + }); + setStatus('sent'); + } else { + track(analyticsEvents.marketingLeadFormFail, { + surface: 'contact', + source_section: 'contact-form', + error_reason: 'api_error', + }); + setStatus('error'); + } + } catch { + track(analyticsEvents.marketingLeadFormFail, { + surface: 'contact', + source_section: 'contact-form', + error_reason: 'network_error', + }); + setStatus('error'); + } + } + + if (status === 'sent') { + return ( +
+ Thanks. We'll be in touch within one business day. +
+ ); + } + + const inputStyle: React.CSSProperties = { + display: 'block', + width: '100%', + padding: '10px 12px', + fontSize: tokens.typography.body.size, + fontFamily: tokens.typography.body.family, + color: tokens.colors.textPrimary, + background: tokens.surfaces.surface, + border: `1px solid ${tokens.surfaces.border}`, + borderRadius: 6, + marginTop: 4, + }; + + return ( +
+ + + +