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 *));