Skip to content
Merged
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
19 changes: 13 additions & 6 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,16 @@ pnpm --filter @openuidev/docs dev
pnpm --filter @openuidev/docs build
```

### `/chat` demo configuration
### Chat and comparison demo configuration

The standalone `/chat` page always starts in **OpenUI OSS** mode. Its selected mode is not
stored across reloads. Live OSS generation uses the existing server-side
`OPENROUTER_API_KEY`.
`/chat` remains the standalone OpenUI OSS and Cloud chat and starts in **OpenUI OSS** mode. Its
selected mode is not stored across reloads.

`/compare` compares two visible response modes at a time and defaults to **Rendered Markdown vs
OpenUI Cloud**. Use its page-level switcher to show **Markdown vs OSS** or **OSS vs Cloud**. The
selected pair is stored in the `pair` query parameter. All three comparison providers remain
mounted and receive each shared prompt; switching the visible pair resets the demo. Markdown and
OSS generation use the existing server-side `OPENROUTER_API_KEY`.

OpenUI Cloud requires the following server-side variables. If either is missing, Cloud requests
show the unavailable state at runtime:
Expand All @@ -32,8 +37,8 @@ THESYS_API_KEY=your-cloud-key
```

Do not expose `THESYS_API_KEY` through a `NEXT_PUBLIC_*` variable. The browser generates an
anonymous user ID, retains it in `sessionStorage`, and sends it with Cloud requests so persisted
conversations remain scoped to that tab session.
anonymous user ID, retains it in `sessionStorage`, and sends it with Cloud requests. Active
comparison threads are not restored after a refresh.

The Cloud feature flag is intentionally fail-closed. Keep it disabled on public deployments until
a shared, cross-instance session-and-IP rate limiter, Cloud organization budgets/token scopes, and
Expand Down Expand Up @@ -71,6 +76,8 @@ docs/
│ │ └── theme-builder/ # Theme creator interface
│ │
│ ├── blog/ # Blog pages
│ ├── chat/ # Standalone OpenUI OSS and Cloud chat
│ ├── compare/ # Pairwise Markdown, OSS, and Cloud comparison
│ ├── demo/ # Demo route
│ ├── playground/ # Interactive playground
│ │
Expand Down
68 changes: 64 additions & 4 deletions docs/app/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,48 @@ import OpenAI from "openai";
import type { ChatCompletionMessageParam } from "openai/resources/chat/completions.mjs";
import { join } from "path";

const systemPrompt = readFileSync(join(process.cwd(), "generated/chat-system-prompt.txt"), "utf-8");
const openUiSystemPrompt = readFileSync(
join(process.cwd(), "generated/chat-system-prompt.txt"),
"utf-8",
);

const markdownSystemPrompt = `You are a helpful assistant. Respond using clear, well-structured GitHub-Flavored Markdown.

Use headings, lists, tables, links, block quotes, and fenced code blocks when they make the response easier to understand. Use the available tools when they are relevant, and incorporate their results into the answer.

Return only Markdown content. Do not emit OpenUI Lang, component syntax, JSON UI descriptions, or instructions for a renderer.`;

type ResponseMode = "markdown" | "openui";

interface ChatRequestBody {
messages: unknown[];
responseMode?: ResponseMode;
}

function invalidRequest(message: string) {
return Response.json({ error: { message } }, { status: 400 });
}

function parseRequestBody(body: unknown): ChatRequestBody | Response {
if (!body || typeof body !== "object" || Array.isArray(body)) {
return invalidRequest("Request body must be a JSON object");
}

const { messages, responseMode } = body as Record<string, unknown>;

if (!Array.isArray(messages)) {
return invalidRequest("messages must be an array");
}

if (responseMode !== undefined && responseMode !== "markdown" && responseMode !== "openui") {
return invalidRequest('responseMode must be either "markdown" or "openui"');
}

return {
messages,
responseMode: responseMode as ResponseMode | undefined,
};
}

// ── Tool implementations ──

