diff --git a/cockpit/chat/generative-ui/angular/src/app/app.config.ts b/cockpit/chat/generative-ui/angular/src/app/app.config.ts index bbe3e5e49..1e72bb3c9 100644 --- a/cockpit/chat/generative-ui/angular/src/app/app.config.ts +++ b/cockpit/chat/generative-ui/angular/src/app/app.config.ts @@ -2,13 +2,11 @@ import { ApplicationConfig } from '@angular/core'; import { provideAgent } from '@cacheplane/angular'; import { provideChat } from '@cacheplane/chat'; -import { provideRender } from '@cacheplane/render'; import { environment } from '../environments/environment'; export const appConfig: ApplicationConfig = { providers: [ provideAgent({ apiUrl: environment.langGraphApiUrl }), provideChat({}), - provideRender({}), ], }; diff --git a/cockpit/chat/generative-ui/angular/src/app/generative-ui.component.ts b/cockpit/chat/generative-ui/angular/src/app/generative-ui.component.ts index 0089176c7..ebfd92f18 100644 --- a/cockpit/chat/generative-ui/angular/src/app/generative-ui.component.ts +++ b/cockpit/chat/generative-ui/angular/src/app/generative-ui.component.ts @@ -1,42 +1,28 @@ // SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 import { Component } from '@angular/core'; -import { ChatComponent, ChatGenerativeUiComponent } from '@cacheplane/chat'; +import { ChatComponent, views } from '@cacheplane/chat'; import { agent } from '@cacheplane/angular'; import { environment } from '../environments/environment'; +import { WeatherCardComponent } from './views/weather-card.component'; +import { StatCardComponent } from './views/stat-card.component'; +import { ContainerComponent } from './views/container.component'; + +const myViews = views({ + weather_card: WeatherCardComponent, + stat_card: StatCardComponent, + container: ContainerComponent, +}); -/** - * GenerativeUiComponent demonstrates dynamic UI generation within - * chat messages using ChatComponent and ChatGenerativeUiComponent. - * The agent embeds render specs that are rendered as live components. - */ @Component({ selector: 'app-generative-ui', standalone: true, - imports: [ChatComponent, ChatGenerativeUiComponent], - template: ` -
- - -
- `, + imports: [ChatComponent], + template: ``, }) export class GenerativeUiComponent { - protected readonly stream = agent({ + protected readonly agentRef = agent({ apiUrl: environment.langGraphApiUrl, - assistantId: environment.streamingAssistantId, + assistantId: environment.generativeUiAssistantId, }); + protected readonly myViews = myViews; } diff --git a/cockpit/chat/generative-ui/angular/src/app/views/container.component.ts b/cockpit/chat/generative-ui/angular/src/app/views/container.component.ts new file mode 100644 index 000000000..29d5479a7 --- /dev/null +++ b/cockpit/chat/generative-ui/angular/src/app/views/container.component.ts @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { Component, input } from '@angular/core'; +import type { Spec } from '@json-render/core'; +import { RenderElementComponent } from '@cacheplane/render'; + +@Component({ + selector: 'app-container', + standalone: true, + imports: [RenderElementComponent], + template: ` +
+ @for (key of childKeys(); track key) { + + } +
+ `, +}) +export class ContainerComponent { + readonly childKeys = input([]); + readonly spec = input.required(); +} diff --git a/cockpit/chat/generative-ui/angular/src/app/views/stat-card.component.ts b/cockpit/chat/generative-ui/angular/src/app/views/stat-card.component.ts new file mode 100644 index 000000000..0d5407cb9 --- /dev/null +++ b/cockpit/chat/generative-ui/angular/src/app/views/stat-card.component.ts @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { Component, input } from '@angular/core'; + +@Component({ + selector: 'app-stat-card', + standalone: true, + template: ` +
+
{{ label() }}
+
{{ value() }}
+
+ `, +}) +export class StatCardComponent { + readonly label = input(''); + readonly value = input(''); +} diff --git a/cockpit/chat/generative-ui/angular/src/app/views/weather-card.component.ts b/cockpit/chat/generative-ui/angular/src/app/views/weather-card.component.ts new file mode 100644 index 000000000..9114640ec --- /dev/null +++ b/cockpit/chat/generative-ui/angular/src/app/views/weather-card.component.ts @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { Component, input } from '@angular/core'; + +@Component({ + selector: 'app-weather-card', + standalone: true, + template: ` +
+
+

{{ city() }}

