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
243 changes: 243 additions & 0 deletions docs/components/mdx/mermaid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
"use client";

import { useTheme } from "next-themes";
import { use, useCallback, useEffect, useId, useRef, useState, useSyncExternalStore } from "react";

const MIN_ZOOM = 0.1;
const MAX_ZOOM = 4;
const ZOOM_STEP = 0.2;

function subscribe() {
return () => {};
}

function useIsClient() {
return useSyncExternalStore(
subscribe,
() => true,
() => false,
);
}

const cache = new Map<string, Promise<unknown>>();

function cachePromise<T>(key: string, setPromise: () => Promise<T>): Promise<T> {
const cached = cache.get(key);
if (cached) return cached as Promise<T>;
const promise = setPromise();
cache.set(key, promise);
return promise;
}

export function Mermaid({ chart, initialZoom = 1 }: { chart: string; initialZoom?: number }) {
const isClient = useIsClient();
if (!isClient) return null;
return <MermaidContent chart={chart} initialZoom={initialZoom} />;
}

function MermaidContent({ chart, initialZoom = 1 }: { chart: string; initialZoom?: number }) {
const id = useId();
const { resolvedTheme } = useTheme();
const isDark = resolvedTheme === "dark";

const [zoom, setZoom] = useState(initialZoom);
const [position, setPosition] = useState({ x: 0, y: 0 });
const [isDragging, setIsDragging] = useState(false);
const [hasInteracted, setHasInteracted] = useState(false);

const containerRef = useRef<HTMLDivElement>(null);
const svgRef = useRef<HTMLDivElement>(null);
const dragStart = useRef({ x: 0, y: 0 });
const positionRef = useRef(position);
const zoomRef = useRef(zoom);

useEffect(() => {
positionRef.current = position;
}, [position]);
useEffect(() => {
zoomRef.current = zoom;
}, [zoom]);

const { default: mermaid } = use(cachePromise("mermaid", () => import("mermaid")));

mermaid.initialize({
startOnLoad: false,
securityLevel: "loose",
fontFamily: "inherit",
theme: isDark ? "dark" : "default",
});

const { svg, bindFunctions } = use(
cachePromise(`${chart}-${resolvedTheme}`, () =>
mermaid.render(id, chart.replaceAll("\\n", "\n")),
),
);

useEffect(() => {
const el = containerRef.current;
if (!el) return;
const onWheel = (e: WheelEvent) => {
e.preventDefault();
setHasInteracted(true);
if (e.ctrlKey || e.metaKey) {
const delta = e.deltaY > 0 ? -ZOOM_STEP : ZOOM_STEP;
setZoom((prev) => Math.min(Math.max(prev + delta, MIN_ZOOM), MAX_ZOOM));
} else {
setPosition((prev) => ({
x: prev.x - e.deltaX,
y: prev.y - e.deltaY,
}));
}
};
el.addEventListener("wheel", onWheel, { passive: false });
return () => el.removeEventListener("wheel", onWheel);
}, []);

// Drag to pan
const handleMouseDown = useCallback((e: React.MouseEvent) => {
if (e.button !== 0) return;
e.stopPropagation();
setIsDragging(true);
setHasInteracted(true);
dragStart.current = {
x: e.clientX - positionRef.current.x,
y: e.clientY - positionRef.current.y,
};
}, []);

const handleMouseMove = useCallback(
(e: React.MouseEvent) => {
if (!isDragging) return;
setPosition({
x: e.clientX - dragStart.current.x,
y: e.clientY - dragStart.current.y,
});
},
[isDragging],
);

const handleMouseUp = useCallback(() => setIsDragging(false), []);

// Styles
const border = isDark ? "#27272a" : "#e4e4e7";
const btnBg = isDark ? "#18181b" : "#ffffff";
const btnColor = isDark ? "#a1a1aa" : "#52525b";
const badgeBg = isDark ? "#18181b" : "#f4f4f5";
const badgeColor = isDark ? "#a1a1aa" : "#52525b";

const btnBase: React.CSSProperties = {
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
borderRadius: 6,
border: `1px solid ${border}`,
background: btnBg,
color: btnColor,
cursor: "pointer",
userSelect: "none",
};

return (
<div
style={{
position: "relative",
margin: "1.5rem 0",
borderRadius: 8,
border: `1px solid ${border}`,
overflow: "hidden",
}}
>
{/* Top-right controls */}
<div style={{ position: "absolute", top: 8, right: 8, zIndex: 10 }}>
<button
style={{ ...btnBase, width: "auto", padding: "0 10px", fontSize: 11 }}
onClick={() => {
setZoom(initialZoom);
setPosition({ x: 0, y: 0 });
}}
title="Reset"
>
Reset
</button>
</div>

{/* Canvas */}
<div
ref={containerRef}
style={{
width: "100%",
aspectRatio: "16/9",
minHeight: 260,
overflow: "hidden",
cursor: isDragging ? "grabbing" : "grab",
display: "flex",
alignItems: "center",
justifyContent: "center",
touchAction: "none",
userSelect: "none",
}}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
>
<div
ref={(el) => {
(svgRef as React.MutableRefObject<HTMLDivElement | null>).current = el;
if (el) bindFunctions?.(el);
}}
dangerouslySetInnerHTML={{ __html: svg }}
style={{
transform: `translate(${position.x}px, ${position.y}px) scale(${zoom})`,
transformOrigin: "center center",
transition: isDragging ? "none" : "transform 0.15s ease-out",
maxWidth: "100%",
maxHeight: "100%",
userSelect: "none",
}}
/>
</div>

{/* Bottom-left zoom badge + hint */}
<div
style={{
position: "absolute",
bottom: 8,
left: 8,
display: "flex",
flexDirection: "row",
gap: 4,
zIndex: 10,
pointerEvents: "none",
}}
>
<span
style={{
padding: "3px 8px",
borderRadius: 6,
border: `1px solid ${border}`,
background: badgeBg,
color: badgeColor,
fontSize: 11,
}}
>
{Math.round(zoom * 100)}%
</span>
{!hasInteracted && (
<span
style={{
padding: "3px 8px",
borderRadius: 6,
border: `1px solid ${border}`,
background: badgeBg,
color: badgeColor,
fontSize: 10,
}}
>
Scroll to pan · Pinch or Ctrl+Scroll to zoom
</span>
)}
</div>
</div>
);
}
23 changes: 22 additions & 1 deletion docs/content/docs/chat/api-contract.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,28 @@ const myCustomFormat = {
};
```

{/* add visual: flow-chart showing how messageFormat.toApi affects outgoing chat and thread-create requests, and how messageFormat.fromApi affects thread loading */}
<Mermaid initialZoom={1.25} chart={`flowchart TD
subgraph Converters["Built-in Converters"]
C1["Default — AG-UI message shape"]
C2["openAIMessageFormat — OpenAI chat messages"]
C3["openAIConversationMessageFormat — OpenAI Responses items"]
end

