Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .claude/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@
"runtimeArgs": ["-c", "export PATH=/Users/blove/.nvm/versions/node/v22.14.0/bin:$PATH && npx nx serve cockpit-langgraph-streaming-angular --port 4300"],
"port": 4300
},
{
"name": "ag-ui-streaming",
"runtimeExecutable": "/bin/bash",
"runtimeArgs": ["-c", "export PATH=/Users/blove/.nvm/versions/node/v22.14.0/bin:$PATH && npx nx serve cockpit-ag-ui-streaming-angular --port $PORT"],
"port": 4350,
"autoPort": true
},
{
"name": "examples-chat",
"runtimeExecutable": "/bin/bash",
Expand Down

Large diffs are not rendered by default.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
120 changes: 91 additions & 29 deletions apps/website/src/app/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { tokens } from '@ngaf/design-tokens';
import { MdxRenderer } from '../../../components/docs/MdxRenderer';
import { DocsTOC } from '../../../components/docs/DocsTOC';
import { AuthorByline } from '../../../components/blog/AuthorByline';
import { TagChips } from '../../../components/blog/TagChips';
import { Eyebrow } from '../../../components/ui/Eyebrow';
import { getAllPosts, getPostBySlug } from '../../../lib/blog';
import { getAuthor } from '../../../lib/blog-authors';
import { extractHeadings } from '../../../lib/extract-headings';
import { createPageMetadata } from '../../../lib/site-metadata';

interface Params {
Expand All @@ -28,40 +33,97 @@ export async function generateMetadata({ params }: Params): Promise<Metadata> {
});
}

function formatDate(iso: string): string {
// Parse YYYY-MM-DD as a date in UTC, then format using UTC parts to avoid
// timezone shifts (e.g. "2026-05-21" rendering as "May 20" west of UTC).
const d = new Date(`${iso}T00:00:00Z`);
return d.toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
timeZone: 'UTC',
});
}

