From a6e3af97a00c0314a48520c9d0a790b6fb4367aa Mon Sep 17 00:00:00 2001 From: Abhin Rustagi Date: Tue, 28 Jul 2026 18:54:36 +0530 Subject: [PATCH 01/28] refactor(openui-cloud): slim template onto shared @openuidev packages - ModelSwitcher: use @openuidev/react-ui (drop local model-switcher/); pass provider logos as {light,dark} - Billing: replace local BillingCreditsDialog with dev-only OpenUICreditsModal from @openuidev/devtools - LLM: swap createCloudChatLLM for fetchLLM + buildBody (429 flows via observability); hold LLM in a ref - Cleanup: remove unused deps, cloud-chat-llm, chat-error, cloud-chat-constants, orphaned CSS/logos Depends on #893 (packages) being merged + published first. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01MpABGCgS6C4HRdQSrwhPMR --- .../src/templates/openui-cloud/package.json | 23 +-- .../openui-cloud/src/app/globals.css | 28 --- .../templates/openui-cloud/src/app/layout.tsx | 3 + .../templates/openui-cloud/src/app/page.tsx | 2 +- .../src/components/billing-credits-dialog.tsx | 37 ---- .../src/components/cloud-chat.tsx | 175 +++++++++++++----- .../components/model-switcher/ModelBadge.tsx | 44 ----- .../components/model-switcher/ModelRow.tsx | 16 -- .../model-switcher/ProviderLogo.tsx | 23 --- .../model-switcher/TriggerContent.tsx | 41 ---- .../components/model-switcher/UsagePolicy.tsx | 46 ----- .../src/components/model-switcher/index.tsx | 90 --------- .../src/components/model-switcher/utils.ts | 58 ------ .../templates/openui-cloud/src/lib/billing.ts | 6 - .../openui-cloud/src/lib/chat-error.ts | 28 --- .../src/lib/cloud-chat-constants.tsx | 70 ------- .../src/lib/cloud-chat-llm.test.ts | 92 --------- .../openui-cloud/src/lib/cloud-chat-llm.tsx | 57 ------ .../src/templates/openui-cloud/src/lib/env.ts | 4 + .../templates/openui-cloud/src/lib/models.ts | 46 ----- .../templates/openui-cloud/src/lib/models.tsx | 50 +++++ 21 files changed, 186 insertions(+), 753 deletions(-) delete mode 100644 packages/openui-cli/src/templates/openui-cloud/src/components/billing-credits-dialog.tsx delete mode 100644 packages/openui-cli/src/templates/openui-cloud/src/components/model-switcher/ModelBadge.tsx delete mode 100644 packages/openui-cli/src/templates/openui-cloud/src/components/model-switcher/ModelRow.tsx delete mode 100644 packages/openui-cli/src/templates/openui-cloud/src/components/model-switcher/ProviderLogo.tsx delete mode 100644 packages/openui-cli/src/templates/openui-cloud/src/components/model-switcher/TriggerContent.tsx delete mode 100644 packages/openui-cli/src/templates/openui-cloud/src/components/model-switcher/UsagePolicy.tsx delete mode 100644 packages/openui-cli/src/templates/openui-cloud/src/components/model-switcher/index.tsx delete mode 100644 packages/openui-cli/src/templates/openui-cloud/src/components/model-switcher/utils.ts delete mode 100644 packages/openui-cli/src/templates/openui-cloud/src/lib/chat-error.ts delete mode 100644 packages/openui-cli/src/templates/openui-cloud/src/lib/cloud-chat-constants.tsx delete mode 100644 packages/openui-cli/src/templates/openui-cloud/src/lib/cloud-chat-llm.test.ts delete mode 100644 packages/openui-cli/src/templates/openui-cloud/src/lib/cloud-chat-llm.tsx delete mode 100644 packages/openui-cli/src/templates/openui-cloud/src/lib/models.ts create mode 100644 packages/openui-cli/src/templates/openui-cloud/src/lib/models.tsx diff --git a/packages/openui-cli/src/templates/openui-cloud/package.json b/packages/openui-cli/src/templates/openui-cloud/package.json index 854f0d4f2..bc6151caa 100644 --- a/packages/openui-cli/src/templates/openui-cloud/package.json +++ b/packages/openui-cli/src/templates/openui-cloud/package.json @@ -11,39 +11,22 @@ "test": "vitest run" }, "dependencies": { - "@floating-ui/react-dom": "2.1.3", "@openuidev/lang-core": "0.2.10", - "@openuidev/react-headless": "^0.9.3", + "@openuidev/react-headless": "^0.9.4", "@openuidev/react-lang": "0.2.9", - "@openuidev/react-ui": "0.13.1", + "@openuidev/react-ui": "^0.13.3", "@openuidev/thesys": "0.3.0", "@openuidev/thesys-server": "0.1.3", - "@radix-ui/react-dialog": "1.1.15", - "@radix-ui/react-tooltip": "^1.2.0", - "@tanstack/react-table": "8.21.3", - "@tiptap/extension-placeholder": "2.27.2", - "@tiptap/react": "2.27.2", - "@tiptap/starter-kit": "2.27.2", - "clsx": "2.1.1", - "katex": "0.16.44", - "lodash": "^4.18.1", "lucide-react": "^0.575.0", - "mermaid": "11.15.0", "next": "^16.2.11", "openai": "^6.22.0", "react": "19.2.3", "react-dom": "19.2.3", - "recharts": "2.15.4", - "rehype-katex": "7.0.1", - "remark-breaks": "4.0.0", - "remark-gfm": "4.0.1", - "remark-math": "6.0.0", - "tiny-invariant": "1.3.3", - "unist-util-visit": "5.1.0", "zod": "^4.0.0", "zustand": "^4.5.5" }, "devDependencies": { + "@openuidev/devtools": "^0.0.2", "@tailwindcss/postcss": "^4", "@types/node": "^20", "@types/react": "^19", diff --git a/packages/openui-cli/src/templates/openui-cloud/src/app/globals.css b/packages/openui-cli/src/templates/openui-cloud/src/app/globals.css index 2bd2bad65..50633c5ad 100644 --- a/packages/openui-cli/src/templates/openui-cloud/src/app/globals.css +++ b/packages/openui-cli/src/templates/openui-cloud/src/app/globals.css @@ -48,24 +48,6 @@ letter-spacing: var(--openui-text-body-sm-letter-spacing) !important; } -.openui-cloud-billing-dialog__description { - margin: 0; - color: var(--openui-text-neutral-secondary); - font: var(--openui-text-body-default); - letter-spacing: var(--openui-text-body-default-letter-spacing); -} - -.openui-cloud-billing-dialog__actions { - display: flex; - align-items: center; - gap: var(--openui-space-s); - margin-top: var(--openui-space-xs); -} - -.openui-cloud-root--billing-credits-required .openui-agent-thread-error { - display: none; -} - .openui-cloud-mobile-header .openui-agent-mobile-header-logo-container { display: none; } @@ -84,14 +66,4 @@ .openui-agent-thread-error .openui-callout { align-items: flex-start; } - - .openui-cloud-billing-dialog__actions { - flex-direction: column; - align-items: stretch; - } - - .openui-cloud-billing-dialog__actions .openui-button-base { - width: 100%; - justify-content: center; - } } diff --git a/packages/openui-cli/src/templates/openui-cloud/src/app/layout.tsx b/packages/openui-cli/src/templates/openui-cloud/src/app/layout.tsx index a4d2026fe..2a81c2422 100644 --- a/packages/openui-cli/src/templates/openui-cloud/src/app/layout.tsx +++ b/packages/openui-cli/src/templates/openui-cloud/src/app/layout.tsx @@ -1,4 +1,6 @@ import { ThemeProvider } from "@/hooks/use-system-theme"; +import { isDevelopment } from "@/lib/env"; +import { OpenUIDevtools } from "@openuidev/devtools"; import type { Metadata } from "next"; import { Inter } from "next/font/google"; import "./globals.css"; @@ -21,6 +23,7 @@ export default function RootLayout({ {children} + {isDevelopment() && } ); diff --git a/packages/openui-cli/src/templates/openui-cloud/src/app/page.tsx b/packages/openui-cli/src/templates/openui-cloud/src/app/page.tsx index d75fb021e..ff9182b6c 100644 --- a/packages/openui-cli/src/templates/openui-cloud/src/app/page.tsx +++ b/packages/openui-cli/src/templates/openui-cloud/src/app/page.tsx @@ -1,7 +1,7 @@ import "@openuidev/react-ui/components.css"; import "@openuidev/thesys/styles.css"; -import { CloudChat } from "@/components/cloud-chat"; +import CloudChat from "@/components/cloud-chat"; export const dynamic = "force-dynamic"; diff --git a/packages/openui-cli/src/templates/openui-cloud/src/components/billing-credits-dialog.tsx b/packages/openui-cli/src/templates/openui-cloud/src/components/billing-credits-dialog.tsx deleted file mode 100644 index 89fb1c7a3..000000000 --- a/packages/openui-cli/src/templates/openui-cloud/src/components/billing-credits-dialog.tsx +++ /dev/null @@ -1,37 +0,0 @@ -"use client"; - -import { - BILLING_CREDITS_ACTION_LABEL, - BILLING_CREDITS_ERROR_MESSAGE, - BILLING_CREDITS_ERROR_TITLE, - BILLING_URL, -} from "@/lib/billing"; -import { Button } from "@openuidev/react-ui"; -import { Modal } from "@openuidev/react-ui/Modal"; -import { CreditCard } from "lucide-react"; - -interface BillingCreditsDialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; -} - -export function BillingCreditsDialog({ open, onOpenChange }: BillingCreditsDialogProps) { - return ( - -

