diff --git a/.gitignore b/.gitignore index 6d431405f..21a866348 100644 --- a/.gitignore +++ b/.gitignore @@ -28,12 +28,5 @@ apps/website/public/demo/ out .vercel -# LangGraph -.langgraph_api/ -langgraph-combined.json - -# Playwright -test-results/ - -# Deploy output -deploy/ +# Whitepaper signup data +apps/website/data/ diff --git a/apps/website/content/AGENTS.md.template b/apps/website/content/AGENTS.md.template index 651fd245c..41817dde0 100644 --- a/apps/website/content/AGENTS.md.template +++ b/apps/website/content/AGENTS.md.template @@ -1,6 +1,6 @@ # stream-resource v@VERSION@ -Angular streaming library for LangChain/LangGraph. Provides `streamResource()` — full parity with React's `useStream()`. +Angular streaming library for LangChain/LangGraph. Provides `streamResource()` — Signal-native streaming for Angular agents, built for LangGraph. ## Install npm install stream-resource diff --git a/apps/website/content/CLAUDE.md.template b/apps/website/content/CLAUDE.md.template index 651fd245c..41817dde0 100644 --- a/apps/website/content/CLAUDE.md.template +++ b/apps/website/content/CLAUDE.md.template @@ -1,6 +1,6 @@ # stream-resource v@VERSION@ -Angular streaming library for LangChain/LangGraph. Provides `streamResource()` — full parity with React's `useStream()`. +Angular streaming library for LangChain/LangGraph. Provides `streamResource()` — Signal-native streaming for Angular agents, built for LangGraph. ## Install npm install stream-resource diff --git a/apps/website/content/docs-v2/getting-started/introduction.mdx b/apps/website/content/docs-v2/getting-started/introduction.mdx index 574c57b4d..e1c35bc0d 100644 --- a/apps/website/content/docs-v2/getting-started/introduction.mdx +++ b/apps/website/content/docs-v2/getting-started/introduction.mdx @@ -1,6 +1,6 @@ # Introduction -StreamResource brings full parity with React's `useStream()` hook to Angular 20+. Build streaming AI applications with Angular Signals, connect to LangGraph agents, and ship production-ready frontends for your AI products. +StreamResource is the Signal-native streaming library for Angular 20+ — built natively for LangGraph, without React translation layers. Build streaming AI applications with Angular Signals, connect to LangGraph agents, and ship production-ready frontends for your AI products. This guide walks you through the complete workflow: build a LangGraph agent in Python, run it locally, connect it to an Angular app with streamResource(), and deploy to production. diff --git a/apps/website/e2e/website.spec.ts b/apps/website/e2e/website.spec.ts index e1bf0592e..870ebec25 100644 --- a/apps/website/e2e/website.spec.ts +++ b/apps/website/e2e/website.spec.ts @@ -12,10 +12,9 @@ test('landing page renders architecture section', async ({ page }) => { await expect(page.getByText('Architecture').first()).toBeVisible(); }); -test('landing page renders 6 feature cards', async ({ page }) => { +test('landing page renders fair comparison section', async ({ page }) => { await page.goto('/'); - const featureSection = page.locator('section').filter({ hasText: 'Features' }); - await expect(featureSection).toBeVisible(); + await expect(page.getByText('What Angular Stream Resource adds').first()).toBeVisible(); }); test('pricing page shows 4 plan cards', async ({ page }) => { diff --git a/apps/website/public/whitepaper-preview.html b/apps/website/public/whitepaper-preview.html new file mode 100644 index 000000000..4a840fd55 --- /dev/null +++ b/apps/website/public/whitepaper-preview.html @@ -0,0 +1,167 @@ + + + + + + + + + + + +
+
StreamResource · Production Readiness Guide
+

From Prototype
to Production

+

The Angular Agent Readiness Guide

+
cacheplane.io · 2026
+
⚠ PREVIEW — placeholder content. Set ANTHROPIC_API_KEY and run npm run generate-whitepaper for real content.
+
+ + +
+

Contents

+ +
+ 01 + Streaming State Management +
+
+ 02 + Thread Persistence +
+
+ 03 + Tool-Call Rendering +
+
+ 04 + Human Approval Flows +
+
+ 05 + Generative UI +
+
+ 06 + Deterministic Testing +
+
+ + + +
+
Chapter 1
+

Streaming State Management

+

Overview

+

When you move from prototype to production, the requirements change fundamentally. What worked in a demo — direct API calls, synchronous state, manual zone management — falls apart at scale.

+

The Signals-Native Approach

+

