From f8684863a681ae4ae917b0209f325f208a1a551b Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Sat, 30 Aug 2025 22:08:22 +0530 Subject: [PATCH] feat(cli): add streamdown in ai example of all react web templates (#554) --- apps/cli/src/constants.ts | 1 + apps/cli/src/helpers/addons/examples-setup.ts | 2 +- apps/cli/src/helpers/core/template-manager.ts | 13 +++++++++++ .../base/src/components/response.tsx.hbs | 22 +++++++++++++++++++ .../ai/web/react/next/src/app/ai/page.tsx.hbs | 13 +++++------ .../react/react-router/src/routes/ai.tsx.hbs | 7 ++---- .../tanstack-router/src/routes/ai.tsx.hbs | 7 ++---- .../tanstack-start/src/routes/ai.tsx.hbs | 7 ++---- .../web-base/src/{index.css => index.css.hbs} | 3 +++ 9 files changed, 51 insertions(+), 24 deletions(-) create mode 100644 apps/cli/templates/examples/ai/web/react/base/src/components/response.tsx.hbs rename apps/cli/templates/frontend/react/web-base/src/{index.css => index.css.hbs} (97%) diff --git a/apps/cli/src/constants.ts b/apps/cli/src/constants.ts index 82c1f56..28509fd 100644 --- a/apps/cli/src/constants.ts +++ b/apps/cli/src/constants.ts @@ -108,6 +108,7 @@ export const dependencyVersionMap = { "@ai-sdk/vue": "^2.0.9", "@ai-sdk/svelte": "^3.0.9", "@ai-sdk/react": "^2.0.9", + streamdown: "^1.1.6", "@orpc/server": "^1.8.4", "@orpc/client": "^1.8.4", diff --git a/apps/cli/src/helpers/addons/examples-setup.ts b/apps/cli/src/helpers/addons/examples-setup.ts index 8d1e45a..04b88e9 100644 --- a/apps/cli/src/helpers/addons/examples-setup.ts +++ b/apps/cli/src/helpers/addons/examples-setup.ts @@ -43,7 +43,7 @@ export async function setupExamples(config: ProjectConfig) { } else if (hasSvelte) { dependencies.push("@ai-sdk/svelte"); } else if (hasReactWeb) { - dependencies.push("@ai-sdk/react"); + dependencies.push("@ai-sdk/react", "streamdown"); } await addPackageDependency({ dependencies, diff --git a/apps/cli/src/helpers/core/template-manager.ts b/apps/cli/src/helpers/core/template-manager.ts index f352e44..02c13ce 100644 --- a/apps/cli/src/helpers/core/template-manager.ts +++ b/apps/cli/src/helpers/core/template-manager.ts @@ -740,6 +740,19 @@ export async function setupExamplesTemplate( if (hasReactWeb) { const exampleWebSrc = path.join(exampleBaseDir, "web/react"); if (await fs.pathExists(exampleWebSrc)) { + if (example === "ai") { + const exampleWebBaseSrc = path.join(exampleWebSrc, "base"); + if (await fs.pathExists(exampleWebBaseSrc)) { + await processAndCopyFiles( + "**/*", + exampleWebBaseSrc, + webAppDir, + context, + false, + ); + } + } + const reactFramework = context.frontend.find((f) => [ "next", diff --git a/apps/cli/templates/examples/ai/web/react/base/src/components/response.tsx.hbs b/apps/cli/templates/examples/ai/web/react/base/src/components/response.tsx.hbs new file mode 100644 index 0000000..f583cf2 --- /dev/null +++ b/apps/cli/templates/examples/ai/web/react/base/src/components/response.tsx.hbs @@ -0,0 +1,22 @@ +"use client"; + +import { type ComponentProps, memo } from "react"; +import { Streamdown } from "streamdown"; +import { cn } from "@/lib/utils"; + +type ResponseProps = ComponentProps; + +export const Response = memo( + ({ className, ...props }: ResponseProps) => ( + *:first-child]:mt-0 [&>*:last-child]:mb-0", + className, + )} + {...props} + /> + ), + (prevProps, nextProps) => prevProps.children === nextProps.children, +); + +Response.displayName = "Response"; diff --git a/apps/cli/templates/examples/ai/web/react/next/src/app/ai/page.tsx.hbs b/apps/cli/templates/examples/ai/web/react/next/src/app/ai/page.tsx.hbs index 08593ea..953a6f5 100644 --- a/apps/cli/templates/examples/ai/web/react/next/src/app/ai/page.tsx.hbs +++ b/apps/cli/templates/examples/ai/web/react/next/src/app/ai/page.tsx.hbs @@ -2,10 +2,11 @@ import { useChat } from "@ai-sdk/react"; import { DefaultChatTransport } from "ai"; -import { Input } from "@/components/ui/input"; -import { Button } from "@/components/ui/button"; import { Send } from "lucide-react"; -import { useRef, useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; +import { Response } from "@/components/response"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; export default function AIPage() { const [input, setInput] = useState(""); @@ -51,11 +52,7 @@ export default function AIPage() {

{message.parts?.map((part, index) => { if (part.type === "text") { - return ( -
- {part.text} -
- ); + return {part.text}; } return null; })} diff --git a/apps/cli/templates/examples/ai/web/react/react-router/src/routes/ai.tsx.hbs b/apps/cli/templates/examples/ai/web/react/react-router/src/routes/ai.tsx.hbs index 4dfa8bc..d6aff21 100644 --- a/apps/cli/templates/examples/ai/web/react/react-router/src/routes/ai.tsx.hbs +++ b/apps/cli/templates/examples/ai/web/react/react-router/src/routes/ai.tsx.hbs @@ -4,6 +4,7 @@ import { DefaultChatTransport } from "ai"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Send } from "lucide-react"; +import { Response } from "@/components/response"; const AI: React.FC = () => { const [input, setInput] = useState(""); @@ -49,11 +50,7 @@ const AI: React.FC = () => {

{message.parts?.map((part, index) => { if (part.type === "text") { - return ( -
- {part.text} -
- ); + return {part.text}; } return null; })} diff --git a/apps/cli/templates/examples/ai/web/react/tanstack-router/src/routes/ai.tsx.hbs b/apps/cli/templates/examples/ai/web/react/tanstack-router/src/routes/ai.tsx.hbs index f51f554..81e634e 100644 --- a/apps/cli/templates/examples/ai/web/react/tanstack-router/src/routes/ai.tsx.hbs +++ b/apps/cli/templates/examples/ai/web/react/tanstack-router/src/routes/ai.tsx.hbs @@ -5,6 +5,7 @@ import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Send } from "lucide-react"; import { useRef, useEffect, useState } from "react"; +import { Response } from "@/components/response"; export const Route = createFileRoute("/ai")({ component: RouteComponent, @@ -54,11 +55,7 @@ function RouteComponent() {

{message.parts?.map((part, index) => { if (part.type === "text") { - return ( -
- {part.text} -
- ); + return {part.text}; } return null; })} diff --git a/apps/cli/templates/examples/ai/web/react/tanstack-start/src/routes/ai.tsx.hbs b/apps/cli/templates/examples/ai/web/react/tanstack-start/src/routes/ai.tsx.hbs index 318f32b..e76dfc6 100644 --- a/apps/cli/templates/examples/ai/web/react/tanstack-start/src/routes/ai.tsx.hbs +++ b/apps/cli/templates/examples/ai/web/react/tanstack-start/src/routes/ai.tsx.hbs @@ -5,6 +5,7 @@ import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Send } from "lucide-react"; import { useRef, useEffect, useState } from "react"; +import { Response } from "@/components/response"; export const Route = createFileRoute("/ai")({ component: RouteComponent, @@ -54,11 +55,7 @@ function RouteComponent() {

{message.parts?.map((part, index) => { if (part.type === "text") { - return ( -
- {part.text} -
- ); + return {part.text}; } return null; })} diff --git a/apps/cli/templates/frontend/react/web-base/src/index.css b/apps/cli/templates/frontend/react/web-base/src/index.css.hbs similarity index 97% rename from apps/cli/templates/frontend/react/web-base/src/index.css rename to apps/cli/templates/frontend/react/web-base/src/index.css.hbs index d775cf9..af25d84 100644 --- a/apps/cli/templates/frontend/react/web-base/src/index.css +++ b/apps/cli/templates/frontend/react/web-base/src/index.css.hbs @@ -1,5 +1,8 @@ @import "tailwindcss"; @import "tw-animate-css"; +{{#if (includes examples "ai")}} +@source "../node_modules/streamdown/dist/index.js"; +{{/if}} @custom-variant dark (&:where(.dark, .dark *));