- {BILLING_CREDITS_ERROR_MESSAGE} -

-
- -
-
- ); -} diff --git a/packages/openui-cli/src/templates/openui-cloud/src/components/cloud-chat.tsx b/packages/openui-cli/src/templates/openui-cloud/src/components/cloud-chat.tsx index fe430f49f..e777042cb 100644 --- a/packages/openui-cli/src/templates/openui-cloud/src/components/cloud-chat.tsx +++ b/packages/openui-cli/src/templates/openui-cloud/src/components/cloud-chat.tsx @@ -2,16 +2,20 @@ import { getPersistedModel, usePersistedModel } from "@/hooks/use-persisted-model"; import { useTheme } from "@/hooks/use-system-theme"; -import { shouldShowBillingCreditsNotice } from "@/lib/billing"; +import { isDevelopment } from "@/lib/env"; +import { MODEL_OPTIONS } from "@/lib/models"; +import { OpenUICreditsModal } from "@openuidev/devtools"; import { - DARK_LOGO_URL, - LIGHT_LOGO_URL, - PROMPT_TEMPLATES, - starters, -} from "@/lib/cloud-chat-constants"; -import { createCloudChatLLM } from "@/lib/cloud-chat-llm"; -import { defineArtifactCategories } from "@openuidev/react-headless"; -import { AgentInterface } from "@openuidev/react-ui"; + fetchLLM, + openAIConversationMessageFormat, + openAIResponsesAdapter, +} from "@openuidev/react-headless"; +import { + AgentInterface, + ModelSwitcher, + defineArtifactCategories, + type PromptTemplate, +} from "@openuidev/react-ui"; import { chatLibrary, presentationArtifactRenderer, @@ -19,9 +23,7 @@ import { useOpenuiCloudStorage, } from "@openuidev/thesys"; import { FileText, Presentation } from "lucide-react"; -import { useCallback, useEffect, useState } from "react"; -import { BillingCreditsDialog } from "./billing-credits-dialog"; -import { ModelSwitcher } from "./model-switcher"; +import { useCallback, useEffect, useRef } from "react"; const { artifactRenderers, artifactCategories } = defineArtifactCategories([ { @@ -36,56 +38,58 @@ const { artifactRenderers, artifactCategories } = defineArtifactCategories([ }, ]); -const showBillingCreditsNotice = shouldShowBillingCreditsNotice(); +const LIGHT_LOGO_URL = "/openui-cloud-logo-light.svg"; +const DARK_LOGO_URL = "/openui-cloud-logo-dark.svg"; -export function CloudChat() { +export default function CloudChat() { const mode = useTheme(); const [selectedModel, setSelectedModel] = usePersistedModel(); - const [billingDialogOpen, setBillingDialogOpen] = useState(false); - const [billingCreditsRequired, setBillingCreditsRequired] = useState(false); - const [llm] = useState(() => - createCloudChatLLM({ - // Read the persisted model directly so the LLM starts on the saved - // selection at construction. selectedModel is still the server snapshot - // (DEFAULT_MODEL) during the first client render; the effect below also - // keeps it in sync afterwards. - initialModel: getPersistedModel(), - showBillingCreditsNotice, - onRequestStart: () => { - if (showBillingCreditsNotice) setBillingCreditsRequired(false); - }, - onBillingCreditsRequired: () => { - setBillingCreditsRequired(true); - setBillingDialogOpen(true); - }, + + // buildBody (below) reads the current model at request time via this ref, so + // switching models takes effect without recreating the LLM. Seed it with the + // persisted value so the first request uses the saved selection (selectedModel + // is still the server snapshot during the first client render). + const selectedModelRef = useRef(getPersistedModel()); + useEffect(() => { + selectedModelRef.current = selectedModel; + }, [selectedModel]); + + // Create the LLM once and keep it across renders via a ref (not state — it + // never changes and never needs to trigger a re-render). The `??=` lazy-init + // runs fetchLLM exactly once and is tolerant of re-renders / strict-mode + // double-invocation. buildBody reads the live model from selectedModelRef, so + // switching models reuses this instance. + const llmRef = useRef | null>(null); + const llm = (llmRef.current ??= fetchLLM({ + url: "/api/chat", + streamAdapter: openAIResponsesAdapter(), + // The cloud route persists history (store:true + conversation), so send + // only the latest message — plus the currently selected model. + buildBody: ({ threadId, messages }) => ({ + threadId, + input: openAIConversationMessageFormat.toApi(messages.slice(-1)), + model: selectedModelRef.current, }), - ); + })); const storage = useOpenuiCloudStorage({ token: "/api/frontend-token", apiBaseUrl: "https://api.thesys.dev", features: { artifact: true }, }); - // Keep the LLM in sync with the persisted selection (initial restore + changes). - useEffect(() => { - llm.setSelectedModel(selectedModel); - }, [llm, selectedModel]); - const handleModelChange = useCallback( (model: string) => { - llm.setSelectedModel(model); - // Persist + notify; useSyncExternalStore re-reads and re-renders. + // Persist + notify; useSyncExternalStore re-reads and re-renders, and the + // ref effect above propagates the new model to buildBody. setSelectedModel(model); }, - [llm, setSelectedModel], + [setSelectedModel], ); + const logoPath = mode === "dark" ? DARK_LOGO_URL : LIGHT_LOGO_URL; + return ( -
+
+ } /> - + - {showBillingCreditsNotice ? ( - - ) : null} + {isDevelopment() && }
); } + +const PROMPT_TEMPLATES: PromptTemplate[] = [ + { + displayText: "Create a presentation", + prompt: "Create a presentation about ", + icon: , + completions: [ + { + displayText: "The rise of reusable rockets and commercial spaceflight", + prompt: "the rise of reusable rockets and commercial spaceflight", + icon: <>, + }, + { + displayText: "How Formula 1 became a global business", + prompt: "how Formula 1 became a global business", + icon: <>, + }, + { + displayText: "Why electric vehicles are changing transportation", + prompt: "why electric vehicles are changing transportation", + icon: <>, + }, + ], + }, + { + displayText: "Write a report", + prompt: "Write a report on ", + icon: , + completions: [ + { + displayText: "Global coffee market trends and consumer preferences", + prompt: "global coffee market trends and consumer preferences", + icon: <>, + }, + { + displayText: "The state of the electric vehicle market in 2026", + prompt: "the state of the electric vehicle market in 2026", + icon: <>, + }, + { + displayText: "Global travel trends and emerging destinations", + prompt: "global travel trends and emerging destinations", + icon: <>, + }, + ], + }, +]; + +const starters = [ + { + displayText: "Relive the FIFA World Cup 2026", + prompt: "Relive the FIFA World Cup 2026.", + icon: <>, + }, + { + displayText: "Create a report on global coffee trends", + prompt: "Create a report on global coffee trends.", + icon: <>, + }, + { + displayText: "Help me plan my next vacation", + prompt: "Help me plan my next vacation.", + icon: <>, + }, +]; diff --git a/packages/openui-cli/src/templates/openui-cloud/src/components/model-switcher/ModelBadge.tsx b/packages/openui-cli/src/templates/openui-cloud/src/components/model-switcher/ModelBadge.tsx deleted file mode 100644 index cc67f17c9..000000000 --- a/packages/openui-cli/src/templates/openui-cloud/src/components/model-switcher/ModelBadge.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { type ModelOption } from "@/lib/models"; - -import { getModelBadge } from "./utils"; - -// Recommended = info (blue), Free = success (green), both from the @openuidev -// design tokens. Labels use neutral primary text while the leading dot carries -// the semantic color. Trigger and row differ only in shape/size. -const BADGE_DOT_COLORS = { - recommended: "bg-[var(--openui-border-info-emphasis)]", - free: "bg-[var(--openui-border-success-emphasis)]", -} as const; - -export function ModelBadge({ - model, - variant, -}: { - model: ModelOption; - variant: "trigger" | "row"; -}) { - const badge = getModelBadge(model); - if (!badge) return null; - - if (variant === "trigger") { - return ( - - - ); - } - - return ( - - - ); -} diff --git a/packages/openui-cli/src/templates/openui-cloud/src/components/model-switcher/ModelRow.tsx b/packages/openui-cli/src/templates/openui-cloud/src/components/model-switcher/ModelRow.tsx deleted file mode 100644 index ea79046a3..000000000 --- a/packages/openui-cli/src/templates/openui-cloud/src/components/model-switcher/ModelRow.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { type ModelOption } from "@/lib/models"; - -import { ModelBadge } from "./ModelBadge"; -import { ProviderLogo } from "./ProviderLogo"; - -// A single row inside the dropdown: provider logo, name, and a Recommended/Free -// chip. -export function ModelRow({ model }: { model: ModelOption }) { - return ( - - - {model.name} - - - ); -} diff --git a/packages/openui-cli/src/templates/openui-cloud/src/components/model-switcher/ProviderLogo.tsx b/packages/openui-cli/src/templates/openui-cloud/src/components/model-switcher/ProviderLogo.tsx deleted file mode 100644 index d9d65c6d6..000000000 --- a/packages/openui-cli/src/templates/openui-cloud/src/components/model-switcher/ProviderLogo.tsx +++ /dev/null @@ -1,23 +0,0 @@ -"use client"; - -import { useTheme } from "@/hooks/use-system-theme"; - -export function ProviderLogo({ - provider, - size = "md", -}: { - provider: string; - size?: "sm" | "md"; -}) { - const mode = useTheme(); - const box = size === "sm" ? "h-5 w-5 rounded-md" : "h-7 w-7 rounded-lg"; - const img = size === "sm" ? "h-3 w-3" : "h-4 w-4"; - return ( - - ); -} diff --git a/packages/openui-cli/src/templates/openui-cloud/src/components/model-switcher/TriggerContent.tsx b/packages/openui-cli/src/templates/openui-cloud/src/components/model-switcher/TriggerContent.tsx deleted file mode 100644 index d952c600e..000000000 --- a/packages/openui-cli/src/templates/openui-cloud/src/components/model-switcher/TriggerContent.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { type ModelOption } from "@/lib/models"; - -import { ModelBadge } from "./ModelBadge"; -import { ProviderLogo } from "./ProviderLogo"; - -// Neutral skeleton shown until the client reads the persisted model from -// localStorage, so a refresh doesn't flash the default model. -function TriggerSkeleton() { - return ( - <> -