StreamResource provides a signals-native approach that eliminates the boilerplate:

+
@Component({...})
+export class ChatComponent {
+  chat = streamResource<{ messages: BaseMessage[] }>({
+    assistantId: 'chat_agent',
+  });
+}
+
+

Production Checklist

+
  • Are your message signals OnPush-compatible?
  • +
  • Is isStreaming() driving your loading UI without polling?
  • +
  • Are you avoiding manual zone patching?
+
+
+
Chapter 2
+

Thread Persistence

+

Overview

+

Demos work with ephemeral state. Production agents need conversation history that survives page refreshes, tab switches, and navigation — wired to LangGraph's MemorySaver backend.

+

The threadId Pattern

+
provideStreamResource({
+  apiUrl: 'http://localhost:2024',
+  threadId: signal(localStorage.getItem('threadId')),
+  onThreadId: (id) => localStorage.setItem('threadId', id),
+})
+
+

Production Checklist

+
  • Does your agent UI resume threads correctly after a browser refresh?
  • +
  • Can users switch between conversations?
  • +
  • Is thread state scoped correctly per user?
+
+
+
Chapter 3
+

Tool-Call Rendering

+

Overview

+

LangGraph agents invoke tools mid-stream. The UI needs to show tool execution state in real time — steps appearing as the tool runs, a final result, and collapsible history.

+

Progressive Disclosure

+

+
+
+

Production Checklist

+
  • Do your tool call cards handle partial step state during streaming?
  • +
  • Is tool history collapsible after completion?
  • +
  • Can you distinguish pending vs. completed tool calls?
+
+
+
Chapter 4
+

Human Approval Flows

+

Overview

+

Production agents that take consequential actions must pause for human approval before proceeding. This requires a tight loop between LangGraph's interrupt() primitive and Angular UI.

+

The Interrupt Pattern

+
// In your component
+approved = computed(() => this.chat.interrupt()?.approved ?? false);
+onApprove() {
+  this.chat.submit(null, { command: { resume: true } });
+}
+
+

Production Checklist

+
  • Can your agent UI recover gracefully if a user cancels an interrupt?
  • +
  • Are approve/edit/cancel actions clearly mapped to resume commands?
  • +
  • Is interrupt state persisted across refreshes?
+
+
+
Chapter 5
+

Generative UI

+

Overview

+

The most advanced production agents emit structured UI specs — not just text. A data analysis agent might render a live table. A booking agent might render a reservation form.

+

The Registry Pattern

+
defineAngularRegistry({
+  'data-table': DataTableComponent,
+  'booking-form': BookingFormComponent,
+  'chart-widget': ChartWidgetComponent,
+});
+
+

Production Checklist

+
  • Can your agent emit UI components without tight coupling to the frontend codebase?
  • +
  • Does JSON patch streaming enable progressive UI updates?
  • +
  • Is your component registry decoupled from agent logic?
+
+
+
Chapter 6
+

Deterministic Testing

+

Overview

+

Agent UIs are notoriously hard to test because they depend on live LLM responses. Flaky tests, slow CI, and inability to reproduce edge cases are the main reasons agent UIs ship with low confidence.

+

The MockStreamTransport Approach

+
const ref = createMockStreamResourceRef();
+ref.messages.set([{ role: 'assistant', content: 'Hello' }]);
+ref.isStreaming.set(false);
+expect(fixture.nativeElement.querySelector('.message').textContent)
+  .toBe('Hello');
+
+

Production Checklist

+
  • Do your agent component tests run offline and complete in under 100ms each?
  • +
  • Are streaming, interrupts, tool calls, and generative UI all tested in isolation?
  • +
  • Is MockStreamTransport used instead of mocking streamResource() itself?
