Skip to content
Draft
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
29 changes: 29 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,35 @@ a shared, cross-instance session-and-IP rate limiter, Cloud organization budgets
an approved conversation retention/deletion process are in place. Same-origin validation is an
additional browser safeguard, not a substitute for those cost controls.

### Demo interaction analytics

The four hosted demos (`/compare`, `/demo/github`, `/chat`, and `/demos`) emit
`demo_agent_interaction` PostHog events for accepted user interactions. Most turns emit one event;
`/compare` emits one per visible panel. The instrumentation supplies only these custom dimensions
(PostHog may also attach its standard page context):

- `demo`: the stable demo identifier
- `variant`: the visible comparison mode or chat mode when applicable
- `model`: the canonical provider/model ID requested for the interaction
- `interaction_source`: `composer`, `starter`, or `rendered_action`

`/compare` emits two events for a shared prompt, one per available mode in the selected visible
pair. Its hidden third controller still receives the prompt in the background, but that work is
not counted as a visible user interaction. A generated `continue_conversation` action emits one
event for the panel it targets. Prefill-only chips, blocked submissions, restored messages, and
local generated-UI state changes do not emit events.

Demo analytics never add prompt or message content, generated output, action payloads, form state,
GitHub usernames, action or tool URLs, thread or Cloud user IDs, tool arguments, or raw errors. The
docs site uses the shared, fail-open PostHog client in `lib/analytics.ts`; published OpenUI packages
do not initialize or send analytics.

OpenUI visitors remain anonymous. Outbound `thesys.dev` links carry PostHog's current anonymous
distinct ID and session ID as handoff query parameters, not event properties. The receiving console
validates and removes those parameters before analytics initialization, bootstraps the distinct ID
as anonymous, and later merges the demo history through its canonical `posthog.identify(user.id)`
call. Both applications must use the same PostHog project, and the console resets PostHog on logout.

## Project structure