+ {{ weatherEmoji() }} +
+
{{ temperature() }}°F
+
{{ condition() }}
+
+ `, +}) +export class WeatherCardComponent { + readonly city = input(''); + readonly temperature = input(0); + readonly condition = input(''); + + weatherEmoji(): string { + const c = this.condition().toLowerCase(); + if (c.includes('sun') || c.includes('clear')) return '☀️'; + if (c.includes('cloud') || c.includes('overcast')) return '☁️'; + if (c.includes('rain')) return '🌧️'; + if (c.includes('snow')) return '❄️'; + if (c.includes('storm') || c.includes('thunder')) return '⛈️'; + return '🌤️'; + } +} diff --git a/cockpit/chat/generative-ui/angular/src/environments/environment.development.ts b/cockpit/chat/generative-ui/angular/src/environments/environment.development.ts index 18c14185d..32ff7a3bb 100644 --- a/cockpit/chat/generative-ui/angular/src/environments/environment.development.ts +++ b/cockpit/chat/generative-ui/angular/src/environments/environment.development.ts @@ -1,5 +1,5 @@ export const environment = { production: false, - langGraphApiUrl: 'http://localhost:4508/api', - streamingAssistantId: 'c-generative-ui', + langGraphApiUrl: 'http://localhost:4310/api', + generativeUiAssistantId: 'generative_ui', }; diff --git a/cockpit/chat/generative-ui/angular/src/environments/environment.ts b/cockpit/chat/generative-ui/angular/src/environments/environment.ts index 2c727e253..1cdf59a2a 100644 --- a/cockpit/chat/generative-ui/angular/src/environments/environment.ts +++ b/cockpit/chat/generative-ui/angular/src/environments/environment.ts @@ -1,5 +1,5 @@ export const environment = { production: true, langGraphApiUrl: '/api', - streamingAssistantId: 'c-generative-ui', + generativeUiAssistantId: 'generative_ui', }; diff --git a/cockpit/chat/generative-ui/python/docs/guide.md b/cockpit/chat/generative-ui/python/docs/guide.md index 92f2ba78c..8a6c0ce60 100644 --- a/cockpit/chat/generative-ui/python/docs/guide.md +++ b/cockpit/chat/generative-ui/python/docs/guide.md @@ -1,76 +1,73 @@ -# Chat Generative UI with @cacheplane/chat +# Generative UI with Streaming Auto-Detection -Render dynamic UI components within chat messages using -ChatGenerativeUiComponent. The agent embeds JSON render specs -in responses that are rendered as live Angular components. +Render dynamic UI components within chat messages using the streaming +auto-detection pipeline. As tokens stream in, the system detects JSON, +parses it incrementally, and renders Angular components in real time. -Add generative UI to your chat interface using `ChatGenerativeUiComponent` -from `@cacheplane/chat` and `provideRender()` from `@cacheplane/render`. -Configure both providers to enable spec detection and rendering. +Add generative UI to your chat interface using `views()` from +`@cacheplane/chat`. Register view components and pass them to +`ChatComponent` via the `[views]` input. - + -Generative UI requires both `provideChat()` and `provideRender()`: - -```typescript -import { provideRender } from '@cacheplane/render'; -import { provideChat } from '@cacheplane/chat'; - -export const appConfig: ApplicationConfig = { - providers: [ - provideStreamResource({ apiUrl: environment.langGraphApiUrl }), - provideChat({}), - provideRender({}), - ], -}; -``` +Create Angular components for each UI type the agent can emit. +Each component uses `input()` signals to receive props from the +rendered spec. - + -Configure the backend agent to include JSON render specs in its -responses using fenced code blocks with the `render-spec` tag. +Use the `views()` function to map spec type names to Angular components: - - +```typescript +import { views } from '@cacheplane/chat'; -ChatGenerativeUiComponent automatically scans messages for render -spec code blocks and extracts them for rendering. +const myViews = views({ + weather_card: WeatherCardComponent, + stat_card: StatCardComponent, + container: ContainerComponent, +}); +``` - + -Use the component in your template alongside ChatComponent: +Pass the view map to `ChatComponent` via the `[views]` input: ```html - - + ``` - + -Register custom Angular components to handle specific spec types: - -```typescript -provideRender({ - registry: defineAngularRegistry({ - card: MyCardComponent, - chart: MyChartComponent, - }), -}) -``` +Instruct the LLM to respond with raw JSON following the Spec schema. +No code fences or markdown — just valid JSON so the streaming pipeline +can detect and parse it incrementally. +## How Streaming Auto-Detection Works + +1. **Token streaming** — The LLM streams response tokens to the client. +2. **ContentClassifier** — Inspects the incoming token buffer and detects + when the content is JSON rather than plain text or markdown. +3. **Partial JSON parser** — As JSON tokens arrive, a partial parser + builds an incremental parse tree without waiting for the full payload. +4. **ParseTreeStore** — Materializes the partial parse tree into a live + `Spec` object (elements map + root key) that updates on every chunk. +5. **Component rendering** — The `[views]` registry resolves each element + type to an Angular component, which renders incrementally as the spec + grows. + -Generative UI bridges the gap between conversational AI and rich -interactive interfaces — the agent can create forms, dashboards, -and visualizations on the fly. +Because detection and parsing happen on every streamed chunk, the user +sees UI components materialize progressively — cards appear and fill in +as the LLM generates the JSON structure. diff --git a/cockpit/chat/generative-ui/python/langgraph.json b/cockpit/chat/generative-ui/python/langgraph.json index 583df221b..cd12bd55e 100644 --- a/cockpit/chat/generative-ui/python/langgraph.json +++ b/cockpit/chat/generative-ui/python/langgraph.json @@ -1,6 +1,6 @@ { "graphs": { - "c-generative-ui": "./src/graph.py:graph" + "generative_ui": "./src/graph.py:graph" }, "dependencies": ["."], "python_version": "3.12", diff --git a/cockpit/chat/generative-ui/python/prompts/generative-ui.md b/cockpit/chat/generative-ui/python/prompts/generative-ui.md index a1991a980..98c06c985 100644 --- a/cockpit/chat/generative-ui/python/prompts/generative-ui.md +++ b/cockpit/chat/generative-ui/python/prompts/generative-ui.md @@ -1,22 +1,46 @@ -# Chat Generative UI Assistant +# Generative UI Assistant -You are an assistant that demonstrates dynamic UI generation within -chat responses using render specs. +You are a generative-UI assistant. You MUST respond with **raw JSON only** — no markdown, no code fences, no explanation text. Your entire response must be a single valid JSON object following the Spec format below. -When the user asks you to create a UI element, include a JSON render spec -in your response using a fenced code block with the `render-spec` language tag. -For example: +## Spec Schema -```render-spec +A **Spec** is a JSON object with two required top-level keys: + +``` +{ + "elements": { [key: string]: Element }, + "rootKey": string +} +``` + +An **Element** has: + +``` { - "type": "card", - "props": { "title": "Generated Card" }, - "children": [ - { "type": "text", "props": { "content": "This card was generated by the AI" } } - ] + "type": string, // component type name + "props": { ... }, // component-specific properties + "children?": string[] // ordered list of element keys (references into `elements`) } ``` -The frontend will detect these specs and render them as live Angular -components inline within the chat message. Explain what you are generating -and why in the surrounding text. +## Available Component Types + +| Type | Props | Children | +|-----------------|--------------------------------------------------------------|----------| +| `container` | *(none)* | Yes | +| `weather_card` | `city` (string), `temperature` (number), `condition` (string)| No | +| `stat_card` | `label` (string), `value` (string) | No | + +## Rules + +1. Respond ONLY with valid JSON. No markdown. No code fences. No surrounding text. +2. Every element referenced in a `children` array must exist as a key in `elements`. +3. `rootKey` must reference a key that exists in `elements`. +4. Use `container` to group multiple cards together. +5. Choose component types that best match the user's request. + +## Example Response + +If the user asks "What's the weather in Chicago and New York?", respond exactly like: + +{"elements":{"root":{"type":"container","props":{},"children":["chicago","nyc"]},"chicago":{"type":"weather_card","props":{"city":"Chicago","temperature":45,"condition":"Partly Cloudy"}},"nyc":{"type":"weather_card","props":{"city":"New York","temperature":52,"condition":"Sunny"}}},"rootKey":"root"} diff --git a/docs/superpowers/plans/2026-04-08-generative-ui-spike.md b/docs/superpowers/plans/2026-04-08-generative-ui-spike.md new file mode 100644 index 000000000..d05fde8ef --- /dev/null +++ b/docs/superpowers/plans/2026-04-08-generative-ui-spike.md @@ -0,0 +1,814 @@ +# Generative UI Spike — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a cockpit capability at `cockpit/langgraph/generative-ui/` that proves the streaming generative UI pipeline end-to-end — LLM streams JSON spec tokens → auto-detected → parsed → rendered as Angular components. + +**Architecture:** Follow the existing cockpit capability pattern (`cockpit/langgraph/streaming/` as template). Python graph instructs the LLM to return a JSON-render Spec. Angular frontend uses `ChatComponent` with `[views]` input — two simple view components (WeatherCard, StatCard). Registration via manifest + route-resolution. + +**Tech Stack:** LangGraph (Python), Angular 20+, `@cacheplane/chat`, `@cacheplane/render`, Tailwind CSS + +--- + +## File Structure + +### New Files (Python Backend) + +| File | Purpose | +|------|---------| +| `cockpit/langgraph/generative-ui/python/src/graph.py` | LangGraph graph that generates JSON-render Specs | +| `cockpit/langgraph/generative-ui/python/src/index.ts` | Module metadata | +| `cockpit/langgraph/generative-ui/python/prompts/generative-ui.md` | System prompt with Spec schema | +| `cockpit/langgraph/generative-ui/python/docs/guide.md` | Narrative docs | +| `cockpit/langgraph/generative-ui/python/langgraph.json` | LangGraph config | +| `cockpit/langgraph/generative-ui/python/project.json` | Nx project config | + +### New Files (Angular Frontend) + +| File | Purpose | +|------|---------| +| `cockpit/langgraph/generative-ui/angular/src/app/generative-ui.component.ts` | Main app component with ChatComponent + views | +| `cockpit/langgraph/generative-ui/angular/src/app/app.config.ts` | Angular providers | +| `cockpit/langgraph/generative-ui/angular/src/app/views/weather-card.component.ts` | WeatherCard view component | +| `cockpit/langgraph/generative-ui/angular/src/app/views/stat-card.component.ts` | StatCard view component | +| `cockpit/langgraph/generative-ui/angular/src/main.ts` | Bootstrap | +| `cockpit/langgraph/generative-ui/angular/src/environments/environment.ts` | Production env | +| `cockpit/langgraph/generative-ui/angular/src/environments/environment.development.ts` | Dev env | +| `cockpit/langgraph/generative-ui/angular/src/index.html` | HTML shell | +| `cockpit/langgraph/generative-ui/angular/src/styles.css` | Tailwind + design tokens | +| `cockpit/langgraph/generative-ui/angular/project.json` | Nx Angular app config | +| `cockpit/langgraph/generative-ui/angular/tsconfig.json` | TS config | +| `cockpit/langgraph/generative-ui/angular/tsconfig.app.json` | App TS config | +| `cockpit/langgraph/generative-ui/angular/proxy.conf.json` | Dev proxy | +| `cockpit/langgraph/generative-ui/angular/package.json` | NPM metadata | +| `cockpit/langgraph/generative-ui/angular/vercel.json` | Vercel build config | + +### Modified Files (Registration) + +| File | Change | +|------|--------| +| `libs/cockpit-registry/src/lib/manifest.ts` | Add `'generative-ui'` to langgraph core-capabilities | +| `apps/cockpit/src/lib/route-resolution.ts` | Import and register the module | + +--- + +### Task 1: Python Backend — Graph + Prompt + Config + +**Files:** +- Create: `cockpit/langgraph/generative-ui/python/src/graph.py` +- Create: `cockpit/langgraph/generative-ui/python/prompts/generative-ui.md` +- Create: `cockpit/langgraph/generative-ui/python/langgraph.json` +- Create: `cockpit/langgraph/generative-ui/python/src/index.ts` +- Create: `cockpit/langgraph/generative-ui/python/project.json` +- Create: `cockpit/langgraph/generative-ui/python/docs/guide.md` + +- [ ] **Step 1: Create the system prompt** + +Create `cockpit/langgraph/generative-ui/python/prompts/generative-ui.md`: + +````markdown +# Generative UI Assistant + +You are a helpful assistant that responds with structured JSON UI specifications. + +When the user asks about weather, locations, or data, respond with a JSON object that follows this exact schema: + +```json +{ + "root": "", + "elements": { + "": { + "type": "", + "props": { ... }, + "children": ["", ""] + } + } +} +``` + +## Available component types + +### `container` +A layout wrapper that renders its children vertically. +- Props: none required +- Children: array of element keys + +### `weather_card` +Displays weather information for a city. +- Props: + - `city` (string): City name + - `temperature` (number): Temperature in Fahrenheit + - `condition` (string): Weather condition (e.g., "Sunny", "Cloudy", "Rainy") + +### `stat_card` +Displays a single statistic. +- Props: + - `label` (string): What the stat measures (e.g., "Humidity", "Wind Speed") + - `value` (string): The formatted value (e.g., "65%", "12 mph") + +## Rules + +1. Always respond with ONLY a valid JSON object — no markdown, no explanation, no code fences +2. Use a `container` as the root element when you have multiple components +3. Give each element a unique key (e.g., "root", "weather", "stat-1", "stat-2") +4. Include 2-4 elements total for variety +5. Make the data realistic and varied + +## Example response + +For "What's the weather in Seattle?": + +```json +{ + "root": "root", + "elements": { + "root": { + "type": "container", + "props": {}, + "children": ["weather", "stat-humidity", "stat-wind"] + }, + "weather": { + "type": "weather_card", + "props": { + "city": "Seattle", + "temperature": 58, + "condition": "Overcast" + } + }, + "stat-humidity": { + "type": "stat_card", + "props": { + "label": "Humidity", + "value": "78%" + } + }, + "stat-wind": { + "type": "stat_card", + "props": { + "label": "Wind Speed", + "value": "8 mph NW" + } + } + } +} +``` +```` + +- [ ] **Step 2: Create the Python graph** + +Create `cockpit/langgraph/generative-ui/python/src/graph.py`: + +```python +""" +LangGraph Generative UI Graph + +A StateGraph that instructs the LLM to return JSON-render Spec objects. +The Angular frontend auto-detects the JSON and renders it as Angular +components via the streaming generative UI pipeline. +""" + +from pathlib import Path +from langgraph.graph import StateGraph, MessagesState, END +from langchain_openai import ChatOpenAI +from langchain_core.messages import SystemMessage + +PROMPTS_DIR = Path(__file__).parent.parent / "prompts" + + +def build_generative_ui_graph(): + llm = ChatOpenAI(model="gpt-4o-mini", streaming=True) + + async def generate(state: MessagesState) -> dict: + system_prompt = (PROMPTS_DIR / "generative-ui.md").read_text() + messages = [SystemMessage(content=system_prompt)] + state["messages"] + response = await llm.ainvoke(messages) + return {"messages": [response]} + + graph = StateGraph(MessagesState) + graph.add_node("generate", generate) + graph.set_entry_point("generate") + graph.add_edge("generate", END) + + return graph.compile() + + +graph = build_generative_ui_graph() +``` + +- [ ] **Step 3: Create langgraph.json** + +Create `cockpit/langgraph/generative-ui/python/langgraph.json`: + +```json +{ + "graphs": { + "generative_ui": "./src/graph.py:graph" + }, + "dependencies": ["."], + "python_version": "3.12", + "env": ".env" +} +``` + +- [ ] **Step 4: Create module metadata** + +Create `cockpit/langgraph/generative-ui/python/src/index.ts`: + +```typescript +export interface CockpitCapabilityModule { + id: string; + manifestIdentity: { + product: 'langgraph'; + section: 'core-capabilities'; + topic: 'generative-ui'; + page: 'overview'; + language: 'python'; + }; + title: string; + docsPath: string; + promptAssetPaths: string[]; + codeAssetPaths: string[]; + backendAssetPaths: string[]; + docsAssetPaths: string[]; + runtimeUrl?: string; + devPort?: number; +} + +export const langgraphGenerativeUiPythonModule: CockpitCapabilityModule = { + id: 'langgraph-generative-ui-python', + manifestIdentity: { + product: 'langgraph', + section: 'core-capabilities', + topic: 'generative-ui', + page: 'overview', + language: 'python', + }, + title: 'LangGraph Generative UI (Python)', + docsPath: '/docs/langgraph/core-capabilities/generative-ui/overview/python', + promptAssetPaths: ['cockpit/langgraph/generative-ui/python/prompts/generative-ui.md'], + codeAssetPaths: [ + 'cockpit/langgraph/generative-ui/angular/src/app/generative-ui.component.ts', + 'cockpit/langgraph/generative-ui/angular/src/app/app.config.ts', + 'cockpit/langgraph/generative-ui/angular/src/app/views/weather-card.component.ts', + 'cockpit/langgraph/generative-ui/angular/src/app/views/stat-card.component.ts', + ], + backendAssetPaths: [ + 'cockpit/langgraph/generative-ui/python/src/graph.py', + ], + docsAssetPaths: ['cockpit/langgraph/generative-ui/python/docs/guide.md'], + runtimeUrl: 'langgraph/generative-ui', + devPort: 4310, +}; +``` + +- [ ] **Step 5: Create Nx project config** + +Create `cockpit/langgraph/generative-ui/python/project.json`: + +```json +{ + "name": "cockpit-langgraph-generative-ui-python", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "cockpit/langgraph/generative-ui/python/src", + "projectType": "library", + "targets": { + "build": { + "executor": "@nx/js:tsc", + "outputs": ["{workspaceRoot}/dist/cockpit/langgraph/generative-ui/python"], + "options": { + "outputPath": "dist/cockpit/langgraph/generative-ui/python", + "main": "cockpit/langgraph/generative-ui/python/src/index.ts", + "tsConfig": "cockpit/langgraph/generative-ui/python/tsconfig.json" + } + }, + "smoke": { + "executor": "nx:run-commands", + "options": { + "cwd": "cockpit/langgraph/generative-ui/python", + "command": "npx tsx -e \"import { langgraphGenerativeUiPythonModule } from './src/index.ts'; const m = langgraphGenerativeUiPythonModule; if (m.id !== 'langgraph-generative-ui-python') { throw new Error('Unexpected module: ' + m.id); }\"" + } + } + } +} +``` + +- [ ] **Step 6: Create docs guide** + +Create `cockpit/langgraph/generative-ui/python/docs/guide.md`: + +````markdown +# Generative UI + +This example demonstrates streaming generative UI — an LLM returns JSON-render Specs that are auto-detected and rendered as Angular components in real time. + +## How It Works + +1. The LangGraph agent receives a user message +2. The LLM generates a JSON-render Spec as its response (not markdown) +3. Tokens stream to the Angular frontend via SSE +4. `ChatComponent` auto-detects the JSON via `ContentClassifier` +5. `@cacheplane/partial-json` parses the incomplete JSON character-by-character +6. `ParseTreeStore` materializes the parse tree into a `Spec` signal +7. `RenderSpecComponent` renders the spec using the view registry +8. Components update live as tokens arrive — string props grow visibly + +## View Components + +This example registers two view components: + +- **WeatherCard** — Displays city, temperature, and weather condition +- **StatCard** — Displays a label/value pair (humidity, wind speed, etc.) + +## Key Code + +```typescript +// Register views +const myViews = views({ + weather_card: WeatherCardComponent, + stat_card: StatCardComponent, + container: ContainerComponent, +}); + +// Pass to ChatComponent + +``` + +No manual JSON parsing, no content type detection, no spec wiring — the `ChatComponent` handles everything automatically. +```` + +- [ ] **Step 7: Create Python tsconfig.json** + +Create `cockpit/langgraph/generative-ui/python/tsconfig.json`: + +```json +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc" + }, + "include": ["src/**/*.ts"] +} +``` + +- [ ] **Step 8: Commit** + +```bash +git add cockpit/langgraph/generative-ui/python/ +git commit -m "feat(cockpit): add generative UI Python graph and module metadata" +``` + +--- + +### Task 2: Angular Frontend — View Components + App + +**Files:** +- Create: All files under `cockpit/langgraph/generative-ui/angular/` + +- [ ] **Step 1: Create WeatherCardComponent** + +Create `cockpit/langgraph/generative-ui/angular/src/app/views/weather-card.component.ts`: + +```typescript +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { Component, input } from '@angular/core'; + +@Component({ + selector: 'app-weather-card', + standalone: true, + template: ` +
+
+