+
+ + + \ No newline at end of file diff --git a/apps/website/public/whitepaper.pdf b/apps/website/public/whitepaper.pdf new file mode 100644 index 000000000..69bdf349b Binary files /dev/null and b/apps/website/public/whitepaper.pdf differ diff --git a/apps/website/scripts/generate-whitepaper.ts b/apps/website/scripts/generate-whitepaper.ts new file mode 100644 index 000000000..07abe0ed8 --- /dev/null +++ b/apps/website/scripts/generate-whitepaper.ts @@ -0,0 +1,262 @@ +import Anthropic from '@anthropic-ai/sdk'; +import fs from 'fs'; +import path from 'path'; +import puppeteer from 'puppeteer'; + +const client = new Anthropic(); +const MODEL = process.env['ANTHROPIC_MODEL'] ?? 'claude-opus-4-5'; +const OUTPUT_PDF = 'apps/website/public/whitepaper.pdf'; +const OUTPUT_HTML = 'apps/website/public/whitepaper-preview.html'; + +// ── Chapter definitions ────────────────────────────────────────────────── +const CHAPTERS = [ + { + id: 'streaming-state', + title: 'Streaming State Management', + prompt: `Write a 400-600 word chapter for an engineering white paper titled "From Prototype to Production: The Angular Agent Readiness Guide". + +Chapter topic: Streaming State Management + +Context: Angular teams building LangGraph-powered agents must wire SSE event streams into reactive UI. Without the right primitives, they end up with custom zone-patching, manual subscription management, and brittle token accumulation logic that breaks under load. + +Cover: +- Why streaming state is hard in Angular (zone.js, change detection, timing) +- The signals-native approach: how streamResource() exposes messages() as Signal +- How isStreaming() lets developers drive loading UI without polling +- Code example: minimal streamResource() setup (TypeScript snippet, 8-12 lines) +- Production checklist item: "Are your message signals OnPush-compatible?" + +Tone: Direct, technical, peer-to-peer. No fluff. Audience is senior Angular engineers.`, + }, + { + id: 'thread-persistence', + title: 'Thread Persistence', + prompt: `Write a 400-600 word chapter for an engineering white paper titled "From Prototype to Production: The Angular Agent Readiness Guide". + +Chapter topic: Thread Persistence + +Context: Demos work with ephemeral state. Production agents need conversation history that survives page refreshes, tab switches, and navigation — wired to LangGraph's MemorySaver backend. + +Cover: +- Why stateless agent UIs fail in production +- The threadId signal and onThreadId callback pattern +- How to persist threadId to localStorage and restore on mount +- Thread list UI and switching between conversations +- Code example: provideStreamResource() with threadId (8-12 lines) +- Production checklist item: "Does your agent UI resume threads correctly after a browser refresh?" + +Tone: Direct, technical, peer-to-peer. No fluff. Audience is senior Angular engineers.`, + }, + { + id: 'tool-call-rendering', + title: 'Tool-Call Rendering', + prompt: `Write a 400-600 word chapter for an engineering white paper titled "From Prototype to Production: The Angular Agent Readiness Guide". + +Chapter topic: Tool-Call Rendering + +Context: LangGraph agents invoke tools mid-stream. The UI needs to show tool execution state in real time — steps appearing as the tool runs, a final result, and collapsible history — without parsing raw SSE events by hand. + +Cover: +- What tool call events look like in the raw stream +- Why hand-parsing is fragile and hard to test +- The headless primitive and prebuilt option +- Progressive disclosure: showing steps live, collapsing on completion +- Code example: binding (8-12 lines of Angular template) +- Production checklist item: "Do your tool call cards handle partial step state during streaming?" + +Tone: Direct, technical, peer-to-peer. No fluff. Audience is senior Angular engineers.`, + }, + { + id: 'human-approval-flows', + title: 'Human Approval Flows', + prompt: `Write a 400-600 word chapter for an engineering white paper titled "From Prototype to Production: The Angular Agent Readiness Guide". + +Chapter topic: Human Approval Flows (Interrupts) + +Context: Production agents that take consequential actions — sending emails, deploying services, modifying data — must pause for human approval before proceeding. This requires a tight loop between LangGraph's interrupt() primitive and Angular UI. + +Cover: +- The LangGraph interrupt() and Command.RESUME pattern +- Why polling and custom websocket approaches are brittle +- The interrupt() signal in streamResource() and how it maps to approval state +- headless and prebuilt +- The three approval actions: approve, edit, cancel — and how each maps to a resume command +- Code example: interrupt signal binding (8-12 lines) +- Production checklist item: "Can your agent UI recover gracefully if a user cancels an interrupt?" + +Tone: Direct, technical, peer-to-peer. No fluff. Audience is senior Angular engineers.`, + }, + { + id: 'generative-ui', + title: 'Generative UI', + prompt: `Write a 400-600 word chapter for an engineering white paper titled "From Prototype to Production: The Angular Agent Readiness Guide". + +Chapter topic: Generative UI + +Context: The most advanced production agents emit structured UI specs — not just text. A data analysis agent might render a live table. A booking agent might render a reservation form. Without a framework for this, teams either hardcode component logic into the agent or skip the feature entirely. + +Cover: +- The onCustomEvent pattern in LangGraph: how agents emit structured data +- The @cacheplane/render approach: json-render specs, defineAngularRegistry(), +- How JSON patch streaming enables progressive UI updates (rows appearing as data arrives) +- The registry pattern: decoupling agent from component implementation +- Code example: defineAngularRegistry() registration (8-12 lines) +- Production checklist item: "Can your agent emit UI components without tight coupling to the frontend codebase?" + +Tone: Direct, technical, peer-to-peer. No fluff. Audience is senior Angular engineers.`, + }, + { + id: 'deterministic-testing', + title: 'Deterministic Testing', + prompt: `Write a 400-600 word chapter for an engineering white paper titled "From Prototype to Production: The Angular Agent Readiness Guide". + +Chapter topic: Deterministic Testing + +Context: Agent UIs are notoriously hard to test because they depend on live LLM responses. Flaky tests, slow CI, and inability to reproduce edge cases are the main reasons agent UIs ship with low confidence. + +Cover: +- Why testing agent components against real LLM APIs is impractical +- The MockStreamTransport approach: scripted event sequences, no server needed +- createMockStreamResourceRef(): writable signals you control directly in tests +- How to test streaming, interrupts, tool calls, and generative UI in isolation +- Code example: createMockStreamResourceRef() test pattern (10-14 lines) +- Production checklist item: "Do your agent component tests run offline and complete in under 100ms each?" + +Tone: Direct, technical, peer-to-peer. No fluff. Audience is senior Angular engineers.`, + }, +]; + +// ── Markdown to HTML converter ─────────────────────────────────────────── +function mdToHTML(md: string): string { + return md + .replace(/```[\w]*\n([\s\S]*?)```/g, '
$1
') + .replace(/^### (.+)$/gm, '