```
Expand Down
3 changes: 2 additions & 1 deletion docs/app/api/chat/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { DEFAULT_MODEL } from "@/lib/openui-cloud/models";
import { readFileSync } from "fs";
import { NextRequest } from "next/server";
import OpenAI from "openai";
Expand Down Expand Up @@ -293,7 +294,7 @@ export async function POST(req: NextRequest) {
apiKey,
baseURL: "https://openrouter.ai/api/v1",
});
const MODEL = "openai/gpt-5.4";
const MODEL = DEFAULT_MODEL;

const cleanMessages = (messages as any[])
.filter(
Expand Down
22 changes: 21 additions & 1 deletion docs/app/chat/_components/agent-surfaces/cloud-agent-surface.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
"use client";

import {
captureDemoAgentInteraction,
getDemoInteractionSourceFromMessages,
} from "@/lib/demo-analytics";
import { createCloudChatLLM } from "@/lib/openui-cloud/chat-llm";
import { DEFAULT_MODEL } from "@/lib/openui-cloud/models";
import { CLOUD_USER_ID_HEADER, getOrCreateCloudUserId } from "@/lib/openui-cloud/user-id";
Expand Down Expand Up @@ -34,11 +38,27 @@ const CLOUD_STARTERS = [
prompt: "Write a brief market-analysis report on the EV sector.",
},
];
const CLOUD_STARTER_PROMPTS = CLOUD_STARTERS.map((starter) => starter.prompt);

export function CloudAgentSurface() {
const [selectedModel, setSelectedModel] = useState(DEFAULT_MODEL);
const [userId] = useState(getOrCreateCloudUserId);
const [llm] = useState(createCloudChatLLM);
const [llm] = useState(() =>
createCloudChatLLM(({ model, messages }) => {
const interactionSource = getDemoInteractionSourceFromMessages(
messages,
CLOUD_STARTER_PROMPTS,
);
if (!interactionSource) return;

captureDemoAgentInteraction({
demo: "openui_chat",
variant: "cloud",
model,
interaction_source: interactionSource,
});
}),
);
const cloudFetch = useMemo<typeof fetch>(() => {
return async (input, init) => {
if (typeof input !== "string" || input !== "/api/openui-cloud/frontend-token") {
Expand Down
19 changes: 19 additions & 0 deletions docs/app/chat/_components/agent-surfaces/oss-agent-surface.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
"use client";

import {
captureDemoAgentInteraction,
getDemoInteractionSourceFromMessages,
} from "@/lib/demo-analytics";
import { isDemoCreditsErrorPayload } from "@/lib/demo-credits";
import { DEFAULT_MODEL } from "@/lib/openui-cloud/models";
import {
AgentInterface,
openAIAdapter,
Expand Down Expand Up @@ -36,11 +41,25 @@ const OSS_STARTERS = [
"Show me a carousel of 3 popular travel destinations with images, descriptions, and best time to visit.",
},
];
const OSS_STARTER_PROMPTS = OSS_STARTERS.map((starter) => starter.prompt);

export function OssAgentSurface({ onCreditsExhausted }: OssAgentSurfaceProps) {
const llm = useMemo<ChatLLM>(
() => ({
send: async ({ messages, signal }) => {
const interactionSource = getDemoInteractionSourceFromMessages(
messages,
OSS_STARTER_PROMPTS,
);
if (interactionSource) {
captureDemoAgentInteraction({
demo: "openui_chat",
variant: "oss",
model: DEFAULT_MODEL,
interaction_source: interactionSource,
});
}

const response = await fetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
"use client";

import {
captureDemoAgentInteraction,
getDemoInteractionSourceFromMessages,
} from "@/lib/demo-analytics";
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";
Expand All @@ -15,7 +19,18 @@ interface CloudAgentSurfaceProps {

export function CloudAgentSurface({ registry }: CloudAgentSurfaceProps) {
const [userId] = useState(getOrCreateCloudUserId);
const [llm] = useState(createCloudChatLLM);
const [llm] = useState(() =>
createCloudChatLLM(({ model, messages }) => {
if (getDemoInteractionSourceFromMessages(messages) !== "rendered_action") return;

captureDemoAgentInteraction({
demo: "compare",
variant: "cloud",
model,
interaction_source: "rendered_action",
});
}),
);
const cloudFetch = useMemo<typeof fetch>(() => {
return async (input, init) => {
if (typeof input !== "string" || input !== "/api/openui-cloud/frontend-token") {
Expand Down
20 changes: 18 additions & 2 deletions docs/app/compare/_components/agent-surfaces/oss-agent-surface.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
"use client";

import { AgentInterface } from "@openuidev/react-ui";
import {
captureDemoAgentInteraction,
getDemoInteractionSourceFromMessages,
} from "@/lib/demo-analytics";
import { DEFAULT_MODEL } from "@/lib/openui-cloud/models";
import { AgentInterface, type Message } from "@openuidev/react-ui";
import { openuiChatLibrary } from "@openuidev/react-ui/genui-lib";
import { useCallback } from "react";
import type { ComparisonControllerRegistry } from "../comparison-mode-controller";
import { ComparisonModeControllerBridge } from "../comparison-mode-controller";
import { ComparisonSurfaceWelcome } from "./comparison-surface-welcome";
Expand All @@ -13,7 +19,17 @@ interface OssAgentSurfaceProps {
}

export function OssAgentSurface({ onCreditsExhausted, registry }: OssAgentSurfaceProps) {
const llm = useComparisonChatLLM("openui", onCreditsExhausted);
const observeSend = useCallback((messages: readonly Message[]) => {
if (getDemoInteractionSourceFromMessages(messages) !== "rendered_action") return;

captureDemoAgentInteraction({
demo: "compare",
variant: "oss",
model: DEFAULT_MODEL,
interaction_source: "rendered_action",
});
}, []);
const llm = useComparisonChatLLM("openui", onCreditsExhausted, observeSend);

return (
<div className="chat-agent-surface" data-chat-mode="oss">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
"use client";

import { isDemoCreditsErrorPayload } from "@/lib/demo-credits";
import { openAIAdapter, openAIMessageFormat, type ChatLLM } from "@openuidev/react-ui";
import {
openAIAdapter,
openAIMessageFormat,
type ChatLLM,
type Message,
} from "@openuidev/react-ui";
import { useMemo } from "react";

type ComparisonResponseMode = "markdown" | "openui";
Expand All @@ -13,10 +18,17 @@ type ComparisonResponseMode = "markdown" | "openui";
export function useComparisonChatLLM(
responseMode: ComparisonResponseMode,
onCreditsExhausted: () => void,
onSend?: (messages: readonly Message[]) => void,
): ChatLLM {
return useMemo<ChatLLM>(
() => ({
send: async ({ messages, signal }) => {
try {
onSend?.(messages);
} catch {
// Analytics observers must never interfere with the chat request.
}

const response = await fetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
Expand All @@ -36,7 +48,7 @@ export function useComparisonChatLLM(
},
streamProtocol: openAIAdapter(),
}),
[onCreditsExhausted, responseMode],
[onCreditsExhausted, onSend, responseMode],
);
}

Expand Down
10 changes: 7 additions & 3 deletions docs/app/compare/_components/compare-page-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ import {
getComparisonPair,
type ComparisonPair,
} from "./chat-types";
import { ComparisonControls } from "./comparison-controls";
import { captureComparisonPromptInteractions } from "./comparison-analytics";
import { ComparisonControls, type ComparisonSubmissionSource } from "./comparison-controls";
import type {
ComparisonMode,
ComparisonModeController,
Expand Down Expand Up @@ -148,15 +149,18 @@ export function ComparePageClient({
}, [registry, unavailableModes]);

const submitToAll = useCallback(
(content: string) => {
(content: string, source: ComparisonSubmissionSource) => {
const controllers = getReadyControllers();
if (!controllers) return;
const states = controllers.map((controller) => controller.getSnapshot());
if (states.some((state) => !state.isReady || state.isRunning)) return;

const availableModes = new Set(controllers.map(({ mode }) => mode));
const visibleModes = getComparisonPair(pair).modes.filter((mode) => availableModes.has(mode));
captureComparisonPromptInteractions(visibleModes, source);
void Promise.allSettled(controllers.map((controller) => controller.send(content)));
},
[getReadyControllers],
[getReadyControllers, pair],
);

const stopAll = useCallback(() => {
Expand Down
22 changes: 22 additions & 0 deletions docs/app/compare/_components/comparison-analytics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {
captureDemoAgentInteraction,
type DemoInteractionSource,
} from "../../../lib/demo-analytics";
import { DEFAULT_MODEL } from "../../../lib/openui-cloud/models";
import type { ComparisonMode } from "./comparison-mode-controller";

export type ComparisonPromptSource = Extract<DemoInteractionSource, "composer" | "starter">;

export function captureComparisonPromptInteractions(
modes: readonly ComparisonMode[],
interactionSource: ComparisonPromptSource,
): void {
modes.forEach((mode) => {
captureDemoAgentInteraction({
demo: "compare",
variant: mode,
model: DEFAULT_MODEL,
interaction_source: interactionSource,
});
});
}
14 changes: 8 additions & 6 deletions docs/app/compare/_components/comparison-controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,14 @@ const COMPARISON_SUGGESTIONS = [
},
] as const;

export type ComparisonSubmissionSource = "composer" | "starter";

interface ComparisonControlsProps {
comparisonPair: ComparisonPair;
isReady: boolean;
isRunning: boolean;
hasStarted: boolean;
onSubmit: (content: string) => void;
onSubmit: (content: string, source: ComparisonSubmissionSource) => void;
onStop: () => void;
onReset: () => void;
isDegraded: boolean;
Expand Down Expand Up @@ -121,11 +123,11 @@ export function ComparisonControls({
return () => observer.disconnect();
}, [text]);

const submit = (content: string) => {
const submit = (content: string, source: ComparisonSubmissionSource) => {
const next = content.trim();
if (!next || !isReady || isRunning) return;

onSubmit(next);
onSubmit(next, source);
setText("");
};

Expand All @@ -152,7 +154,7 @@ export function ComparisonControls({
key={suggestion.label}
type="button"
className={styles.suggestionButton}
onClick={() => submit(suggestion.prompt)}
onClick={() => submit(suggestion.prompt, "starter")}
disabled={!isReady || isRunning || (needsCloud && !cloudEnabled)}
title={
needsCloud && !cloudEnabled
Expand Down Expand Up @@ -217,14 +219,14 @@ export function ComparisonControls({
onKeyDown={(event) => {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
submit(text);
submit(text, "composer");
}
}}
/>
<button
type="button"
className={styles.composerSubmit}
onClick={isRunning ? onStop : () => submit(text)}
onClick={isRunning ? onStop : () => submit(text, "composer")}
disabled={!isRunning && (!isReady || text.trim().length === 0)}
aria-label={
isRunning
Expand Down
2 changes: 1 addition & 1 deletion docs/app/demo/github/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const STREAM_RESULT = {
} as const;
export type StreamResult = (typeof STREAM_RESULT)[keyof typeof STREAM_RESULT];

export const GITHUB_DEMO_MODEL = "anthropic/claude-sonnet-4-6";
export { GITHUB_DEMO_MODEL } from "../../../lib/demo-models";
export const GITHUB_DEMO_MODEL_LABEL = "Claude Sonnet 4.6";

export type GitHubStarterIconKey =
Expand Down
Loading
Loading