{{ city() }}

+ {{ weatherEmoji() }} +
+
{{ temperature() }}°F
+
{{ condition() }}
+
+ `, +}) +export class WeatherCardComponent { + readonly city = input(''); + readonly temperature = input(0); + readonly condition = input(''); + + weatherEmoji(): string { + const c = this.condition().toLowerCase(); + if (c.includes('sun') || c.includes('clear')) return '☀️'; + if (c.includes('cloud') || c.includes('overcast')) return '☁️'; + if (c.includes('rain')) return '🌧️'; + if (c.includes('snow')) return '❄️'; + if (c.includes('storm') || c.includes('thunder')) return '⛈️'; + return '🌤️'; + } +} +``` + +- [ ] **Step 2: Create StatCardComponent** + +Create `cockpit/langgraph/generative-ui/angular/src/app/views/stat-card.component.ts`: + +```typescript +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { Component, input } from '@angular/core'; + +@Component({ + selector: 'app-stat-card', + standalone: true, + template: ` +
+
{{ label() }}
+
{{ value() }}
+
+ `, +}) +export class StatCardComponent { + readonly label = input(''); + readonly value = input(''); +} +``` + +- [ ] **Step 3: Create ContainerComponent** + +Create `cockpit/langgraph/generative-ui/angular/src/app/views/container.component.ts`: + +```typescript +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { Component, input } from '@angular/core'; +import type { Spec } from '@json-render/core'; +import { RenderElementComponent } from '@cacheplane/render'; + +@Component({ + selector: 'app-container', + standalone: true, + imports: [RenderElementComponent], + template: ` +
+ @for (key of childKeys(); track key) { + + } +
+ `, +}) +export class ContainerComponent { + readonly childKeys = input([]); + readonly spec = input.required(); +} +``` + +- [ ] **Step 4: Create main app component** + +Create `cockpit/langgraph/generative-ui/angular/src/app/generative-ui.component.ts`: + +```typescript +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { Component } from '@angular/core'; +import { ChatComponent, views } from '@cacheplane/chat'; +import { agent } from '@cacheplane/angular'; +import { environment } from '../environments/environment'; +import { WeatherCardComponent } from './views/weather-card.component'; +import { StatCardComponent } from './views/stat-card.component'; +import { ContainerComponent } from './views/container.component'; + +const myViews = views({ + weather_card: WeatherCardComponent, + stat_card: StatCardComponent, + container: ContainerComponent, +}); + +@Component({ + selector: 'app-generative-ui', + standalone: true, + imports: [ChatComponent], + template: ``, +}) +export class GenerativeUiComponent { + protected readonly agentRef = agent({ + apiUrl: environment.langGraphApiUrl, + assistantId: environment.generativeUiAssistantId, + }); + + protected readonly myViews = myViews; +} +``` + +- [ ] **Step 5: Create app config** + +Create `cockpit/langgraph/generative-ui/angular/src/app/app.config.ts`: + +```typescript +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { ApplicationConfig } from '@angular/core'; +import { provideAgent } from '@cacheplane/angular'; +import { provideChat } from '@cacheplane/chat'; +import { environment } from '../environments/environment'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideAgent({ apiUrl: environment.langGraphApiUrl }), + provideChat({}), + ], +}; +``` + +- [ ] **Step 6: Create bootstrap + environments + config files** + +Create `cockpit/langgraph/generative-ui/angular/src/main.ts`: + +```typescript +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { bootstrapApplication } from '@angular/platform-browser'; +import { appConfig } from './app/app.config'; +import { GenerativeUiComponent } from './app/generative-ui.component'; + +bootstrapApplication(GenerativeUiComponent, appConfig).catch(console.error); +``` + +Create `cockpit/langgraph/generative-ui/angular/src/environments/environment.ts`: + +```typescript +export const environment = { + production: true, + langGraphApiUrl: '/api', + generativeUiAssistantId: 'generative_ui', +}; +``` + +Create `cockpit/langgraph/generative-ui/angular/src/environments/environment.development.ts`: + +```typescript +export const environment = { + production: false, + langGraphApiUrl: 'http://localhost:4310/api', + generativeUiAssistantId: 'generative_ui', +}; +``` + +Create `cockpit/langgraph/generative-ui/angular/src/index.html`: + +```html + + + + + LangGraph Generative UI — Angular + + + + + + + + +``` + +Create `cockpit/langgraph/generative-ui/angular/src/styles.css` (copy streaming pattern): + +```css +@import "../../../../../libs/design-tokens/src/lib/tokens.css"; +@import "tailwindcss"; +@source "../../../../../libs/chat/src/"; + +@theme { + --color-bg: var(--ds-bg); + --color-surface: #ffffff; + --color-accent: var(--ds-accent); + --color-accent-light: var(--ds-accent-light); + --color-text-primary: var(--ds-text-primary); + --color-text-secondary: var(--ds-text-secondary); + --color-text-muted: var(--ds-text-muted); + --color-border: var(--ds-accent-border); + --color-error: #ef4444; + --color-success: #22c55e; + --font-sans: var(--ds-font-sans); + --font-serif: var(--ds-font-serif); + --font-mono: var(--ds-font-mono); +} + +*, *::before, *::after { box-sizing: border-box; } + +body { + margin: 0; + font-family: var(--ds-font-sans); + background: var(--ds-bg); + color: var(--ds-text-primary); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +``` + +Create `cockpit/langgraph/generative-ui/angular/proxy.conf.json`: + +```json +{ + "/api": { + "target": "http://localhost:8123", + "secure": false, + "changeOrigin": true, + "pathRewrite": { "^/api": "" }, + "ws": true + } +} +``` + +Create `cockpit/langgraph/generative-ui/angular/package.json`: + +```json +{ + "name": "@cacheplane/cockpit-langgraph-generative-ui-angular", + "version": "0.0.1", + "peerDependencies": { + "@cacheplane/chat": "^0.0.1", + "@cacheplane/render": "^0.0.1", + "@cacheplane/angular": "^0.0.1", + "@json-render/core": "^0.16.0", + "@langchain/langgraph-sdk": "^0.0.36" + }, + "license": "PolyForm-Noncommercial-1.0.0", + "sideEffects": false +} +``` + +Create `cockpit/langgraph/generative-ui/angular/vercel.json`: + +```json +{ + "$schema": "https://openapi.vercel.sh/vercel.json", + "buildCommand": "npx nx build cockpit-langgraph-generative-ui-angular", + "outputDirectory": "dist/cockpit/langgraph/generative-ui/angular/browser", + "framework": null +} +``` + +Create `cockpit/langgraph/generative-ui/angular/tsconfig.json`: + +```json +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "noPropertyAccessFromIndexSignature": false, + "experimentalDecorators": true, + "module": "preserve", + "emitDeclarationOnly": false, + "composite": false, + "lib": ["es2022", "dom"], + "skipLibCheck": true, + "strict": false + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": false, + "strictInputAccessModifiers": false, + "strictTemplates": false + }, + "files": [], + "include": [], + "references": [ + { "path": "./tsconfig.app.json" } + ] +} +``` + +Create `cockpit/langgraph/generative-ui/angular/tsconfig.app.json`: + +```json +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "lib": ["es2022", "dom"], + "types": [], + "emitDeclarationOnly": false + }, + "files": ["src/main.ts"], + "include": ["src/**/*.ts"] +} +``` + +Create `cockpit/langgraph/generative-ui/angular/project.json`: + +```json +{ + "name": "cockpit-langgraph-generative-ui-angular", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "cockpit/langgraph/generative-ui/angular/src", + "projectType": "application", + "targets": { + "build": { + "executor": "@angular/build:application", + "outputs": ["{options.outputPath.base}"], + "options": { + "outputPath": { + "base": "dist/cockpit/langgraph/generative-ui/angular", + "browser": "" + }, + "browser": "cockpit/langgraph/generative-ui/angular/src/main.ts", + "tsConfig": "cockpit/langgraph/generative-ui/angular/tsconfig.app.json", + "styles": ["cockpit/langgraph/generative-ui/angular/src/styles.css"] + }, + "configurations": { + "production": { + "budgets": [ + { "type": "initial", "maximumWarning": "500kb", "maximumError": "1mb" }, + { "type": "anyComponentStyle", "maximumWarning": "4kb", "maximumError": "8kb" } + ], + "outputHashing": "none" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true, + "fileReplacements": [ + { + "replace": "cockpit/langgraph/generative-ui/angular/src/environments/environment.ts", + "with": "cockpit/langgraph/generative-ui/angular/src/environments/environment.development.ts" + } + ] + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "continuous": true, + "executor": "@angular/build:dev-server", + "configurations": { + "production": { "buildTarget": "cockpit-langgraph-generative-ui-angular:build:production" }, + "development": { "buildTarget": "cockpit-langgraph-generative-ui-angular:build:development" } + }, + "defaultConfiguration": "development", + "options": { + "proxyConfig": "cockpit/langgraph/generative-ui/angular/proxy.conf.json" + } + } + } +} +``` + +- [ ] **Step 7: Commit** + +```bash +git add cockpit/langgraph/generative-ui/angular/ +git commit -m "feat(cockpit): add generative UI Angular frontend with view components" +``` + +--- + +### Task 3: Registration — Manifest + Route Resolution + +**Files:** +- Modify: `libs/cockpit-registry/src/lib/manifest.ts` +- Modify: `apps/cockpit/src/lib/route-resolution.ts` + +- [ ] **Step 1: Add generative-ui to APPROVED_TOPICS** + +In `libs/cockpit-registry/src/lib/manifest.ts`, add `'generative-ui'` to the langgraph core-capabilities array: + +```typescript +langgraph: { + 'getting-started': ['overview'], + 'core-capabilities': [ + 'persistence', + 'durable-execution', + 'streaming', + 'interrupts', + 'memory', + 'subgraphs', + 'time-travel', + 'deployment-runtime', + 'generative-ui', + ], +}, +``` + +- [ ] **Step 2: Register module in route-resolution.ts** + +Add import at the top: + +```typescript +import { langgraphGenerativeUiPythonModule } from '../../../../cockpit/langgraph/generative-ui/python/src/index'; +``` + +Add to the `capabilityModules` array: + +```typescript +const capabilityModules = [ + // ... existing modules ... + langgraphGenerativeUiPythonModule, +]; +``` + +- [ ] **Step 3: Verify smoke test passes** + +Run: `export PATH="/Users/blove/.nvm/versions/node/v22.14.0/bin:$PATH" && npx nx smoke cockpit-langgraph-generative-ui-python` +Expected: PASS + +- [ ] **Step 4: Verify Angular build succeeds** + +Run: `export PATH="/Users/blove/.nvm/versions/node/v22.14.0/bin:$PATH" && npx nx build cockpit-langgraph-generative-ui-angular` +Expected: BUILD SUCCESS + +- [ ] **Step 5: Commit** + +```bash +git add libs/cockpit-registry/ apps/cockpit/ +git commit -m "feat(cockpit): register generative-ui capability in manifest and routes" +``` diff --git a/docs/superpowers/specs/2026-04-08-generative-ui-spike-design.md b/docs/superpowers/specs/2026-04-08-generative-ui-spike-design.md new file mode 100644 index 000000000..309426dab --- /dev/null +++ b/docs/superpowers/specs/2026-04-08-generative-ui-spike-design.md @@ -0,0 +1,53 @@ +# Generative UI Spike — Design Spec + +**Date:** 2026-04-08 +**Status:** Draft + +## Goal + +Prove the streaming generative UI pipeline end-to-end: LangGraph agent returns a JSON-render Spec as AI message content → tokens stream → `ContentClassifier` auto-detects JSON → `PartialJsonParser` builds tree → `ParseTreeStore` materializes Spec → `` renders Angular components that update live as tokens arrive. + +## Approach + +Add a new cockpit capability under `cockpit/langgraph/generative-ui/` following the existing capability pattern. The agent returns a full JSON-render Spec as its message content (not as a tool call result). The Angular frontend uses `ChatComponent` with `[views]` input — relying entirely on auto-detection. + +## Python Graph + +A single-node LangGraph graph (`MessagesState`). The node calls an LLM with a system prompt that instructs it to respond with a JSON-render Spec containing 2-3 elements: a `WeatherCard` and one or two `StatCard` components. The LLM streams the JSON token-by-token. + +The system prompt includes the exact Spec schema and available component types so the LLM knows what to generate. + +## Angular Frontend + +### View Components + +**WeatherCard** — Displays city name, temperature, condition, and an icon. Inputs: `city: string`, `temperature: number`, `condition: string`. + +**StatCard** — Displays a label and value (e.g., "Humidity: 65%"). Inputs: `label: string`, `value: string`. + +Both are simple standalone components with Tailwind styling. + +### App Component + +Uses `ChatComponent` with `[views]` input passing a `ViewRegistry` mapping `weather_card` → `WeatherCardComponent` and `stat_card` → `StatCardComponent`. + +## Registration + +- Add `generative-ui` topic to the `langgraph` product's `core-capabilities` section in the cockpit manifest +- Create module metadata (`CockpitCapabilityModule`) in the Python `src/index.ts` +- Register in `route-resolution.ts`'s `capabilityModules` array + +## What It Proves + +1. The parse tree correctly builds from character-by-character LLM output +2. Materialization produces valid `Spec` objects that `` can render +3. Structural sharing works — unchanged elements keep references, render lib skips re-render +4. Character-level prop streaming is visible in the rendered UI (e.g., city name filling in letter by letter) +5. The full pipeline works without any manual wiring — just `[views]` input on `ChatComponent` + +## What It Defers + +- Real-world data (Phase 2: analytics dashboard with charts, data grids, tool calling) +- A2UI support +- Production error handling for malformed JSON +- Interactive state store integration