$1

') + .replace(/^## (.+)$/gm, '

$1

') + .replace(/\*\*(.+?)\*\*/g, '$1') + .replace(/^- (.+)$/gm, '
  • $1
  • ') + .replace(/(
  • [^\n]+<\/li>\n?)+/g, match => `
      ${match}
    `) + .split('\n\n') + .map(block => { + if (block.startsWith('${trimmed}

    ` : ''; + }) + .join('\n'); +} + +// ── HTML builder ───────────────────────────────────────────────────────── +function buildHTML(chapters: Array<{ title: string; content: string }>): string { + const tocHTML = chapters.map((ch, i) => ` +
    + ${String(i + 1).padStart(2, '0')} + ${ch.title} +
    `).join(''); + + const chaptersHTML = chapters.map((ch, i) => ` +
    +
    Chapter ${i + 1}
    +

    ${ch.title}

    +
    ${mdToHTML(ch.content)}
    +
    `).join(''); + + return ` + + + + + + + + + + +
    +
    StreamResource · Production Readiness Guide
    +

    From Prototype
    to Production

    +

    The Angular Agent Readiness Guide

    +
    cacheplane.io · ${new Date().getFullYear()}
    +
    + + +
    +

    Contents

    + ${tocHTML} +
    + + +${chaptersHTML} + + +`; +} + +// ── PDF renderer ───────────────────────────────────────────────────────── +async function renderPDF(html: string, outputPath: string): Promise { + console.log(' Launching browser for PDF render...'); + const browser = await puppeteer.launch({ headless: true }); + const page = await browser.newPage(); + await page.setContent(html, { waitUntil: 'networkidle0' }); + await page.pdf({ + path: outputPath, + format: 'A4', + printBackground: true, + margin: { top: '0', right: '0', bottom: '0', left: '0' }, + }); + await browser.close(); +} + +// ── Chapter generator ──────────────────────────────────────────────────── +async function generateChapter(chapter: typeof CHAPTERS[0]): Promise { + console.log(` Generating: ${chapter.title}...`); + const message = await client.messages.create({ + model: MODEL, + max_tokens: 1500, + messages: [{ role: 'user', content: chapter.prompt }], + }); + const content = message.content[0]; + if (content.type !== 'text') throw new Error(`Unexpected content type: ${content.type}`); + return content.text; +} + +// ── Main ───────────────────────────────────────────────────────────────── +async function main() { + console.log('StreamResource White Paper Generator\n'); + console.log(`Model: ${MODEL}`); + console.log(`Output: ${OUTPUT_PDF}\n`); + + const generatedChapters: Array<{ title: string; content: string }> = []; + + for (const chapter of CHAPTERS) { + const content = await generateChapter(chapter); + generatedChapters.push({ title: chapter.title, content }); + } + + console.log('\nBuilding HTML document...'); + const html = buildHTML(generatedChapters); + fs.mkdirSync(path.dirname(OUTPUT_HTML), { recursive: true }); + fs.writeFileSync(OUTPUT_HTML, html, 'utf8'); + console.log(` HTML preview: ${OUTPUT_HTML}`); + + console.log('Rendering PDF...'); + fs.mkdirSync(path.dirname(OUTPUT_PDF), { recursive: true }); + await renderPDF(html, OUTPUT_PDF); + + const stat = fs.statSync(OUTPUT_PDF); + console.log(`\n✓ Done. PDF saved to ${OUTPUT_PDF} (${Math.round(stat.size / 1024)}KB)`); + console.log(`✓ HTML preview: ${OUTPUT_HTML}`); +} + +main().catch(err => { + console.error('Generation failed:', err); + process.exit(1); +}); diff --git a/apps/website/src/app/api/whitepaper-signup/route.ts b/apps/website/src/app/api/whitepaper-signup/route.ts new file mode 100644 index 000000000..46efa9ef3 --- /dev/null +++ b/apps/website/src/app/api/whitepaper-signup/route.ts @@ -0,0 +1,31 @@ +// apps/website/src/app/api/whitepaper-signup/route.ts +import { NextRequest, NextResponse } from 'next/server'; +import fs from 'fs'; +import path from 'path'; + +const SIGNUPS_FILE = path.join(process.cwd(), 'data', 'whitepaper-signups.ndjson'); + +export async function POST(req: NextRequest) { + let body: { name?: string; email?: string }; + try { + body = await req.json(); + } catch { + return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }); + } + + const { name = '', email = '' } = body; + if (!email || !email.includes('@')) { + return NextResponse.json({ error: 'Valid email required' }, { status: 400 }); + } + + const entry = JSON.stringify({ name: name.trim(), email: email.trim(), ts: new Date().toISOString() }) + '\n'; + try { + fs.mkdirSync(path.dirname(SIGNUPS_FILE), { recursive: true }); + fs.appendFileSync(SIGNUPS_FILE, entry, 'utf8'); + } catch (err) { + console.error('Failed to write signup:', err); + return NextResponse.json({ error: 'Internal error' }, { status: 500 }); + } + + return NextResponse.json({ ok: true }); +} diff --git a/apps/website/src/app/layout.tsx b/apps/website/src/app/layout.tsx index e41a7b83f..75cb5eb20 100644 --- a/apps/website/src/app/layout.tsx +++ b/apps/website/src/app/layout.tsx @@ -22,8 +22,8 @@ const mono = JetBrains_Mono({ }); export const metadata: Metadata = { - title: 'Angular Stream Resource — LangChain Streaming for Angular', - description: 'The Enterprise Streaming Resource for LangChain and Angular. streamResource() brings full parity with React useStream() to Angular 20+.', + title: 'Angular Stream Resource — Signal-Native Streaming for Angular + LangGraph', + description: 'The Enterprise Streaming Resource for LangChain and Angular. Signal-native streaming, thread persistence, and production patterns for Angular 20+.', }; export default function RootLayout({ children }: { children: React.ReactNode }) { diff --git a/apps/website/src/app/page.tsx b/apps/website/src/app/page.tsx index 61b194675..fd5feaeab 100644 --- a/apps/website/src/app/page.tsx +++ b/apps/website/src/app/page.tsx @@ -3,39 +3,50 @@ import { ArchDiagram } from '../components/landing/ArchDiagram'; import { ValueProps } from '../components/landing/ValueProps'; import { LangGraphShowcase } from '../components/landing/LangGraphShowcase'; import { DeepAgentsShowcase } from '../components/landing/DeepAgentsShowcase'; -import { FeatureStrip } from '../components/landing/FeatureStrip'; -import { CodeBlock } from '../components/landing/CodeBlock'; -import { CockpitCTA } from '../components/landing/CockpitCTA'; import { StatsStrip } from '../components/landing/StatsStrip'; -import { tokens } from '@cacheplane/design-tokens'; +import { ProblemSection } from '../components/landing/ProblemSection'; +import { FullStackSection } from '../components/landing/FullStackSection'; +import { ChatFeaturesSection } from '../components/landing/ChatFeaturesSection'; +import { FairComparisonSection } from '../components/landing/FairComparisonSection'; +import { WhitePaperSection } from '../components/landing/WhitePaperSection'; +import { HomePilotCTA } from '../components/landing/HomePilotCTA'; +import { tokens } from '../../lib/design-tokens'; export default async function HomePage() { return (
    - {/* Ambient gradient blobs distributed across the long page */} -
    -
    -
    -
    -
    + {/* Ambient gradient blobs */} +