function readingTimeMin(markdown: string): number {
const words = markdown
.replace(/```[\s\S]*?```/g, '') // strip code fences (not real reading)
.replace(/[#*_`>-]/g, ' ')
.split(/\s+/)
.filter(Boolean).length;
return Math.max(1, Math.round(words / 220));
}

export default async function BlogPostPage({ params }: Params) {
const { slug } = await params;
const post = getPostBySlug(slug);
if (!post || post.frontmatter.draft) notFound();
const author = getAuthor(post.frontmatter.author);
const minutes = readingTimeMin(post.content);
const primaryTag = post.frontmatter.tags?.[0]
? post.frontmatter.tags[0].toUpperCase()
: 'POST';
const headings = extractHeadings(post.content);

return (
<article style={{ maxWidth: 768, margin: '0 auto', padding: '64px 24px' }}>
<header style={{ marginBottom: 32 }}>
<time
dateTime={post.frontmatter.date}
style={{ fontSize: 14, opacity: 0.7 }}
>
{post.frontmatter.date}
</time>
<h1
style={{
fontSize: 44,
fontWeight: 600,
letterSpacing: '-0.02em',
margin: '12px 0 16px',
lineHeight: 1.15,
}}
>
{post.frontmatter.title}
</h1>
<AuthorByline author={author} />
</header>
<MdxRenderer
source={post.content}
library="agent"
section="blog"
slug={post.slug}
title={post.frontmatter.title}
/>
</article>
<div style={{ paddingTop: 80, background: tokens.surfaces.canvas, minHeight: '100vh' }}>
<div
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'flex-start',
maxWidth: 1280,
margin: '0 auto',
gap: 0,
}}
>
<article style={{ width: '100%', maxWidth: 768, padding: '64px 24px', flexShrink: 0 }}>
<header style={{ marginBottom: 48 }}>
<Eyebrow tone="accent" style={{ marginBottom: 24 }}>
{primaryTag} · {formatDate(post.frontmatter.date)} · {minutes} min read
</Eyebrow>
<h1
style={{
fontFamily: tokens.typography.h1.family,
fontSize: tokens.typography.h1.size,
lineHeight: tokens.typography.h1.line,
fontWeight: 700,
letterSpacing: '-0.02em',
color: tokens.colors.textPrimary,
margin: '0 0 24px',
}}
>
{post.frontmatter.title}
</h1>
<p
style={{
fontFamily: tokens.typography.bodyLg.family,
fontSize: tokens.typography.bodyLg.size,
lineHeight: tokens.typography.bodyLg.line,
color: tokens.colors.textSecondary,
margin: '0 0 32px',
maxWidth: '60ch',
}}
>
{post.frontmatter.description}
</p>
<div style={{ display: 'flex', alignItems: 'center', gap: 20, flexWrap: 'wrap', marginBottom: 20 }}>
<AuthorByline author={author} />
</div>
{post.frontmatter.tags && post.frontmatter.tags.length > 0 ? (
<TagChips tags={post.frontmatter.tags} />
) : null}
</header>
<MdxRenderer
source={post.content}
library="agent"
section="blog"
slug={post.slug}
title={post.frontmatter.title}
/>
</article>
<DocsTOC headings={headings} />
</div>
</div>
);
}
16 changes: 9 additions & 7 deletions apps/website/src/app/docs/[library]/[section]/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,15 @@ export default async function DocsPage({ params }: DocsRouteProps) {
<div className="px-6 md:px-12 pt-6">
<DocsBreadcrumb library={library as LibraryId} section={section} slug={slug} title={doc.title} />
</div>
<MdxRenderer
source={doc.content}
library={library as LibraryId}
section={section}
slug={slug}
title={doc.title}
/>
<article className="flex-1 py-8 px-4 sm:px-6 md:px-12 md:max-w-3xl overflow-x-hidden">
<MdxRenderer
source={doc.content}
library={library as LibraryId}
section={section}
slug={slug}
title={doc.title}
/>
</article>
{section === 'api' && (() => {
const entries = loadApiDocs(library);
const nameMap = API_NAME_MAP[library] ?? {};
Expand Down
25 changes: 23 additions & 2 deletions apps/website/src/app/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ html {
padding: 1.25rem 1.5rem;
border-radius: 0.75rem;
border: 1px solid rgba(0, 0, 0, 0.1);
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08), 0 0 0 1px rgba(0, 0, 0, 0.04);
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
overflow-x: auto;
font-size: 0.8rem;
line-height: 1.7;
Expand Down Expand Up @@ -102,8 +102,11 @@ html {
.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); }
.docs-prose p { line-height: 1.75; margin-bottom: 1.25rem; }
.docs-prose ul, .docs-prose ol { margin-bottom: 1.25rem; }
.docs-prose ul, .docs-prose ol { margin-bottom: 1.25rem; padding-left: 1.5rem; }
.docs-prose ul { list-style: disc; }
.docs-prose ol { list-style: decimal; }
.docs-prose li { margin-bottom: 0.25rem; }
.docs-prose li::marker { color: var(--color-text-muted, #555770); }

.docs-table-scroll { max-width: 100%; overflow-x: auto; margin: 1.5rem 0; }
.docs-prose table { width: 100%; border-collapse: collapse; font-size: 0.875rem; margin: 0; }
Expand Down Expand Up @@ -139,6 +142,24 @@ html {
border-radius: var(--radius-sm);
}

/* AG-UI architecture diagram */
.ag-ui-arch-grid {
display: grid;
grid-template-columns: 1fr auto 1fr auto 1fr;
align-items: stretch;
gap: 0;
}
@media (max-width: 720px) {
.ag-ui-arch-grid {
grid-template-columns: 1fr;
}
.ag-ui-arch-arrow {
transform: rotate(90deg);
padding: 12px 0;
margin: 4px auto;
}
}

/* Docs — readable column max-width */
.docs-prose {
max-width: 70ch;
Expand Down
148 changes: 148 additions & 0 deletions apps/website/src/components/docs/AgUiArchDiagram.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { tokens } from '@ngaf/design-tokens';

interface BoxProps {
eyebrow: string;
title: string;
meta: string;
tone?: 'neutral' | 'accent';
}

function Box({ eyebrow, title, meta, tone = 'neutral' }: BoxProps) {
const isAccent = tone === 'accent';
return (
<div
style={{
background: isAccent ? tokens.colors.accentSurface : tokens.surfaces.surface,
border: `1px solid ${isAccent ? tokens.colors.accent + '33' : tokens.surfaces.border}`,
borderRadius: tokens.radius.lg,
padding: '20px 22px',
display: 'flex',
flexDirection: 'column',
gap: 6,
minHeight: 116,
}}
>
<span
style={{
fontFamily: tokens.typography.eyebrow.family,
fontSize: '0.7rem',
fontWeight: 600,
letterSpacing: '0.08em',
textTransform: 'uppercase',
color: isAccent ? tokens.colors.accent : tokens.colors.textMuted,
}}
>
{eyebrow}
</span>
<span
style={{
fontFamily: tokens.typography.fontMono,
fontSize: '0.95rem',
fontWeight: 600,
color: tokens.colors.textPrimary,
}}
>
{title}
</span>
<span
style={{
fontSize: '0.85rem',
lineHeight: 1.5,
color: tokens.colors.textSecondary,
}}
>
{meta}
</span>
</div>
);
}

function ArrowLabel({ label, sub }: { label: string; sub: string }) {
return (
<div
aria-hidden
className="ag-ui-arch-arrow"
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: 4,
color: tokens.colors.textMuted,
padding: '0 4px',
}}
>
<span
style={{
fontFamily: tokens.typography.eyebrow.family,
fontSize: '0.65rem',
fontWeight: 600,
letterSpacing: '0.08em',
textTransform: 'uppercase',
color: tokens.colors.accent,
whiteSpace: 'nowrap',
}}
>
{label}
</span>
<svg width="44" height="14" viewBox="0 0 44 14" fill="none" style={{ display: 'block' }}>
<path
d="M2 7 H36 M30 2 L36 7 L30 12"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
<span style={{ fontSize: '0.72rem', color: tokens.colors.textMuted, whiteSpace: 'nowrap' }}>
{sub}
</span>
</div>
);
}

export function AgUiArchDiagram() {
return (
<figure
style={{
margin: '2rem 0',
padding: '28px 24px',
background: tokens.surfaces.canvas,
border: `1px solid ${tokens.surfaces.border}`,
borderRadius: tokens.radius.lg,
}}
>
<div className="ag-ui-arch-grid">
<Box
eyebrow="Backend"
title="Agent runtime"
meta="LangGraph, CrewAI, Mastra, MS Agent Fwk, Pydantic AI, …"
/>
<ArrowLabel label="AG-UI" sub="SSE" />
<Box
eyebrow="Adapter"
title="@ngaf/ag-ui"
meta="Signal-driven reducer over AG-UI events."
tone="accent"
/>
<ArrowLabel label="Agent contract" sub="signals" />
<Box
eyebrow="Chat UI"
title="@ngaf/chat"
meta="<chat [agent]='…' /> + slots + themes."
/>
</div>
<figcaption
style={{
marginTop: 16,
fontSize: '0.8rem',
color: tokens.colors.textMuted,
textAlign: 'center',
fontStyle: 'italic',
}}
>
Backend speaks AG-UI over SSE → adapter exposes a signal-shaped Agent contract → chat UI renders.
</figcaption>
</figure>
);
}
Loading
Loading