diff --git a/.claude/launch.json b/.claude/launch.json index ff4c563eb..74c73044d 100644 --- a/.claude/launch.json +++ b/.claude/launch.json @@ -4,8 +4,9 @@ { "name": "website-dev", "runtimeExecutable": "/bin/bash", - "runtimeArgs": ["-c", "export PATH=/Users/blove/.nvm/versions/node/v22.14.0/bin:$PATH && npx nx serve website"], - "port": 3000 + "runtimeArgs": ["-c", "export PATH=/Users/blove/.nvm/versions/node/v22.14.0/bin:$PATH && npx nx serve website --port $PORT"], + "port": 3000, + "autoPort": true }, { "name": "cockpit", diff --git a/apps/website/content/docs-v2/api/agent.mdx b/apps/website/content/docs/agent/api/agent.mdx similarity index 95% rename from apps/website/content/docs-v2/api/agent.mdx rename to apps/website/content/docs/agent/api/agent.mdx index 7475f7595..3b89c802e 100644 --- a/apps/website/content/docs-v2/api/agent.mdx +++ b/apps/website/content/docs/agent/api/agent.mdx @@ -52,21 +52,21 @@ For plain HTTP requests that return a single value and complete, Angular's built Build your first streaming component end-to-end in under five minutes. Deep dive into SSE lifecycle, error handling, and reconnect strategies. Understand how angular integrates with Angular's reactivity model. diff --git a/apps/website/content/docs-v2/api/api-docs.json b/apps/website/content/docs/agent/api/api-docs.json similarity index 100% rename from apps/website/content/docs-v2/api/api-docs.json rename to apps/website/content/docs/agent/api/api-docs.json diff --git a/apps/website/content/docs-v2/api/fetch-stream-transport.mdx b/apps/website/content/docs/agent/api/fetch-stream-transport.mdx similarity index 95% rename from apps/website/content/docs-v2/api/fetch-stream-transport.mdx rename to apps/website/content/docs/agent/api/fetch-stream-transport.mdx index 0cdcb409a..1f6977845 100644 --- a/apps/website/content/docs-v2/api/fetch-stream-transport.mdx +++ b/apps/website/content/docs/agent/api/fetch-stream-transport.mdx @@ -42,21 +42,21 @@ The transport handles: Learn how the SSE lifecycle maps to resource signals and how to handle reconnects. Server configuration for SSE: headers, timeouts, and edge runtime considerations. The test-time counterpart — push values synchronously without a real server. diff --git a/apps/website/content/docs-v2/api/mock-stream-transport.mdx b/apps/website/content/docs/agent/api/mock-stream-transport.mdx similarity index 96% rename from apps/website/content/docs-v2/api/mock-stream-transport.mdx rename to apps/website/content/docs/agent/api/mock-stream-transport.mdx index 1885a0d45..a7c45ca22 100644 --- a/apps/website/content/docs-v2/api/mock-stream-transport.mdx +++ b/apps/website/content/docs/agent/api/mock-stream-transport.mdx @@ -78,21 +78,21 @@ describe('RepoComponent', () => { Full testing patterns including component harnesses and multi-stream scenarios. The production transport that MockAgentTransport replaces in tests. Full reference for the primitive you are testing against. diff --git a/apps/website/content/docs-v2/api/provide-agent.mdx b/apps/website/content/docs/agent/api/provide-agent.mdx similarity index 94% rename from apps/website/content/docs-v2/api/provide-agent.mdx rename to apps/website/content/docs/agent/api/provide-agent.mdx index 4ef91f911..4159dc5a3 100644 --- a/apps/website/content/docs-v2/api/provide-agent.mdx +++ b/apps/website/content/docs/agent/api/provide-agent.mdx @@ -51,21 +51,21 @@ provideAgent({ transport: MockAgentTransport }) Step-by-step setup guide including peer dependencies and NgModule support. Configure transports for production, SSR, and edge runtimes. Full reference for the core primitive you configure here. diff --git a/apps/website/content/docs-v2/concepts/agent-architecture.mdx b/apps/website/content/docs/agent/concepts/agent-architecture.mdx similarity index 100% rename from apps/website/content/docs-v2/concepts/agent-architecture.mdx rename to apps/website/content/docs/agent/concepts/agent-architecture.mdx diff --git a/apps/website/content/docs-v2/concepts/angular-signals.mdx b/apps/website/content/docs/agent/concepts/angular-signals.mdx similarity index 100% rename from apps/website/content/docs-v2/concepts/angular-signals.mdx rename to apps/website/content/docs/agent/concepts/angular-signals.mdx diff --git a/apps/website/content/docs-v2/concepts/langgraph-basics.mdx b/apps/website/content/docs/agent/concepts/langgraph-basics.mdx similarity index 100% rename from apps/website/content/docs-v2/concepts/langgraph-basics.mdx rename to apps/website/content/docs/agent/concepts/langgraph-basics.mdx diff --git a/apps/website/content/docs-v2/concepts/state-management.mdx b/apps/website/content/docs/agent/concepts/state-management.mdx similarity index 100% rename from apps/website/content/docs-v2/concepts/state-management.mdx rename to apps/website/content/docs/agent/concepts/state-management.mdx diff --git a/apps/website/content/docs-v2/getting-started/installation.mdx b/apps/website/content/docs/agent/getting-started/installation.mdx similarity index 100% rename from apps/website/content/docs-v2/getting-started/installation.mdx rename to apps/website/content/docs/agent/getting-started/installation.mdx diff --git a/apps/website/content/docs-v2/getting-started/introduction.mdx b/apps/website/content/docs/agent/getting-started/introduction.mdx similarity index 100% rename from apps/website/content/docs-v2/getting-started/introduction.mdx rename to apps/website/content/docs/agent/getting-started/introduction.mdx diff --git a/apps/website/content/docs-v2/getting-started/quickstart.mdx b/apps/website/content/docs/agent/getting-started/quickstart.mdx similarity index 100% rename from apps/website/content/docs-v2/getting-started/quickstart.mdx rename to apps/website/content/docs/agent/getting-started/quickstart.mdx diff --git a/apps/website/content/docs-v2/guides/deployment.mdx b/apps/website/content/docs/agent/guides/deployment.mdx similarity index 100% rename from apps/website/content/docs-v2/guides/deployment.mdx rename to apps/website/content/docs/agent/guides/deployment.mdx diff --git a/apps/website/content/docs-v2/guides/interrupts.mdx b/apps/website/content/docs/agent/guides/interrupts.mdx similarity index 100% rename from apps/website/content/docs-v2/guides/interrupts.mdx rename to apps/website/content/docs/agent/guides/interrupts.mdx diff --git a/apps/website/content/docs-v2/guides/memory.mdx b/apps/website/content/docs/agent/guides/memory.mdx similarity index 100% rename from apps/website/content/docs-v2/guides/memory.mdx rename to apps/website/content/docs/agent/guides/memory.mdx diff --git a/apps/website/content/docs-v2/guides/persistence.mdx b/apps/website/content/docs/agent/guides/persistence.mdx similarity index 100% rename from apps/website/content/docs-v2/guides/persistence.mdx rename to apps/website/content/docs/agent/guides/persistence.mdx diff --git a/apps/website/content/docs-v2/guides/streaming.mdx b/apps/website/content/docs/agent/guides/streaming.mdx similarity index 100% rename from apps/website/content/docs-v2/guides/streaming.mdx rename to apps/website/content/docs/agent/guides/streaming.mdx diff --git a/apps/website/content/docs-v2/guides/subgraphs.mdx b/apps/website/content/docs/agent/guides/subgraphs.mdx similarity index 100% rename from apps/website/content/docs-v2/guides/subgraphs.mdx rename to apps/website/content/docs/agent/guides/subgraphs.mdx diff --git a/apps/website/content/docs-v2/guides/testing.mdx b/apps/website/content/docs/agent/guides/testing.mdx similarity index 100% rename from apps/website/content/docs-v2/guides/testing.mdx rename to apps/website/content/docs/agent/guides/testing.mdx diff --git a/apps/website/content/docs-v2/guides/time-travel.mdx b/apps/website/content/docs/agent/guides/time-travel.mdx similarity index 100% rename from apps/website/content/docs-v2/guides/time-travel.mdx rename to apps/website/content/docs/agent/guides/time-travel.mdx diff --git a/apps/website/content/docs/chat/api/chat-config.mdx b/apps/website/content/docs/chat/api/chat-config.mdx new file mode 100644 index 000000000..f734451a3 --- /dev/null +++ b/apps/website/content/docs/chat/api/chat-config.mdx @@ -0,0 +1,132 @@ +# ChatConfig + +`ChatConfig` is the configuration interface accepted by `provideChat()`. It defines global settings for chat composition components including the generative UI registry, avatar styling, and assistant naming. + +**Import:** + +```typescript +import type { ChatConfig } from '@cacheplane/chat'; +``` + +## Interface Definition + +```typescript +interface ChatConfig { + /** Default render registry for generative UI components. */ + renderRegistry?: AngularRegistry; + + /** Override the default AI avatar label (default: "A"). */ + avatarLabel?: string; + + /** Override the default assistant display name (default: "Assistant"). */ + assistantName?: string; +} +``` + +## Properties + +### renderRegistry + +```typescript +renderRegistry?: AngularRegistry +``` + +The component registry used by `ChatGenerativeUiComponent` to resolve JSON UI specs to Angular components. This is an `AngularRegistry` from `@cacheplane/render`. + +When not provided, generative UI components will not render -- the `chat-generative-ui` primitive will simply show nothing for any spec passed to it. + +**Example:** + +```typescript +import { createAngularRegistry } from '@cacheplane/render'; +import { WeatherCardComponent } from './weather-card.component'; +import { ChartComponent } from './chart.component'; + +const registry = createAngularRegistry({ + weather_card: WeatherCardComponent, + chart: ChartComponent, +}); + +provideChat({ renderRegistry: registry }); +``` + +See the [Generative UI guide](/docs/chat/guides/generative-ui) for detailed setup. + +### avatarLabel + +```typescript +avatarLabel?: string +``` + +A short string (typically one or two characters) displayed in the AI avatar badge that appears next to assistant messages in composition components. + +**Default:** `"A"` + +**Example:** + +```typescript +provideChat({ avatarLabel: 'AI' }); +``` + +The avatar badge is a small square element styled with `--chat-avatar-bg` and `--chat-avatar-text` CSS variables. + +### assistantName + +```typescript +assistantName?: string +``` + +The display name for the AI assistant. Used in labels, ARIA attributes, and any place where the assistant needs a human-readable name. + +**Default:** `"Assistant"` + +**Example:** + +```typescript +provideChat({ assistantName: 'Code Copilot' }); +``` + +## Accessing ChatConfig at Runtime + +Inject `CHAT_CONFIG` to read configuration values in your own components: + +```typescript +import { inject } from '@angular/core'; +import { CHAT_CONFIG } from '@cacheplane/chat'; +import type { ChatConfig } from '@cacheplane/chat'; + +@Component({ + selector: 'app-chat-header', + template: ` +

{{ assistantName }}

+ `, +}) +export class ChatHeaderComponent { + private config = inject(CHAT_CONFIG, { optional: true }); + + get assistantName(): string { + return this.config?.assistantName ?? 'Assistant'; + } +} +``` + +## Relationship to Other Types + +`ChatConfig` references the following external type: + +| Type | Package | Description | +|------|---------|-------------| +| `AngularRegistry` | `@cacheplane/render` | Maps JSON spec type strings to Angular component classes | + +## Type Location + +The `ChatConfig` interface is defined in two files within the library: + +- `libs/chat/src/lib/provide-chat.ts` -- The canonical definition with JSDoc comments, alongside the `provideChat()` function and `CHAT_CONFIG` token +- `libs/chat/src/lib/chat.types.ts` -- A simplified re-export for internal use + +The public API exports `ChatConfig` as a type-only export: + +```typescript +export type { ChatConfig } from './lib/provide-chat'; +``` diff --git a/apps/website/content/docs/chat/api/create-mock-agent-ref.mdx b/apps/website/content/docs/chat/api/create-mock-agent-ref.mdx new file mode 100644 index 000000000..db6bcb3e9 --- /dev/null +++ b/apps/website/content/docs/chat/api/create-mock-agent-ref.mdx @@ -0,0 +1,238 @@ +# createMockAgentRef() + +`createMockAgentRef()` creates a mock `AgentRef` with writable signals for testing chat components. Instead of connecting to a real LangGraph agent, you get an object whose signals you can set directly to simulate any agent state. + +**Import:** + +```typescript +import { createMockAgentRef } from '@cacheplane/chat'; +``` + +## Signature + +```typescript +function createMockAgentRef(initial?: { + messages?: BaseMessage[]; + status?: ResourceStatus; + isLoading?: boolean; + error?: unknown; + hasValue?: boolean; + isThreadLoading?: boolean; +}): MockAgentRef +``` + +### Parameters + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `initial` | `object` | `{}` | Optional initial values for the mock's signals | + +### Initial Options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `messages` | `BaseMessage[]` | `[]` | Initial message list | +| `status` | `ResourceStatus` | `ResourceStatus.Idle` | Initial resource status | +| `isLoading` | `boolean` | `false` | Initial loading state | +| `error` | `unknown` | `null` | Initial error value | +| `hasValue` | `boolean` | `false` | Whether the ref has a resolved value | +| `isThreadLoading` | `boolean` | `false` | Whether thread data is loading | + +### Returns + +A `MockAgentRef` object -- an `AgentRef` with all signals replaced by `WritableSignal` instances. + +## MockAgentRef Interface + +```typescript +interface MockAgentRef extends AgentRef { + messages: WritableSignal; + status: WritableSignal; + error: WritableSignal; + interrupt: WritableSignal | undefined>; + interrupts: WritableSignal[]>; + isLoading: WritableSignal; + hasValue: WritableSignal; + value: WritableSignal; + toolProgress: WritableSignal; + toolCalls: WritableSignal; + branch: WritableSignal; + history: WritableSignal[]>; + isThreadLoading: WritableSignal; + subagents: WritableSignal>; + activeSubagents: WritableSignal; +} +``` + +Every property on `MockAgentRef` is a `WritableSignal`, so you can call `.set()` to control the state directly in your tests. + +## Mock Methods + +The mock provides no-op implementations for all `AgentRef` methods: + +| Method | Behavior | +|--------|----------| +| `submit()` | Returns `Promise.resolve()` | +| `stop()` | Returns `Promise.resolve()` | +| `reload()` | No-op | +| `switchThread()` | No-op | +| `joinStream()` | Returns `Promise.resolve()` | +| `setBranch()` | No-op | +| `getMessagesMetadata()` | Returns `undefined` | +| `getToolCalls()` | Returns `[]` | + +## Basic Test Usage + +```typescript +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { createMockAgentRef, ChatComponent } from '@cacheplane/chat'; +import type { MockAgentRef } from '@cacheplane/chat'; + +describe('ChatComponent', () => { + let fixture: ComponentFixture; + let mockRef: MockAgentRef; + + beforeEach(async () => { + mockRef = createMockAgentRef({ + messages: [], + isLoading: false, + }); + + await TestBed.configureTestingModule({ + imports: [ChatComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ChatComponent); + fixture.componentRef.setInput('ref', mockRef); + fixture.detectChanges(); + }); + + it('should show empty state when no messages', () => { + const el = fixture.nativeElement; + expect(el.textContent).toContain('Send a message'); + }); + + it('should render messages when set', () => { + mockRef.messages.set([ + { content: 'Hello', _getType: () => 'human' } as any, + { content: 'Hi there!', _getType: () => 'ai' } as any, + ]); + fixture.detectChanges(); + + const el = fixture.nativeElement; + expect(el.textContent).toContain('Hello'); + expect(el.textContent).toContain('Hi there!'); + }); +}); +``` + +## Simulating Agent States + +### Loading State + +```typescript +const ref = createMockAgentRef({ isLoading: true }); +// Typing indicator will appear, input will be disabled +``` + +### Error State + +```typescript +const ref = createMockAgentRef(); +ref.error.set(new Error('Connection failed')); +// ChatErrorComponent will display "Connection failed" +``` + +### Interrupt State + +```typescript +const ref = createMockAgentRef(); +ref.interrupt.set({ + value: 'Please confirm the deletion', + resumable: true, + ns: [], +}); +// ChatInterruptComponent / ChatInterruptPanelComponent will render +``` + +### Tool Calls + +```typescript +const ref = createMockAgentRef(); +ref.toolCalls.set([ + { + id: 'call_1', + name: 'search_docs', + args: { query: 'Angular signals' }, + result: { found: 3 }, + }, +]); +``` + +### History (for Debug Components) + +```typescript +const ref = createMockAgentRef(); +ref.history.set([ + { + values: { messages: [], count: 0 }, + next: ['agent'], + checkpoint: { checkpoint_id: 'cp_1' }, + }, + { + values: { messages: [{ content: 'Hello' }], count: 1 }, + next: ['tools'], + checkpoint: { checkpoint_id: 'cp_2' }, + }, +]); +``` + +## Testing Custom Components + +Use `createMockAgentRef()` to test any component that accepts an `AgentRef` input: + +```typescript +import { Component, input } from '@angular/core'; +import type { AgentRef } from '@cacheplane/angular'; +import { createMockAgentRef } from '@cacheplane/chat'; + +// Your component +@Component({ + selector: 'app-message-count', + template: `Messages: {{ ref().messages().length }}`, +}) +export class MessageCountComponent { + ref = input.required>(); +} + +// Test +it('should display message count', () => { + const mockRef = createMockAgentRef({ + messages: [ + { content: 'One', _getType: () => 'human' } as any, + { content: 'Two', _getType: () => 'ai' } as any, + ], + }); + + const fixture = TestBed.createComponent(MessageCountComponent); + fixture.componentRef.setInput('ref', mockRef); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toContain('Messages: 2'); +}); +``` + +## Spying on Methods + +Since the mock methods are plain functions, you can replace them with spies: + +```typescript +const ref = createMockAgentRef(); +const submitSpy = jest.fn().mockResolvedValue(undefined); +ref.submit = submitSpy; + +// After triggering a submit in the component: +expect(submitSpy).toHaveBeenCalledWith({ + messages: [{ role: 'human', content: 'Hello' }], +}); +``` diff --git a/apps/website/content/docs/chat/api/provide-chat.mdx b/apps/website/content/docs/chat/api/provide-chat.mdx new file mode 100644 index 000000000..0e4e9810e --- /dev/null +++ b/apps/website/content/docs/chat/api/provide-chat.mdx @@ -0,0 +1,179 @@ +# provideChat() + +`provideChat` is the provider factory that registers `@cacheplane/chat` configuration in Angular's dependency injection system. Call it once in your `ApplicationConfig` (or at the route level) to set global defaults used by chat composition components. + +```typescript +import { provideChat } from '@cacheplane/chat'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideChat({ + renderRegistry: myRegistry, + avatarLabel: 'AI', + assistantName: 'My Assistant', + }), + ], +}; +``` + +## Signature + +```typescript +function provideChat(config: ChatConfig): EnvironmentProviders +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| `config` | `ChatConfig` | Configuration object with optional render registry, avatar label, and assistant name | + +**Returns:** `EnvironmentProviders` -- created via `makeEnvironmentProviders()`, compatible with `bootstrapApplication`, `ApplicationConfig`, and route-level `providers`. + +## What It Does + +`provideChat()` registers a single provider: + +```typescript +{ provide: CHAT_CONFIG, useValue: config } +``` + +This makes the `ChatConfig` object available throughout the application via the `CHAT_CONFIG` injection token. + +## CHAT_CONFIG Injection Token + +```typescript +import { CHAT_CONFIG } from '@cacheplane/chat'; + +const CHAT_CONFIG: InjectionToken; +``` + +The token is an `InjectionToken` that can be injected in any component, directive, or service: + +```typescript +import { inject } from '@angular/core'; +import { CHAT_CONFIG } from '@cacheplane/chat'; + +@Component({ /* ... */ }) +export class MyComponent { + private chatConfig = inject(CHAT_CONFIG); +} +``` + + +Injecting `CHAT_CONFIG` without calling `provideChat()` will throw a `NullInjectorError`. Use `inject(CHAT_CONFIG, { optional: true })` if your component should work without global configuration. + + +## Configuration Options + +See the [ChatConfig API reference](/docs/chat/api/chat-config) for the full interface definition. + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `renderRegistry` | `AngularRegistry` | `undefined` | Component registry for generative UI | +| `avatarLabel` | `string` | `"A"` | AI avatar badge text | +| `assistantName` | `string` | `"Assistant"` | AI assistant display name | + +## Usage Patterns + +### Application-Wide Configuration + +```typescript +// app.config.ts +import { provideAgent } from '@cacheplane/angular'; +import { provideChat } from '@cacheplane/chat'; +import { createAngularRegistry } from '@cacheplane/render'; +import { WeatherCardComponent } from './components/weather-card.component'; + +const registry = createAngularRegistry({ + weather_card: WeatherCardComponent, +}); + +export const appConfig: ApplicationConfig = { + providers: [ + provideAgent({ apiUrl: 'http://localhost:2024' }), + provideChat({ + renderRegistry: registry, + avatarLabel: 'B', + assistantName: 'Bot', + }), + ], +}; +``` + +### Route-Level Configuration + +Provide different chat configurations for different parts of your application: + +```typescript +// app.routes.ts +export const routes: Routes = [ + { + path: 'support', + loadComponent: () => import('./support/support-chat.component'), + providers: [ + provideChat({ + assistantName: 'Support Agent', + avatarLabel: 'S', + }), + ], + }, + { + path: 'coding', + loadComponent: () => import('./coding/code-chat.component'), + providers: [ + provideChat({ + assistantName: 'Code Helper', + avatarLabel: 'C', + renderRegistry: codeWidgetRegistry, + }), + ], + }, +]; +``` + +### Without provideChat() + +All chat components work without `provideChat()`. They use defaults: + +- Avatar label: `"A"` +- Assistant name: `"Assistant"` +- No render registry (generative UI disabled) + +```typescript +// This works fine without provideChat() +@Component({ + imports: [ChatComponent], + template: ``, +}) +export class SimpleChatComponent { + chatRef = agent<{ messages: BaseMessage[] }>({ + assistantId: 'chat_agent', + threadId: signal(null), + }); +} +``` + +## What's Next + + + + Full ChatConfig interface reference. + + + Set up renderRegistry for dynamic UI components. + + + Configuration patterns and best practices. + + diff --git a/apps/website/content/docs/chat/components/chat-debug.mdx b/apps/website/content/docs/chat/components/chat-debug.mdx new file mode 100644 index 000000000..d952f0b83 --- /dev/null +++ b/apps/website/content/docs/chat/components/chat-debug.mdx @@ -0,0 +1,231 @@ +# ChatDebugComponent + +`ChatDebugComponent` is a full debug cockpit that combines a chat interface with a side panel for inspecting LangGraph execution state. It renders the same chat UI as `ChatComponent` alongside a debug panel with a timeline, state inspector, diff viewer, and navigation controls. + +**Selector:** `chat-debug` + +**Import:** + +```typescript +import { ChatDebugComponent } from '@cacheplane/chat'; +``` + +## When to Use It + +Use `ChatDebugComponent` during development to understand what your LangGraph agent is doing at each step. The debug panel shows: + +- A timeline of execution checkpoints +- State values at each checkpoint +- A diff between consecutive checkpoints +- Navigation controls to step through the execution history + +## Basic Usage + +```typescript +import { Component, signal, ChangeDetectionStrategy } from '@angular/core'; +import { agent } from '@cacheplane/angular'; +import type { BaseMessage } from '@langchain/core/messages'; +import { ChatDebugComponent } from '@cacheplane/chat'; + +@Component({ + selector: 'app-debug-page', + standalone: true, + imports: [ChatDebugComponent], + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +
+ +
+ `, +}) +export class DebugPageComponent { + chatRef = agent<{ messages: BaseMessage[] }>({ + assistantId: 'chat_agent', + threadId: signal(null), + }); +} +``` + +## API + +### Inputs + +| Input | Type | Default | Description | +|-------|------|---------|-------------| +| `ref` | `AgentRef` | **Required** | The agent ref providing streaming state and execution history | + +## Layout + +The component renders a two-column layout: + +- **Left**: The chat area (messages, typing indicator, error display, input) +- **Right**: The collapsible debug panel (320px wide) + +The debug panel can be toggled open/closed with a button on the divider. + +## Debug Panel Sections + +### Summary + +At the top, `DebugSummaryComponent` displays the total number of checkpoints and cumulative execution duration. + +### Controls + +`DebugControlsComponent` provides VCR-style navigation buttons: + +| Button | Action | Disabled When | +|--------|--------|---------------| +| `\|<` | Jump to start | Already at first checkpoint | +| `<` | Step back | Already at first checkpoint | +| `>` | Step forward | Already at last checkpoint | +| `>\|` | Jump to end | Already at last checkpoint | + +### Timeline + +`DebugTimelineComponent` renders a vertical timeline rail with one `DebugCheckpointCardComponent` per checkpoint. Each card shows: + +- The node name (from `state.next[0]` or `"Step N"`) +- Optional duration badge (milliseconds) +- Optional token count badge +- Visual selection state (highlighted dot and border) + +Click a checkpoint to select it and view its details below. + +### Detail + +When a checkpoint is selected, `DebugDetailComponent` renders two sections: + +1. **State Diff** (`DebugStateDiffComponent`): Shows what changed between the previous and current checkpoint as color-coded entries: + - Green (`+`): Added keys + - Red (`-`): Removed keys + - Yellow (`~`): Changed values (shows before and after) + +2. **Current State** (`DebugStateInspectorComponent`): Displays the full state values as formatted JSON. + +## Debug Sub-Components + +Each debug sub-component is exported individually for custom debug UIs: + +| Component | Selector | Purpose | +|-----------|----------|---------| +| `DebugTimelineComponent` | `chat-debug-timeline` | Vertical timeline of checkpoints | +| `DebugCheckpointCardComponent` | `chat-debug-checkpoint-card` | Single checkpoint card in the timeline | +| `DebugControlsComponent` | `chat-debug-controls` | VCR-style step navigation | +| `DebugSummaryComponent` | `chat-debug-summary` | Step count and total duration | +| `DebugDetailComponent` | `chat-debug-detail` | State diff + state inspector | +| `DebugStateDiffComponent` | `chat-debug-state-diff` | Color-coded state diff | +| `DebugStateInspectorComponent` | `chat-debug-state-inspector` | JSON state viewer | + +## Debug Utilities + +### toDebugCheckpoint() + +Converts a `ThreadState` entry to a `DebugCheckpoint`: + +```typescript +import { toDebugCheckpoint } from '@cacheplane/chat'; + +const checkpoint = toDebugCheckpoint(threadState, index); +// { node: 'agent', checkpointId: 'abc123' } +``` + +### extractStateValues() + +Extracts the state values from a `ThreadState`, returning `{}` if unavailable: + +```typescript +import { extractStateValues } from '@cacheplane/chat'; + +const values = extractStateValues(threadState); +// { messages: [...], someKey: 'value' } +``` + +### computeStateDiff() + +Computes a recursive diff between two state objects: + +```typescript +import { computeStateDiff } from '@cacheplane/chat'; +import type { DiffEntry } from '@cacheplane/chat'; + +const diff: DiffEntry[] = computeStateDiff(beforeState, afterState); +``` + +### DebugCheckpoint Type + +```typescript +interface DebugCheckpoint { + node?: string; + duration?: number; + tokenCount?: number; + checkpointId?: string; +} +``` + +### DiffEntry Type + +```typescript +interface DiffEntry { + path: string; + type: 'added' | 'removed' | 'changed'; + before?: unknown; + after?: unknown; +} +``` + +## Building a Custom Debug Panel + +You can use the sub-components individually to build a custom debug layout: + +```typescript +import { Component, signal, computed, ChangeDetectionStrategy } from '@angular/core'; +import { + DebugTimelineComponent, + DebugDetailComponent, + toDebugCheckpoint, + extractStateValues, +} from '@cacheplane/chat'; +import type { DebugCheckpoint } from '@cacheplane/chat'; + +@Component({ + selector: 'app-custom-debug', + standalone: true, + imports: [DebugTimelineComponent, DebugDetailComponent], + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +
+ + + @if (selectedIndex() >= 0) { + + } +
+ `, +}) +export class CustomDebugComponent { + // chatRef = agent(...) + + selectedIndex = signal(-1); + + checkpoints = computed((): DebugCheckpoint[] => + this.chatRef.history().map((state, i) => toDebugCheckpoint(state, i)) + ); + + currentState = computed(() => + extractStateValues(this.chatRef.history()[this.selectedIndex()]) + ); + + previousState = computed(() => { + const idx = this.selectedIndex(); + if (idx <= 0) return {}; + return extractStateValues(this.chatRef.history()[idx - 1]); + }); +} +``` diff --git a/apps/website/content/docs/chat/components/chat-input.mdx b/apps/website/content/docs/chat/components/chat-input.mdx new file mode 100644 index 000000000..6b6483913 --- /dev/null +++ b/apps/website/content/docs/chat/components/chat-input.mdx @@ -0,0 +1,159 @@ +# ChatInputComponent + +`ChatInputComponent` is the text input primitive for sending messages to a LangGraph agent. It provides a textarea with auto-sizing, a send button, keyboard handling, and automatic disabling while the agent is loading. + +**Selector:** `chat-input` + +**Import:** + +```typescript +import { ChatInputComponent, submitMessage } from '@cacheplane/chat'; +``` + +## Basic Usage + +```html + +``` + +## API + +### Inputs + +| Input | Type | Default | Description | +|-------|------|---------|-------------| +| `ref` | `AgentRef` | **Required** | The agent ref to submit messages to | +| `submitOnEnter` | `boolean` | `true` | When `true`, pressing Enter submits the message. Shift+Enter inserts a newline. When `false`, Enter always inserts a newline. | +| `placeholder` | `string` | `''` | Placeholder text shown when the input is empty | + +### Outputs + +| Output | Type | Description | +|--------|------|-------------| +| `submitted` | `string` | Emits the trimmed message text after successful submission | + +## Behavior + +### Submit Flow + +When the user submits (via Enter key or clicking the send button): + +1. The message text is trimmed +2. If empty after trimming, nothing happens +3. `ref.submit()` is called with `{ messages: [{ role: 'human', content: trimmed }] }` +4. The `submitted` output emits the trimmed text +5. The input is cleared +6. Focus is returned to the textarea + +### Disabled State + +The input is automatically disabled when `ref.isLoading()` returns `true`. This prevents sending messages while the agent is processing. Both the textarea and the send button reflect the disabled state. + +### Auto-Sizing + +The textarea uses `field-sizing: content` CSS, which allows it to grow with its content up to a maximum height of `120px`. After that, the textarea scrolls internally. + +### Keyboard Handling + +| Key | `submitOnEnter: true` | `submitOnEnter: false` | +|-----|----------------------|----------------------| +| Enter | Submits the message | Inserts a newline | +| Shift+Enter | Inserts a newline | Inserts a newline | + +## submitMessage() Helper + +The `submitMessage()` function is exported as a standalone utility for programmatic message submission: + +```typescript +import { submitMessage } from '@cacheplane/chat'; + +function sendGreeting(ref: AgentRef) { + const result = submitMessage(ref, 'Hello!'); + // result is 'Hello!' or null if the text was empty +} +``` + +**Signature:** + +```typescript +function submitMessage( + ref: AgentRef, + text: string +): string | null +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| `ref` | `AgentRef` | The agent ref to submit to | +| `text` | `string` | The message text to send | + +**Returns:** The trimmed message string if submitted, or `null` if the trimmed text was empty. + +The function calls `ref.submit({ messages: [{ role: 'human', content: trimmed }] })` under the hood. + +## Styling + +The component renders a `
` containing a `