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
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ Want to see the finished result before you build?
</CalloutActions>
</Callout>

<Callout type="info" title="No backend yet?">
[Try without a backend](/docs/chat/getting-started/try-without-a-backend) renders the same `<chat>` with a fake agent — no server, no key.
</Callout>

<Steps>
<Step title="Install the packages">

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Angular 20–22 project with an agent provider configured. See [Agent Installati
</Callout>

<Callout type="info" title="No backend yet?">
The provider steps below assume a running LangGraph server at `http://localhost:2024`. If you don't have one, jump to [Run with no backend](#run-with-no-backend) — `mockAgent()` drives the UI with canned messages so you can see `<chat>` render before wiring a real agent.
[Try without a backend](/docs/chat/getting-started/try-without-a-backend) renders the same `<chat>` with a fake agent — no server, no key. The provider steps below assume a running LangGraph server at `http://localhost:2024`.
</Callout>

<Callout type="info" title="Can I use this commercially?">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
---
title: Try without a backend
description: Render a real Threadplane chat with provideFakeAgent() — no server, no LLM, no account — then swap in a real adapter.
---

# Try without a backend

Render a real `<chat>` in your Angular app with no server, no LLM and no account. `provideFakeAgent()` streams a canned reply in-process through the same components you will ship. When the UI looks right, swap the provider for a real adapter.

<Callout type="info" title="What you need">
An Angular 20–22 application. Nothing else.
</Callout>

<Steps>
<Step title="Install">

```bash
npm install @threadplane/chat @threadplane/langgraph @langchain/core @langchain/langgraph-sdk marked
```

`provideFakeAgent()` ships inside the adapter packages, so install the adapter you expect to use later. The LangChain packages are peers of `@threadplane/langgraph`; `marked` renders assistant markdown.

<Callout type="tip" title="Bundle budget">
A default Angular application caps the initial bundle at 1 MB, and a chat UI plus the LangGraph SDK exceeds it. Raise `budgets[].maximumError` for the `initial` bundle in `angular.json` if `ng build` reports a budget error.
</Callout>

</Step>
<Step title="Provide the fake agent">

```ts
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideFakeAgent } from '@threadplane/langgraph';

export const appConfig: ApplicationConfig = {
providers: [
provideFakeAgent({ tokens: ['Hello', ' from', ' Threadplane.'] }),
],
};
```

`FakeAgentConfig` accepts `tokens`, `reasoningTokens` and `delayMs`. Leave them out and the fake agent streams a default reply.

</Step>
<Step title="Render the chat">

```ts
// app.component.ts
import { Component } from '@angular/core';
import { injectAgent } from '@threadplane/langgraph';
import { ChatComponent } from '@threadplane/chat';

@Component({
selector: 'app-root',
imports: [ChatComponent],
template: `<chat [agent]="agent" />`,
})
export class AppComponent {
protected readonly agent = injectAgent();
}
```

Run `ng serve`, type anything, and watch the reply stream in.

</Step>
<Step title="Test it">

```ts
// app.component.spec.ts
import { TestBed } from '@angular/core/testing';
import { provideFakeAgent } from '@threadplane/langgraph';
import { AppComponent } from './app.component';

it('streams the fake reply', async () => {
TestBed.configureTestingModule({
imports: [AppComponent],
providers: [provideFakeAgent({ tokens: ['Hello', ' from', ' Threadplane.'], delayMs: 0 })],
});
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const textarea = fixture.nativeElement.querySelector('textarea');
textarea.value = 'hi';
textarea.dispatchEvent(new Event('input'));
fixture.detectChanges();
fixture.nativeElement.querySelector('button[aria-label="Send message"]').click();
await fixture.whenStable();
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toContain('Hello from Threadplane.');
});
```

The `detectChanges()` call after the input event matters: the send button stays disabled until change detection runs, so a click before it does nothing.

</Step>
</Steps>

## Connect a real adapter

Replace `provideFakeAgent(...)` with one line and keep every component as it is:

- **LangGraph**: `provideAgent({ apiUrl: 'http://localhost:2024', assistantId: 'agent' })` — [LangGraph quickstart](/docs/langgraph/getting-started/quickstart)
- **AG-UI**: `provideAgent({ url: 'http://localhost:8000/agent' })` from `@threadplane/ag-ui` — [AG-UI quickstart](/docs/ag-ui/getting-started/quickstart)

Not every backend emits every capability; see [Choosing an adapter](/docs/choosing-an-adapter).
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ Build a streaming chat component with `injectAgent()` in 5 minutes.
Angular 20–22 project using a Node.js version supported by that Angular major. If you need setup help, see the [Installation](/docs/langgraph/getting-started/installation) guide.
</Callout>

<Callout type="info" title="No backend yet?">
[Try without a backend](/docs/chat/getting-started/try-without-a-backend) renders the same `<chat>` with a fake agent — no server, no key.
</Callout>

<Steps>
<Step title="Install the package">

Expand Down
38 changes: 38 additions & 0 deletions apps/website/e2e/home-hero.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { test, expect } from '@playwright/test';

test.describe('homepage hero', () => {
test('install dialog opens, is keyboard operable, and copies the visible command', async ({ page, context }) => {
await context.grantPermissions(['clipboard-read', 'clipboard-write']);
await page.goto('/');
await page.getByRole('button', { name: 'Install Threadplane' }).click();
const dialog = page.getByRole('dialog', { name: 'Install Threadplane' });
await expect(dialog).toBeVisible();
await expect(dialog.getByRole('radio', { name: 'Try without a backend' })).toHaveAttribute('aria-checked', 'true');
await dialog.getByRole('radio', { name: 'Try without a backend' }).focus();
await page.keyboard.press('ArrowRight');
await expect(dialog.getByRole('radio', { name: 'LangGraph' })).toHaveAttribute('aria-checked', 'true');
const visible = await dialog.getByTestId('install-command').textContent();
await dialog.getByRole('button', { name: 'Copy install command' }).click();
const copied = await page.evaluate(() => navigator.clipboard.readText());
expect(copied).toBe(visible);
await page.keyboard.press('Escape');
await expect(dialog).toBeHidden();
await expect(page.getByRole('button', { name: 'Install Threadplane' })).toBeFocused();
});

test('poster renders before the frame and the frame mounts on desktop', async ({ page }) => {
await page.goto('/');
const demo = page.locator('[data-hero-demo]');
await expect(demo.locator('img')).toHaveAttribute('src', '/screenshots/hero-walkthrough-poster.webp');
await expect(demo.locator('iframe')).toHaveAttribute('src', 'https://demo.threadplane.ai/hero');
});

test('mobile shows Play walkthrough instead of the frame', async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto('/');
const demo = page.locator('[data-hero-demo]');
await demo.scrollIntoViewIfNeeded();
await expect(demo.getByRole('button', { name: 'Play walkthrough' })).toBeVisible();
await expect(demo.locator('iframe')).toHaveCount(0);
});
});
7 changes: 5 additions & 2 deletions apps/website/e2e/website.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ async function expectNoHorizontalOverflow(
test('landing page renders hero headline', async ({ page }) => {
await page.goto('/');
await expect(page.locator('#hero-heading')).toBeVisible();
await expect(page.locator('#hero-heading')).toHaveText('The AI agent UI framework for Angular.');
const headline = await page.locator('#hero-heading').textContent();
expect(headline?.toLowerCase()).toContain('angular');
});
Expand All @@ -31,11 +32,13 @@ test('landing page renders the dark proof band', async ({ page }) => {
await expect(page.locator('#proof[data-surface="dark"]')).toBeVisible();
});

test('landing page renders feature blocks (Stream/Render/Ship)', async ({ page }) => {
test('landing page renders feature blocks (Stream/Persist/Approve/Render/Test)', async ({ page }) => {
await page.goto('/');
await expect(page.locator('#stream-heading')).toBeVisible();
await expect(page.locator('#persist-heading')).toBeVisible();
await expect(page.locator('#approve-heading')).toBeVisible();
await expect(page.locator('#render-heading')).toBeVisible();
await expect(page.locator('#ship-heading')).toBeVisible();
await expect(page.locator('#test-heading')).toBeVisible();
});

test('landing page no longer carries the retired promises section', async ({ page }) => {
Expand Down
4 changes: 2 additions & 2 deletions apps/website/src/app/opengraph-image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* file in any route folder.
*/
import { ImageResponse } from 'next/og';
import { POSITIONING_PROOF_POINTS, PRIMARY_TAGLINE, SHORT_POSITIONING_DESCRIPTION } from '../lib/positioning';
import { HERO_H1, POSITIONING_PROOF_POINTS, PRIMARY_TAGLINE, SHORT_POSITIONING_DESCRIPTION } from '../lib/positioning';
import { loadCardFonts } from './og-font';

// Node runtime (not edge) so we can read the bundled Garamond TTF off disk.
Expand Down Expand Up @@ -61,7 +61,7 @@ export default async function OpenGraphImage() {
maxWidth: 980,
}}
>
Build fullstack agentic Angular apps.
{HERO_H1}
</div>

{/* Subhead */}
Expand Down
Loading
Loading