Expand Down Expand Up @@ -209,7 +250,19 @@ function sseToolCallArgs(
// ── Route handler ──

export async function POST(req: NextRequest) {
const { messages } = await req.json();
let body: unknown;
try {
body = await req.json();
} catch {
return invalidRequest("Request body must be valid JSON");
}

const parsedBody = parseRequestBody(body);
if (parsedBody instanceof Response) {
return parsedBody;
}

const { messages, responseMode = "openui" } = parsedBody;

const apiKey = process.env.OPENROUTER_API_KEY;
if (!apiKey) {
Expand All @@ -226,7 +279,11 @@ export async function POST(req: NextRequest) {
const MODEL = "openai/gpt-5.4";

const cleanMessages = (messages as any[])
.filter((m) => m.role !== "tool")
.filter(
(m) =>
m.role !== "tool" &&
(responseMode === "openui" || (m.role !== "system" && m.role !== "developer")),
)
.map((m) => {
if (m.role === "assistant" && m.tool_calls?.length) {
const { tool_calls: _tc, ...rest } = m;
Expand All @@ -236,7 +293,10 @@ export async function POST(req: NextRequest) {
});

const chatMessages: ChatCompletionMessageParam[] = [
{ role: "system" as const, content: systemPrompt },
{
role: "system" as const,
content: responseMode === "markdown" ? markdownSystemPrompt : openUiSystemPrompt,
},
...cleanMessages,
];

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"use client";

import { createCloudChatLLM } from "@/lib/openui-cloud/chat-llm";
import { DEFAULT_MODEL } from "@/lib/openui-cloud/models";
import { CLOUD_USER_ID_HEADER } from "@/lib/openui-cloud/user-id";
import { CLOUD_USER_ID_HEADER, getOrCreateCloudUserId } from "@/lib/openui-cloud/user-id";
import { defineArtifactCategories } from "@openuidev/react-headless";
import { AgentInterface } from "@openuidev/react-ui";
import {
Expand All @@ -12,9 +13,7 @@ import {
} from "@openuidev/thesys";
import { useCallback, useEffect, useMemo, useState } from "react";
import styles from "../../chat-page.module.css";
import { createCloudChatLLM } from "./cloud-chat-llm";
import { CloudModelSwitcher } from "./cloud-model-switcher";
import { getOrCreateCloudUserId } from "./cloud-user-id";

const { artifactRenderers, artifactCategories } = defineArtifactCategories([
{ name: "Presentations", renderers: [presentationArtifactRenderer] },
Expand Down
28 changes: 0 additions & 28 deletions docs/app/chat/_components/agent-surfaces/cloud-user-id.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"use client";

import { createCloudChatLLM } from "@/lib/openui-cloud/chat-llm";
import { CLOUD_USER_ID_HEADER, getOrCreateCloudUserId } from "@/lib/openui-cloud/user-id";
import { AgentInterface } from "@openuidev/react-ui";
import { artifactRenderers, chatLibrary, useOpenuiCloudStorage } from "@openuidev/thesys";
import { useMemo, useState } from "react";
import type { ComparisonControllerRegistry } from "../comparison-mode-controller";
import { ComparisonModeControllerBridge } from "../comparison-mode-controller";
import { ComparisonSurfaceWelcome } from "./comparison-surface-welcome";

interface CloudAgentSurfaceProps {
registry: ComparisonControllerRegistry;
}

export function CloudAgentSurface({ registry }: CloudAgentSurfaceProps) {
const [userId] = useState(getOrCreateCloudUserId);
const [llm] = useState(createCloudChatLLM);
const cloudFetch = useMemo<typeof fetch>(() => {
return async (input, init) => {
if (typeof input !== "string" || input !== "/api/openui-cloud/frontend-token") {
return fetch(input, init);
}

const headers = new Headers(init?.headers);
headers.set(CLOUD_USER_ID_HEADER, userId);
return fetch(input, { ...init, headers });
};
}, [userId]);
const cloudStorage = useOpenuiCloudStorage({
token: "/api/openui-cloud/frontend-token",
fetch: cloudFetch,
});

return (
<div className="chat-agent-surface">
<AgentInterface
storage={cloudStorage}
llm={llm}
componentLibrary={chatLibrary}
artifactRenderers={artifactRenderers}
scrollVariant="always"
>
<AgentInterface.Sidebar />
<AgentInterface.Welcome>
<ComparisonSurfaceWelcome mode="cloud" />
</AgentInterface.Welcome>
<AgentInterface.Composer>
<ComparisonModeControllerBridge mode="cloud" registry={registry} />
</AgentInterface.Composer>
</AgentInterface>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"use client";

import { AlignLeft, Blocks, Sparkles } from "lucide-react";
import styles from "../../chat-page.module.css";
import type { ComparisonMode } from "../comparison-mode-controller";

const CONTENT = {
markdown: {
icon: AlignLeft,
description: "AI responses without OpenUI.",
},
oss: {
icon: Blocks,
description: "AI responses with OpenUI OSS.",
},
cloud: {
icon: Sparkles,
description: "AI responses with OpenUI Cloud.",
},
} as const;

export function ComparisonSurfaceWelcome({ mode }: { mode: ComparisonMode }) {
const { icon: Icon, description } = CONTENT[mode];

return (
<div className={styles.surfaceWelcome}>
<span className={styles.surfaceWelcomeIcon} aria-hidden="true">
<Icon size={24} strokeWidth={1.6} />
</span>
<p>{description}</p>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"use client";

import { AgentInterface } from "@openuidev/react-ui";
import type { ComparisonControllerRegistry } from "../comparison-mode-controller";
import { ComparisonModeControllerBridge } from "../comparison-mode-controller";
import { ComparisonSurfaceWelcome } from "./comparison-surface-welcome";
import { useComparisonChatLLM } from "./use-comparison-chat-llm";

interface MarkdownAgentSurfaceProps {
themeMode: "light" | "dark";
onCreditsExhausted: () => void;
registry: ComparisonControllerRegistry;
}

export function MarkdownAgentSurface({
themeMode,
onCreditsExhausted,
registry,
}: MarkdownAgentSurfaceProps) {
const llm = useComparisonChatLLM("markdown", onCreditsExhausted);

return (
<div className="chat-agent-surface" data-chat-mode="markdown">
<AgentInterface
llm={llm}
agentName="Markdown"
scrollVariant="always"
theme={{ mode: themeMode }}
>
<AgentInterface.Sidebar />
<AgentInterface.Welcome>
<ComparisonSurfaceWelcome mode="markdown" />
</AgentInterface.Welcome>
<AgentInterface.Composer>
<ComparisonModeControllerBridge mode="markdown" registry={registry} />
</AgentInterface.Composer>
</AgentInterface>
</div>
);
}
38 changes: 38 additions & 0 deletions docs/app/compare/_components/agent-surfaces/oss-agent-surface.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"use client";

import { AgentInterface } from "@openuidev/react-ui";
import { openuiChatLibrary } from "@openuidev/react-ui/genui-lib";
import type { ComparisonControllerRegistry } from "../comparison-mode-controller";
import { ComparisonModeControllerBridge } from "../comparison-mode-controller";
import { ComparisonSurfaceWelcome } from "./comparison-surface-welcome";
import { useComparisonChatLLM } from "./use-comparison-chat-llm";

interface OssAgentSurfaceProps {
themeMode: "light" | "dark";
onCreditsExhausted: () => void;
registry: ComparisonControllerRegistry;
}

export function OssAgentSurface({ themeMode, onCreditsExhausted, registry }: OssAgentSurfaceProps) {
const llm = useComparisonChatLLM("openui", onCreditsExhausted);

return (
<div className="chat-agent-surface" data-chat-mode="oss">
<AgentInterface
llm={llm}
componentLibrary={openuiChatLibrary}
agentName="OpenUI OSS"
scrollVariant="always"
theme={{ mode: themeMode }}
>
<AgentInterface.Sidebar />
<AgentInterface.Welcome>
<ComparisonSurfaceWelcome mode="oss" />
</AgentInterface.Welcome>
<AgentInterface.Composer>
<ComparisonModeControllerBridge mode="oss" registry={registry} />
</AgentInterface.Composer>
</AgentInterface>
</div>
);
}
Loading
Loading