Converters -->|"assigned to messageFormat prop"| MF["messageFormat"]

MF -->|"toApi(messages)"| OUT
MF -->|"fromApi(items)"| IN

subgraph OUT["Outgoing — messageFormat.toApi()"]
O1["Chat request — apiUrl or processMessage"]
O2["Thread create — threadApiUrl/create"]
end

subgraph IN["Incoming — messageFormat.fromApi()"]
I1["Thread load — threadApiUrl/get/:id"]
end

`} />

## Related guides

Expand Down
23 changes: 22 additions & 1 deletion docs/content/docs/chat/connecting.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,28 @@ Once you know what each prop does, the decision becomes:
3. Add `streamProtocol` only if your backend does not stream the default OpenUI Protocol.
4. Add `messageFormat` only if your backend expects or returns a non-default message shape.

{/* add visual: flow-chart showing the decision between apiUrl and processMessage, then mapping backend stream output to the correct streamProtocol adapter and messageFormat choice */}
<Mermaid initialZoom={1.24} chart={`flowchart TD
START(["Start"])

START --> Q1{"Need auth, extra fields,\\nor custom request body?"}

Q1 -->|"No"| AU["apiUrl\\nsimplest path"]
Q1 -->|"Yes"| PM["processMessage\\nfull request control"]

AU & PM --> Q2{"What does your\\nbackend stream?"}

Q2 -->|"OpenUI Protocol\\n(default)"| SP0["No streamProtocol needed"]
Q2 -->|"OpenAI SDK\\ntoReadableStream / NDJSON"| SP1["openAIReadableStreamAdapter()"]
Q2 -->|"Raw OpenAI SSE"| SP2["openAIAdapter()"]
Q2 -->|"OpenAI Responses API"| SP3["openAIResponsesAdapter()"]

SP0 & SP1 & SP2 & SP3 --> Q3{"What message shape\\ndoes your backend expect?"}

Q3 -->|"AG-UI shape\\n(default)"| MF0["No messageFormat needed"]
Q3 -->|"OpenAI chat messages"| MF1["openAIMessageFormat"]
Q3 -->|"OpenAI Responses items"| MF2["openAIConversationMessageFormat"]

`} />

## Rules summary

Expand Down
23 changes: 22 additions & 1 deletion docs/content/docs/chat/custom-ui-guide.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,28 @@ This example uses the same backend assumptions as the built-in layouts:

If you want Generative UI in a headless build, you also need to render structured assistant content yourself instead of relying on the built-in `componentLibrary` behavior from the layout components.

{/* add visual: flow-chart showing ChatProvider feeding ThreadSidebar, MessageList, and Composer through useThreadList and useThread */}
<Mermaid initialZoom={2.7} chart={`flowchart TD
subgraph Config["Backend Config"]
PM["processMessage"]
SP["streamProtocol\\nopenAIReadableStreamAdapter()"]
MF["messageFormat\\nopenAIMessageFormat"]
TA["threadApiUrl"]
end

CP["ChatProvider"]

PM --> CP
SP --> CP
MF --> CP
TA --> CP

CP -->|"useThreadList()"| TS["ThreadSidebar\\nthreads · selectedThreadId · isLoadingThreads\\nselectThread() · switchToNewThread()"]
CP -->|"useThread()"| ML["MessageList\\nmessages · isRunning"]
CP -->|"useThread()"| CM["Composer\\nprocessMessage() · cancelMessage() · isRunning"]

TS & ML & CM --> CC["CustomChat"]

`} />

## Related guides

Expand Down
22 changes: 21 additions & 1 deletion docs/content/docs/chat/from-scratch.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,27 @@ This path covers:
- optional thread history
- optional headless customization

{/* add visual: flow-chart showing frontend page -> processMessage -> /api/chat route -> OpenAI -> toReadableStream() -> openAIReadableStreamAdapter() -> rendered UI with componentLibrary */}
```mermaid
flowchart TD
P["Page — FullScreen"]
PM["processMessage()"]
API["/api/chat — route handler"]
OAI["OpenAI — chat.completions.create()"]
RS["response.toReadableStream()"]
AD["openAIReadableStreamAdapter()"]

subgraph UI["Rendered UI"]
TXT["Text responses"]
GEN["Generative UI (componentLibrary)"]
end

P -->|"user message"| PM
PM -->|"POST + OpenAI messages (messageFormat.toApi)"| API
API -->|"stream: true"| OAI
OAI --> RS
RS --> AD
AD -->|"parsed stream"| UI
```

## Prerequisites

Expand Down
Loading
Loading