diff --git a/apps/website/content/docs-v2/getting-started/introduction.mdx b/apps/website/content/docs-v2/getting-started/introduction.mdx index 3f09cf406..3aeae6115 100644 --- a/apps/website/content/docs-v2/getting-started/introduction.mdx +++ b/apps/website/content/docs-v2/getting-started/introduction.mdx @@ -1,63 +1,329 @@ # Introduction -StreamResource brings full parity with React's `useStream()` hook to Angular 20+. It's the enterprise streaming resource for LangChain and Angular — built natively with Angular Signals, not wrapped or adapted. +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 serves two audiences: **Angular developers** building AI-powered applications, and **AI/agent developers** who need a production Angular frontend for their LangGraph agents. + +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. ## What is streamResource()? -`streamResource()` is an Angular function that creates a reactive connection to a LangGraph agent. It returns an object whose properties are Angular Signals — meaning your templates update automatically as the agent streams responses. +`streamResource()` is an Angular function that creates a reactive, streaming connection to a LangGraph agent. It returns an object whose properties are Angular Signals — meaning your templates update automatically as the agent streams responses, token by token. ```typescript const chat = streamResource<{ messages: BaseMessage[] }>({ assistantId: 'chat_agent', }); -// Every property is a Signal -chat.messages() // Signal -chat.status() // Signal<'idle' | 'loading' | 'resolved' | 'error'> -chat.interrupt() // Signal -chat.history() // Signal +// Every property is a Signal — reactive, synchronous, no subscriptions +chat.messages() // Signal +chat.status() // Signal<'idle' | 'loading' | 'resolved' | 'error'> +chat.error() // Signal +chat.interrupt() // Signal +chat.history() // Signal ``` -## What you can build +No RxJS. No manual subscriptions. No async pipes. Just Signals that work with Angular's `OnPush` change detection out of the box. + +## The Architecture + +StreamResource sits between your Angular app and LangGraph Platform: + + + +Creates a reactive resource bound to a specific agent. All state is exposed as Signals. + + +Sends HTTP POST to LangGraph Platform, receives Server-Sent Events with state updates. + + +Executes nodes, calls tools, manages checkpoints. Streams results back in real-time. + + +As tokens arrive, `messages()` updates. Angular re-renders only the affected components. + + + +## Build Your Agent + +LangGraph agents are Python programs defined as directed graphs. Here's a minimal chat agent using the example from this repository: + + + + +```python +# examples/chat-agent/src/chat_agent/agent.py +from langchain_core.messages import SystemMessage +from langchain_core.runnables import RunnableConfig +from langgraph.graph import END, START, MessagesState, StateGraph +from langchain_openai import ChatOpenAI + +llm = ChatOpenAI(model="gpt-4o-mini") + +def call_model(state: MessagesState, config: RunnableConfig) -> dict: + """Invoke the LLM with the current message history.""" + system_prompt = config.get("configurable", {}).get( + "system_prompt", "You are a helpful assistant." + ) + messages = [SystemMessage(content=system_prompt)] + state["messages"] + response = llm.invoke(messages) + return {"messages": [response]} + +# Build the graph: START -> call_model -> END +builder = StateGraph(MessagesState) +builder.add_node("call_model", call_model) +builder.add_edge(START, "call_model") +builder.add_edge("call_model", END) + +graph = builder.compile() +``` + + + + +```json +{ + "dependencies": ["."], + "graphs": { + "chat_agent": "./src/chat_agent/agent.py:graph" + }, + "env": ".env", + "python_version": "3.12" +} +``` + + + + + +`MessagesState` manages a list of messages. The `call_model` node takes the current messages, adds a system prompt, and calls the LLM. The graph runs this single node and returns the response. LangGraph handles streaming, checkpointing, and thread management automatically. + + +## Run Your Agent Locally - -Token-by-token streaming with real-time UI updates. Messages arrive as they're generated. + + +```bash +pip install -U "langgraph-cli[inmem]" +``` + + + + +Create a `.env` file with your API keys: + +```bash +OPENAI_API_KEY=sk-... +LANGSMITH_API_KEY=lsv2_... +``` + + + + +```bash +cd examples/chat-agent +langgraph dev +``` + +Your agent is now running at `http://localhost:2024`. You can test it in LangGraph Studio at `https://smith.langchain.com/studio/`. + + + + +## Connect with Angular + +Now connect your Angular app to the running agent using streamResource(). + + + + +```bash +npm install @cacheplane/stream-resource +``` + + + + +```typescript +// app.config.ts +import { ApplicationConfig } from '@angular/core'; +import { provideStreamResource } from '@cacheplane/stream-resource'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideStreamResource({ + apiUrl: 'http://localhost:2024', + }), + ], +}; +``` + - -Agents pause for approval, confirmation, or correction. Your UI handles the interrupt and resumes execution. + + + + + +```typescript +// chat.component.ts +import { Component, signal, computed } from '@angular/core'; +import { streamResource } from '@cacheplane/stream-resource'; +import type { BaseMessage } from '@langchain/core/messages'; + +@Component({ + selector: 'app-chat', + templateUrl: './chat.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ChatComponent { + input = signal(''); + + // Create the streaming resource — this is the core API + chat = streamResource<{ messages: BaseMessage[] }>({ + assistantId: 'chat_agent', + threadId: signal(localStorage.getItem('threadId')), + onThreadId: (id) => localStorage.setItem('threadId', id), + }); + + // Derived signals — compose with computed() + isStreaming = computed(() => this.chat.status() === 'loading'); + messageCount = computed(() => this.chat.messages().length); + + send() { + const msg = this.input(); + if (!msg.trim()) return; + this.chat.submit({ + messages: [{ role: 'user', content: msg }], + }); + this.input.set(''); + } +} +``` + + + + +```html + +
+ + @for (msg of chat.messages(); track $index) { +
+

{{ msg.content }}

+
+ } + + + @if (isStreaming()) { +
Agent is thinking...
+ } + + + @if (chat.error(); as err) { +
{{ err.message }}
+ } + + +
+ + +
+
+``` + +
+
+
- -Track multiple subagents working in parallel, each with their own message stream and status. + + +```bash +ng serve +``` + +Open `http://localhost:4200` and start chatting with your agent. Messages stream in real-time as the LLM generates them. + - -Inspect agent execution history, fork from checkpoints, and explore alternate paths. +
+ +## Key Concepts + +Here's what streamResource() gives you out of the box: + +| Feature | Signal | Description | +|---------|--------|-------------| +| **Messages** | `chat.messages()` | Live message list, updates as tokens arrive | +| **Status** | `chat.status()` | Current state: idle, loading, resolved, error | +| **Thread persistence** | `threadId` option | Conversations survive page refreshes | +| **Interrupts** | `chat.interrupt()` | Agent pauses for human input | +| **History** | `chat.history()` | Full checkpoint timeline for time-travel | +| **Subagents** | `chat.subagents()` | Track delegated agent work | +| **Tool calls** | `chat.toolCalls()` | See what tools the agent is using | + +## Deploy to Production + +When you're ready to go live, deploy your agent to LangGraph Cloud. + + + + +Your agent code (the Python project with `langgraph.json`) needs to be in a GitHub repository. + + + + +Go to [LangSmith Deployments](https://smith.langchain.com) and click **+ New Deployment**. Connect your GitHub repo and deploy. This takes about 15 minutes. + + + + +Point `apiUrl` to your deployment URL: + +```typescript +provideStreamResource({ + apiUrl: 'https://your-deployment.langsmith.dev', +}) +``` + -## Guides + +Your Angular app is a stateless client. All agent state lives on LangGraph Platform. This means you can deploy your Angular app anywhere — CDN, edge, SSR — without state management concerns. + + +## What's Next - Build a chat component in 5 minutes - - - Detailed setup and configuration + Detailed 5-minute walkthrough with a complete chat component - Token-by-token updates via SSE + Token-by-token updates, stream modes, and status tracking - Thread persistence across sessions + Thread persistence across sessions and reactive thread switching - Human-in-the-loop approval flows + Human-in-the-loop approval and confirmation flows Deterministic testing with MockStreamTransport + + Deep dive into how Signals power streamResource + + + Graphs, nodes, edges, and state for Angular developers + + + Complete streamResource() function reference + diff --git a/apps/website/src/app/docs/[[...slug]]/page.tsx b/apps/website/src/app/docs/[[...slug]]/page.tsx index 558792999..e61f2eac2 100644 --- a/apps/website/src/app/docs/[[...slug]]/page.tsx +++ b/apps/website/src/app/docs/[[...slug]]/page.tsx @@ -5,6 +5,7 @@ import { DocsSearch } from '../../../components/docs/DocsSearch'; import { getDocBySlug, getAllDocSlugs } from '../../../lib/docs-new'; import { ApiDocRenderer, type ApiDocEntry } from '../../../components/docs/ApiDocRenderer'; import { DocsTOC } from '../../../components/docs/DocsTOC'; +import { DocsMobileNav } from '../../../components/docs/DocsMobileNav'; import { extractHeadings } from '../../../lib/extract-headings'; import fs from 'fs'; import path from 'path'; @@ -43,11 +44,14 @@ export default async function DocsPage({ params }: { params: Promise<{ slug?: st if (!doc) notFound(); return ( -
+
-
+
+
+ +
{section === 'api' && (() => { const entries = loadApiDocs(); diff --git a/apps/website/src/app/global.css b/apps/website/src/app/global.css index 7499ec889..4dfc73682 100644 --- a/apps/website/src/app/global.css +++ b/apps/website/src/app/global.css @@ -135,6 +135,7 @@ html { font-weight: 400; } +.docs-prose { overflow-wrap: break-word; word-break: break-word; } .docs-prose h1 { font-size: 1.875rem; font-weight: 700; margin-top: 0; margin-bottom: 1rem; font-family: var(--font-garamond); } .docs-prose h2 { font-size: 1.5rem; font-weight: 600; margin-top: 2.5rem; margin-bottom: 1rem; font-family: var(--font-garamond); } .docs-prose h3 { font-size: 1.25rem; font-weight: 600; margin-top: 2rem; margin-bottom: 0.75rem; font-family: var(--font-garamond); } diff --git a/apps/website/src/components/docs/DocsMobileNav.tsx b/apps/website/src/components/docs/DocsMobileNav.tsx new file mode 100644 index 000000000..5f6e077d7 --- /dev/null +++ b/apps/website/src/components/docs/DocsMobileNav.tsx @@ -0,0 +1,64 @@ +'use client'; +import { useState } from 'react'; +import Link from 'next/link'; +import { docsConfig } from '../../lib/docs-config'; +import { tokens } from '../../../lib/design-tokens'; + +export function DocsMobileNav({ activeSection, activeSlug }: { activeSection: string; activeSlug: string }) { + const [open, setOpen] = useState(false); + + return ( +
+ {/* Toggle button */} + + + {/* Drawer */} + {open && ( + + )} +
+ ); +} diff --git a/apps/website/src/components/docs/MdxRenderer.tsx b/apps/website/src/components/docs/MdxRenderer.tsx index f25c13a0b..d1f6001c8 100644 --- a/apps/website/src/components/docs/MdxRenderer.tsx +++ b/apps/website/src/components/docs/MdxRenderer.tsx @@ -8,6 +8,7 @@ import { CodeGroup } from './mdx/CodeGroup'; import { DocsBreadcrumb } from './DocsBreadcrumb'; import { DocsPrevNext } from './DocsPrevNext'; import rehypePrettyCode from 'rehype-pretty-code'; +import rehypeSlug from 'rehype-slug'; const mdxComponents = { Callout, @@ -34,7 +35,7 @@ interface NewProps { export function MdxRendererNew({ source, section, slug, title }: NewProps) { return ( -
+
diff --git a/apps/website/src/components/docs/mdx/Callout.tsx b/apps/website/src/components/docs/mdx/Callout.tsx index 7eff403a3..2061f40af 100644 --- a/apps/website/src/components/docs/mdx/Callout.tsx +++ b/apps/website/src/components/docs/mdx/Callout.tsx @@ -1,10 +1,54 @@ import { tokens } from '../../../../lib/design-tokens'; const CALLOUT_STYLES = { - info: { border: tokens.colors.accent, bg: 'rgba(0, 64, 144, 0.05)', icon: 'ℹ️' }, - warning: { border: '#f59e0b', bg: 'rgba(245, 158, 11, 0.05)', icon: '⚠️' }, - tip: { border: '#22c55e', bg: 'rgba(34, 197, 94, 0.05)', icon: '💡' }, - danger: { border: '#ef4444', bg: 'rgba(239, 68, 68, 0.05)', icon: '🚫' }, + info: { + accent: tokens.colors.accent, + bg: 'rgba(0, 64, 144, 0.04)', + glassBg: 'rgba(0, 64, 144, 0.06)', + glow: '0 0 20px rgba(0, 64, 144, 0.06)', + iconBg: 'rgba(0, 64, 144, 0.1)', + icon: ( + + + + ), + }, + warning: { + accent: '#e8930c', + bg: 'rgba(232, 147, 12, 0.04)', + glassBg: 'rgba(232, 147, 12, 0.06)', + glow: '0 0 20px rgba(232, 147, 12, 0.06)', + iconBg: 'rgba(232, 147, 12, 0.1)', + icon: ( + + + + ), + }, + tip: { + accent: '#10b981', + bg: 'rgba(16, 185, 129, 0.04)', + glassBg: 'rgba(16, 185, 129, 0.06)', + glow: '0 0 20px rgba(16, 185, 129, 0.06)', + iconBg: 'rgba(16, 185, 129, 0.1)', + icon: ( + + + + ), + }, + danger: { + accent: '#ef4444', + bg: 'rgba(239, 68, 68, 0.04)', + glassBg: 'rgba(239, 68, 68, 0.06)', + glow: '0 0 20px rgba(239, 68, 68, 0.06)', + iconBg: 'rgba(239, 68, 68, 0.1)', + icon: ( + + + + ), + }, } as const; interface Props { @@ -17,27 +61,46 @@ export function Callout({ type = 'info', title, children }: Props) { const s = CALLOUT_STYLES[type]; return (
{title && (
- {s.icon} - {title} +
+ {s.icon} +
+ {title}
)} -
+
{children}
diff --git a/apps/website/src/lib/extract-headings.ts b/apps/website/src/lib/extract-headings.ts index 5438188c2..00d86d9bf 100644 --- a/apps/website/src/lib/extract-headings.ts +++ b/apps/website/src/lib/extract-headings.ts @@ -20,7 +20,8 @@ export function extractHeadings(source: string): DocHeading[] { const match = line.match(/^(#{2,3})\s+(.+)$/); if (match) { const text = match[2].replace(/`/g, ''); - const id = text.toLowerCase().replace(/[^\w\s-]/g, '').replace(/\s+/g, '-'); + // Match rehype-slug's GitHub-style slugification + const id = text.toLowerCase().replace(/[^\w\s-]/g, '').replace(/\s+/g, '-').replace(/^-+|-+$/g, ''); headings.push({ id, text, level: match[1].length }); } } diff --git a/docs/superpowers/specs/2026-04-04-expanded-introduction-design.md b/docs/superpowers/specs/2026-04-04-expanded-introduction-design.md new file mode 100644 index 000000000..a4bb6acc5 --- /dev/null +++ b/docs/superpowers/specs/2026-04-04-expanded-introduction-design.md @@ -0,0 +1,66 @@ +# Expanded Introduction — Full Getting Started Tutorial + +**Date:** 2026-04-04 +**Scope:** Rewrite `getting-started/introduction.mdx` as a comprehensive end-to-end tutorial + +## Overview + +The current introduction is a brief 64-line overview. It needs to become the primary onboarding experience — teaching developers the full workflow from building a LangGraph agent to connecting it with Angular via streamResource(). + +## Target Audience + +Both Angular developers new to AI agents AND AI developers new to Angular. The tutorial should work for either starting point. + +## Tutorial Structure + +The introduction becomes a multi-section tutorial covering the complete workflow: + +### Section 1: What is StreamResource? +- One-paragraph pitch: Angular Signals + LangGraph streaming +- Key value props (3-4 bullets) +- "What you'll build" preview — a streaming chat app connected to a real agent + +### Section 2: Build Your Agent (Python) +- Use the repo's existing `examples/chat-agent` as the reference +- Show the minimal LangGraph agent (`agent.py` with `MessagesState`, `call_model` node) +- Show `langgraph.json` configuration +- Explain the graph: nodes, edges, state + +### Section 3: Run Locally +- Install LangGraph CLI: `pip install -U "langgraph-cli[inmem]"` +- Start dev server: `langgraph dev` +- Verify at `http://localhost:2024` +- Test in LangGraph Studio + +### Section 4: Connect with Angular +- Install streamResource: `npm install @cacheplane/stream-resource` +- Configure provider in `app.config.ts` +- Create a chat component with `streamResource()` +- Show TypeScript + Template code with Tabs +- Explain the Signals: messages(), status(), error() + +### Section 5: Deploy to LangGraph Cloud +- Push agent to GitHub +- Deploy via LangSmith dashboard +- Update Angular `apiUrl` to deployment URL +- Production considerations + +### Section 6: Next Steps +- CardGroup linking to all guide pages +- Links to concepts for deeper understanding + +## MDX Components Used +- `` for prerequisites and tips +- `` for sequential instructions +- `` for TypeScript/Template and Python/Config code +- `` for next steps navigation + +## Files Modified +- Replace: `apps/website/content/docs-v2/getting-started/introduction.mdx` + +## Verification +- Open /docs/getting-started/introduction +- Verify all 6 sections render with MDX components +- Code blocks are syntax highlighted +- TOC shows all sections +- Mobile renders properly diff --git a/package-lock.json b/package-lock.json index 81a6e33e1..86bbde111 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "rehype-pretty-code": "^0.14.3", + "rehype-slug": "^6.0.0", "rxjs": "~7.8.0", "shiki": "^4.0.2" }, @@ -22844,6 +22845,12 @@ "assert-plus": "^1.0.0" } }, + "node_modules/github-slugger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", + "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==", + "license": "ISC" + }, "node_modules/glob": { "version": "13.0.6", "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", @@ -23378,6 +23385,19 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-heading-rank": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-heading-rank/-/hast-util-heading-rank-3.0.0.tgz", + "integrity": "sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-parse-selector": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", @@ -31079,6 +31099,23 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/rehype-slug": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/rehype-slug/-/rehype-slug-6.0.0.tgz", + "integrity": "sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "github-slugger": "^2.0.0", + "hast-util-heading-rank": "^3.0.0", + "hast-util-to-string": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/remark-mdx": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.1.tgz", diff --git a/package.json b/package.json index 302900819..452efa4af 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "rehype-pretty-code": "^0.14.3", + "rehype-slug": "^6.0.0", "rxjs": "~7.8.0", "shiki": "^4.0.2" },