From c0ea40a4bc9ca68a97890891dfe939efb2eb8d16 Mon Sep 17 00:00:00 2001 From: Anton Arnautov Date: Tue, 4 Nov 2025 14:56:57 +0100 Subject: [PATCH 1/4] Initial setup --- packages/react-sdk/package.json | 127 +++++++------ .../react-sdk/src/components/ai-markdown.tsx | 176 ++++++++++++++++++ .../react-sdk/src/components/tools/index.ts | 0 packages/react-sdk/src/index.ts | 1 + packages/react-sdk/tsconfig.lib.json | 21 ++- pnpm-lock.yaml | 74 +++++--- 6 files changed, 302 insertions(+), 97 deletions(-) create mode 100644 packages/react-sdk/src/components/ai-markdown.tsx create mode 100644 packages/react-sdk/src/components/tools/index.ts diff --git a/packages/react-sdk/package.json b/packages/react-sdk/package.json index 20901a4..6ff0df8 100644 --- a/packages/react-sdk/package.json +++ b/packages/react-sdk/package.json @@ -1,62 +1,69 @@ { - "name": "@stream-io/ai-components-react", - "version": "1.0.0", - "private": true, - "description": "React SDK for AI Components", - "main": "./dist/cjs/index.js", - "types": "./dist/types/index.d.ts", - "exports": { - ".": { - "types": "./dist/types/index.d.ts", - "browser": { - "import": "./dist/esm/index.mjs", - "require": "./dist/cjs/index.js" - }, - "default": "./dist/cjs/index.js" - }, - "./stream": { - "types": "./dist/types/stream/index.d.ts", - "browser": { - "import": "./dist/esm/stream/index.mjs", - "require": "./dist/cjs/stream/index.js" - }, - "default": "./dist/cjs/stream/index.js" - } - }, - "files": [ - "dist", - "src", - "!src/*.test.ts" - ], - "scripts": { - "build": "rimraf ./dist && concurrently 'vite build' 'tsc -p ./tsconfig.lib.json'", - "prepare": "pnpm run build", - "test": "vitest" - }, - "keywords": [ - "ai", - "react", - "components", - "sdk" - ], - "author": { - "name": "GetStream.io, Inc.", - "url": "https://getstream.io/team/" - }, - "license": "SEE LICENSE IN LICENSE", - "packageManager": "pnpm@10.13.1", - "peerDependencies": { - "react": "^17 || ^18 || ^19", - "react-dom": "^17 || ^18 || ^19" - }, - "devDependencies": { - "@types/react": "^17", - "@types/react-dom": "^17", - "@types/node": "^24", - "concurrently": "catalog:", - "vite": "catalog:", - "rimraf": "^6.0.1", - "typescript": "catalog:", - "vitest": "catalog:" - } + "name": "@stream-io/ai-components-react", + "version": "1.0.0", + "private": true, + "description": "React SDK for AI Components", + "main": "./dist/cjs/index.js", + "types": "./dist/types/index.d.ts", + "exports": { + ".": { + "types": "./dist/types/index.d.ts", + "browser": { + "import": "./dist/es/index.mjs", + "require": "./dist/cjs/index.js" + }, + "default": "./dist/cjs/index.js" + }, + "./stream": { + "types": "./dist/types/stream/index.d.ts", + "browser": { + "import": "./dist/es/stream/index.mjs", + "require": "./dist/cjs/stream/index.js" + }, + "default": "./dist/cjs/stream/index.js" + } + }, + "files": [ + "dist", + "src", + "!src/*.test.ts" + ], + "scripts": { + "build": "rimraf ./dist && concurrently 'vite build' 'tsc -p ./tsconfig.lib.json'", + "dev": "concurrently 'vite build --watch' 'tsc -p ./tsconfig.lib.json' --watch", + "prepare": "pnpm run build", + "test": "" + }, + "keywords": [ + "ai", + "react", + "components", + "sdk" + ], + "author": { + "name": "GetStream.io, Inc.", + "url": "https://getstream.io/team/" + }, + "license": "SEE LICENSE IN LICENSE", + "packageManager": "pnpm@10.13.1", + "peerDependencies": { + "react": "^17 || ^18 || ^19", + "react-dom": "^17 || ^18 || ^19" + }, + "devDependencies": { + "@types/node": "^24", + "@types/react": "^18.3.26", + "@types/react-dom": "^18.3.7", + "@types/react-syntax-highlighter": "^15.5.13", + "concurrently": "catalog:", + "rimraf": "^6.0.1", + "typescript": "catalog:", + "vite": "catalog:" + }, + "dependencies": { + "clsx": "^2.1.1", + "react-markdown": "^10.1.0", + "react-syntax-highlighter": "15.5.0", + "remark-gfm": "^4.0.1" + } } diff --git a/packages/react-sdk/src/components/ai-markdown.tsx b/packages/react-sdk/src/components/ai-markdown.tsx new file mode 100644 index 0000000..b6f21b3 --- /dev/null +++ b/packages/react-sdk/src/components/ai-markdown.tsx @@ -0,0 +1,176 @@ +import React, { + Children, + type ComponentProps, + type ComponentType, + type ElementType, + isValidElement, + useContext, + useMemo, +} from "react"; +import ReactMarkdown, { + type Components, + type ExtraProps, +} from "react-markdown"; +import remarkGfm from "remark-gfm"; +import { Prism, type SyntaxHighlighterProps } from "react-syntax-highlighter"; +import clsx from "clsx"; + +const SyntaxHighlighter = + Prism as unknown as ComponentType; + +const getToolOrLanguage = (className: string = "") => { + return className.match(/language-(?[\w-]+)/)?.groups?.["tool"]; +}; + +type ToolComponents = Record>; +type MarkdownComponents = Components; + +const AIMarkdownContext = React.createContext<{ + toolComponents: ToolComponents; +}>({ toolComponents: {} }); + +type BaseDefaultPreProps = ComponentProps<"pre"> & ExtraProps; + +type DefaultPreProps = BaseDefaultPreProps & { + Pre?: ComponentType | ElementType; +}; + +const DefaultPre = (props: DefaultPreProps) => { + const { children, Pre = "pre", ...restProps } = props; + + const { toolComponents } = useContext(AIMarkdownContext); + + const [codeElement] = Children.toArray(children); + + if ( + !isValidElement(codeElement) || + codeElement.props.node.tagName !== "code" + ) { + return
{children}
; + } + + const tool = getToolOrLanguage(codeElement.props.className); + + // grab from pre-registered component set and render + const Component = typeof tool === "string" ? toolComponents[tool] : null; + if (Component) { + return ; + } + + // render just a fragment with the code content + // which gets replaced by SyntaxHighlighter (it itself renders pre too) + if (tool) { + return <>{children}; + } + + // treat as regular pre/code block if there's no tool/language + return
{children}
; +}; + +const DefaultSyntaxHighlighter = ({ + children, + language, +}: BaseDefaultCodeProps) => { + return ( + + {children as string} + + ); +}; + +type BaseDefaultCodeProps = ComponentProps<"code"> & + ExtraProps & { language?: string; inline?: boolean }; + +type DefaultCodeProps = BaseDefaultCodeProps & { + Code?: ComponentType | ElementType; + SyntaxHighlighter?: ComponentType; +}; + +const DefaultCode = (props: DefaultCodeProps) => { + const { + node, + className, + children, + SyntaxHighlighter = DefaultSyntaxHighlighter, + Code = "code", + ...restProps + } = props; + + const language = getToolOrLanguage(className); + const inline = !language; + + const Component = inline ? Code : SyntaxHighlighter; + + return ( + + {children} + + ); +}; + +const DefaultComponents = { + pre: DefaultPre, + code: DefaultCode, +} as const; + +interface AIMarkdown { + (props: { + children: string; + toolComponents?: ToolComponents; + markdownComponents?: MarkdownComponents; + }): JSX.Element; + default: typeof DefaultComponents; +} + +export const AIMarkdown: AIMarkdown = (props) => { + const mergedMarkdownComponents: MarkdownComponents = useMemo( + () => ({ + ...DefaultComponents, + ...props.markdownComponents, + }), + [props.markdownComponents] + ); + + const mergedToolComponents: ToolComponents = useMemo( + () => ({ + ...props.toolComponents, + // ...DefaultTools, + }), + [props.toolComponents] + ); + + return ( + + + {props.children} + + + ); +}; + +AIMarkdown.default = DefaultComponents; diff --git a/packages/react-sdk/src/components/tools/index.ts b/packages/react-sdk/src/components/tools/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/react-sdk/src/index.ts b/packages/react-sdk/src/index.ts index e69de29..bbe54e7 100644 --- a/packages/react-sdk/src/index.ts +++ b/packages/react-sdk/src/index.ts @@ -0,0 +1 @@ +export * from './components/ai-markdown'; \ No newline at end of file diff --git a/packages/react-sdk/tsconfig.lib.json b/packages/react-sdk/tsconfig.lib.json index a3ae812..d94599e 100644 --- a/packages/react-sdk/tsconfig.lib.json +++ b/packages/react-sdk/tsconfig.lib.json @@ -1,12 +1,13 @@ { - "extends": "../../tsconfig.root.json", - "compilerOptions": { - "rootDir": "./src", - "outDir": "./dist/types", - "emitDeclarationOnly": true, - "declarationMap": true, - "verbatimModuleSyntax": false - }, - "include": ["src"], - "exclude": ["src/**/*.test.{ts,tsx}"] + "extends": "../../tsconfig.root.json", + "compilerOptions": { + "jsx": "react-jsx", + "rootDir": "./src", + "outDir": "./dist/types", + "emitDeclarationOnly": true, + "declarationMap": true, + "verbatimModuleSyntax": true, + }, + "include": ["src"], + "exclude": ["src/**/*.test.{ts,tsx}"] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c7d84e6..58e47b2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,9 +35,6 @@ importers: eslint-plugin-import: specifier: ^2.32.0 version: 2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.38.0(jiti@2.6.1)) - globals: - specifier: ^16.4.0 - version: 16.4.0 prettier: specifier: ^3.6.2 version: 3.6.2 @@ -141,6 +138,9 @@ importers: examples/react-example: dependencies: + '@stream-io/ai-components-react': + specifier: workspace:^ + version: link:../../packages/react-sdk react: specifier: ^19.1.1 version: 19.2.0 @@ -258,22 +258,37 @@ importers: packages/react-sdk: dependencies: + clsx: + specifier: ^2.1.1 + version: 2.1.1 react: specifier: ^17 || ^18 || ^19 version: 19.2.0 react-dom: specifier: ^17 || ^18 || ^19 version: 19.2.0(react@19.2.0) + react-markdown: + specifier: ^10.1.0 + version: 10.1.0(@types/react@18.3.26)(react@19.2.0) + react-syntax-highlighter: + specifier: 15.5.0 + version: 15.5.0(react@19.2.0) + remark-gfm: + specifier: ^4.0.1 + version: 4.0.1 devDependencies: '@types/node': specifier: ^24 version: 24.9.1 '@types/react': - specifier: ^17 - version: 17.0.89 + specifier: ^18.3.26 + version: 18.3.26 '@types/react-dom': - specifier: ^17 - version: 17.0.26(@types/react@17.0.89) + specifier: ^18.3.7 + version: 18.3.7(@types/react@18.3.26) + '@types/react-syntax-highlighter': + specifier: ^15.5.13 + version: 15.5.13 concurrently: specifier: 'catalog:' version: 9.2.1 @@ -2193,10 +2208,10 @@ packages: '@types/prop-types@15.7.15': resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} - '@types/react-dom@17.0.26': - resolution: {integrity: sha512-Z+2VcYXJwOqQ79HreLU/1fyQ88eXSSFh6I3JdrEHQIfYSI0kCQpTGvOrbE6jFGGYXKsHuwY9tBa/w5Uo6KzrEg==} + '@types/react-dom@18.3.7': + resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} peerDependencies: - '@types/react': ^17.0.0 + '@types/react': ^18.0.0 '@types/react-dom@19.2.2': resolution: {integrity: sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==} @@ -2206,8 +2221,8 @@ packages: '@types/react-syntax-highlighter@15.5.13': resolution: {integrity: sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==} - '@types/react@17.0.89': - resolution: {integrity: sha512-I98SaDCar5lvEYl80ClRIUztH/hyWHR+I2f+5yTVp/MQ205HgYkA2b5mVdry/+nsEIrf8I65KA5V/PASx68MsQ==} + '@types/react@18.3.26': + resolution: {integrity: sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==} '@types/react@19.1.1': resolution: {integrity: sha512-ePapxDL7qrgqSF67s0h9m412d9DbXyC1n59O2st+9rjuuamWsZuD2w55rqY12CbzsZ7uVXb5Nw0gEp9Z8MMutQ==} @@ -2215,12 +2230,6 @@ packages: '@types/react@19.2.2': resolution: {integrity: sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==} - '@types/scheduler@0.16.8': - resolution: {integrity: sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==} - - '@types/stack-utils@2.0.3': - resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} - '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -8407,9 +8416,9 @@ snapshots: '@types/prop-types@15.7.15': {} - '@types/react-dom@17.0.26(@types/react@17.0.89)': + '@types/react-dom@18.3.7(@types/react@18.3.26)': dependencies: - '@types/react': 17.0.89 + '@types/react': 18.3.26 '@types/react-dom@19.2.2(@types/react@19.2.2)': dependencies: @@ -8419,10 +8428,9 @@ snapshots: dependencies: '@types/react': 19.2.2 - '@types/react@17.0.89': + '@types/react@18.3.26': dependencies: '@types/prop-types': 15.7.15 - '@types/scheduler': 0.16.8 csstype: 3.1.3 '@types/react@19.1.1': @@ -8433,10 +8441,6 @@ snapshots: dependencies: csstype: 3.1.3 - '@types/scheduler@0.16.8': {} - - '@types/stack-utils@2.0.3': {} - '@types/unist@2.0.11': {} '@types/unist@3.0.3': {} @@ -12023,7 +12027,23 @@ snapshots: react-is@16.13.1: {} - react-is@18.3.1: {} + react-markdown@10.1.0(@types/react@18.3.26)(react@19.2.0): + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/react': 18.3.26 + devlop: 1.1.0 + hast-util-to-jsx-runtime: 2.3.6 + html-url-attributes: 3.0.1 + mdast-util-to-hast: 13.2.0 + react: 19.2.0 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + unified: 11.0.5 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color react-markdown@10.1.0(@types/react@19.2.2)(react@19.2.0): dependencies: From d544e19c77701b0669d5c94881bd500186ecf215 Mon Sep 17 00:00:00 2001 From: Anton Arnautov Date: Tue, 4 Nov 2025 14:59:00 +0100 Subject: [PATCH 2/4] Example adjustments --- examples/nextjs-ai-chatbot/app/globals.css | 37 ++- examples/nextjs-ai-chatbot/app/rsh-dark.css | 152 ++++++++++++ .../nextjs-ai-chatbot/components/messages.tsx | 226 +++++++++++------- examples/nextjs-ai-chatbot/package.json | 2 + pnpm-lock.yaml | 8 +- 5 files changed, 308 insertions(+), 117 deletions(-) create mode 100644 examples/nextjs-ai-chatbot/app/rsh-dark.css diff --git a/examples/nextjs-ai-chatbot/app/globals.css b/examples/nextjs-ai-chatbot/app/globals.css index 902d4e1..fc4d00d 100644 --- a/examples/nextjs-ai-chatbot/app/globals.css +++ b/examples/nextjs-ai-chatbot/app/globals.css @@ -1,38 +1,29 @@ -@import 'tailwindcss'; +@import url("./rsh-dark.css") layer(syntax-highlighter); +@import "tailwindcss"; @plugin "@tailwindcss/typography"; @plugin "daisyui" { - themes: dim; + themes: dim; } :root { - --primary: blue; + --primary: blue; } .overflow-y-auto { - scrollbar-gutter: stable; -} - -pre { - display: block !important; -} - -pre pre { - border: none !important; - background-color: transparent !important; - box-shadow: none !important; + scrollbar-gutter: stable; } .ai-thinking { - background: linear-gradient(90deg, #00ffe0, #0072ff, #00ffe0); - background-size: 200% auto; - color: transparent; - background-clip: text; - -webkit-background-clip: text; - animation: shimmer 2s linear infinite; + background: linear-gradient(90deg, #00ffe0, #0072ff, #00ffe0); + background-size: 200% auto; + color: transparent; + background-clip: text; + -webkit-background-clip: text; + animation: shimmer 2s linear infinite; } @keyframes shimmer { - to { - background-position: 200% center; - } + to { + background-position: 200% center; + } } diff --git a/examples/nextjs-ai-chatbot/app/rsh-dark.css b/examples/nextjs-ai-chatbot/app/rsh-dark.css new file mode 100644 index 0000000..0a632f1 --- /dev/null +++ b/examples/nextjs-ai-chatbot/app/rsh-dark.css @@ -0,0 +1,152 @@ +code[class*="language-"] { + color: white; + hyphens: none; + tab-size: 4; + font-size: 1em; + word-wrap: normal; + background: none; + text-align: left; + word-break: normal; + -ms-hyphens: none; + -o-tab-size: 4; + font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace; + line-height: 1.5; + text-shadow: 0 -0.1em 0.2em black; + white-space: pre; + -moz-hyphens: none; + word-spacing: normal; + -moz-tab-size: 4; + -webkit-hyphens: none; + } + pre[class*="language-"] { + color: white; + border: 0.3em solid hsl(30, 20%, 40%); + margin: 0.5em 0; + hyphens: none; + padding: 1em; + overflow: auto; + tab-size: 4; + font-size: 1em; + word-wrap: normal; + background: hsl(30, 20%, 25%); + box-shadow: 1px 1px 0.5em black inset; + text-align: left; + word-break: normal; + -ms-hyphens: none; + -o-tab-size: 4; + font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace; + line-height: 1.5; + text-shadow: 0 -0.1em 0.2em black; + white-space: pre; + -moz-hyphens: none; + word-spacing: normal; + -moz-tab-size: 4; + border-radius: 0.5em; + -webkit-hyphens: none; + } + :not(pre) > code[class*="language-"] { + border: 0.13em solid hsl(30, 20%, 40%); + padding: 0.15em 0.2em 0.05em; + background: hsl(30, 20%, 25%); + box-shadow: 1px 1px 0.3em -0.1em black inset; + white-space: normal; + border-radius: 0.3em; + } + .comment { + color: hsl(30, 20%, 50%); + } + .prolog { + color: hsl(30, 20%, 50%); + } + .doctype { + color: hsl(30, 20%, 50%); + } + .cdata { + color: hsl(30, 20%, 50%); + } + .punctuation { + -opacity: 0.7; + } + .namespace { + -opacity: 0.7; + } + .property { + color: hsl(350, 40%, 70%); + } + .tag { + color: hsl(350, 40%, 70%); + } + .boolean { + color: hsl(350, 40%, 70%); + } + .number { + color: hsl(350, 40%, 70%); + } + .constant { + color: hsl(350, 40%, 70%); + } + .symbol { + color: hsl(350, 40%, 70%); + } + .selector { + color: hsl(75, 70%, 60%); + } + .attr-name { + color: hsl(75, 70%, 60%); + } + .string { + color: hsl(75, 70%, 60%); + } + .char { + color: hsl(75, 70%, 60%); + } + .builtin { + color: hsl(75, 70%, 60%); + } + .inserted { + color: hsl(75, 70%, 60%); + } + .operator { + color: hsl(40, 90%, 60%); + } + .entity { + color: hsl(40, 90%, 60%); + cursor: help; + } + .url { + color: hsl(40, 90%, 60%); + } + .language-css .token.string { + color: hsl(40, 90%, 60%); + } + .style .token.string { + color: hsl(40, 90%, 60%); + } + .variable { + color: hsl(40, 90%, 60%); + } + .atrule { + color: hsl(350, 40%, 70%); + } + .attr-value { + color: hsl(350, 40%, 70%); + } + .keyword { + color: hsl(350, 40%, 70%); + } + .regex { + color: #e90; + } + .important { + color: #e90; + font-weight: bold; + } + .bold { + font-weight: bold; + } + .italic { + font-style: italic; + } + .deleted { + color: red; + } \ No newline at end of file diff --git a/examples/nextjs-ai-chatbot/components/messages.tsx b/examples/nextjs-ai-chatbot/components/messages.tsx index 3e3b75c..b7b89ac 100644 --- a/examples/nextjs-ai-chatbot/components/messages.tsx +++ b/examples/nextjs-ai-chatbot/components/messages.tsx @@ -1,102 +1,142 @@ 'use client'; -import { UIMessage } from '@ai-sdk/react'; -import { Sparkles } from 'lucide-react'; -import { Markdown } from '@/components/markdown'; -import { useApp } from '@/contexts/app'; -import { useEffect, useRef } from 'react'; -import { useParams } from 'next/navigation'; -import Image from 'next/image'; -import ToolsOutput from './tools'; +import { UIMessage } from "@ai-sdk/react"; +import { Sparkles } from "lucide-react"; +import { Markdown } from "@/components/markdown"; +import { useApp } from "@/contexts/app"; +import { useEffect, useRef } from "react"; +import { useParams } from "next/navigation"; +import Image from "next/image"; +import ToolsOutput from "./tools"; +import { AIMarkdown } from "@stream-io/ai-components-react"; +import Weather from "./tools/weather"; +import clsx from "clsx"; export default function Messages() { - const { messages, status, isLoadingMessages } = useApp(); - const messagesEndRef = useRef(null); - const { id } = useParams(); - const scrollToBottom = () => { - messagesEndRef.current?.scrollIntoView(); - }; + const { messages, status, isLoadingMessages } = useApp(); + const messagesEndRef = useRef(null); + const { id } = useParams(); + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView(); + }; - useEffect(() => { - scrollToBottom(); - }, [messages, isLoadingMessages]); + useEffect(() => { + scrollToBottom(); + }, [messages, isLoadingMessages]); - if (isLoadingMessages) { - return ( -
- -
- ); - } + if (isLoadingMessages) { + return ( +
+ +
+ ); + } - if (!id) { - return ( -
-

- Welcome to AI Assistant -

-

- Ready to help you with any questions or tasks. How can I assist you - today? -

-
- ); - } + if (!id) { + return ( +
+

+ Welcome to AI Assistant +

+

+ Ready to help you with any questions or tasks. How can I assist you + today? +

+
+ ); + } - return ( -
- {messages.map((m: UIMessage) => ( -
-
- {m.parts.map( - (part, index) => - part.type === 'file' && - part.url && ( - - {part.filename - - ), - )} -
-
- - {m.parts - .map((part: any) => (part.type === 'text' ? part.text : '')) - .join('')} - - -
-
- ))} - {status === 'submitted' && ( -
- - Thinking -
- )} -
-
- ); + return ( +
+ {messages.map((m: UIMessage) => ( +
+
+ {m.parts.map( + (part, index) => + part.type === "file" && + part.url && ( + + {part.filename + + ) + )} +
+
+
{children}
, + code: ({ children, ...rest }) => ( + + {children} + + ), + ol: ({ node, children, ...props }) => ( +
    + {children} +
+ ), + li: ({ node, children, ...props }) => { + return ( +
  • + {children} +
  • + ); + }, + ul: ({ node, children, ...props }) => { + return ( +
      + {children} +
    + ); + }, + }} + > + {m.parts + .map((part: any) => (part.type === "text" ? part.text : "")) + .join("")} +
    + +
    +
    + ))} + {status === "submitted" && ( +
    + + Thinking +
    + )} +
    +
    + ); } diff --git a/examples/nextjs-ai-chatbot/package.json b/examples/nextjs-ai-chatbot/package.json index 90fd816..5cd39bf 100644 --- a/examples/nextjs-ai-chatbot/package.json +++ b/examples/nextjs-ai-chatbot/package.json @@ -13,10 +13,12 @@ "@ai-sdk/openai": "^2.0.53", "@ai-sdk/react": "^2.0.76", "@ai-sdk/xai": "^2.0.27", + "@stream-io/ai-components-react": "workspace:^", "@stream-io/ai-sdk-storage": "workspace:^", "@tailwindcss/typography": "^0.5.19", "ai": "^5.0.76", "animate.css": "^4.1.1", + "clsx": "^2.1.1", "daisyui": "^5.3.7", "lucide-react": "^0.546.0", "next": "16.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 58e47b2..a04d86f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -61,7 +61,10 @@ importers: version: 2.0.86(react@19.2.0)(zod@4.1.12) '@ai-sdk/xai': specifier: ^2.0.27 - version: 2.0.30(zod@4.1.12) + version: 2.0.29(zod@4.1.12) + '@stream-io/ai-components-react': + specifier: workspace:^ + version: link:../../packages/react-sdk '@stream-io/ai-sdk-storage': specifier: workspace:^ version: link:../../packages/node-sdk @@ -74,6 +77,9 @@ importers: animate.css: specifier: ^4.1.1 version: 4.1.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 daisyui: specifier: ^5.3.7 version: 5.4.2 From 3735bd6181858e3733fba4f6ca285bc92ea22f93 Mon Sep 17 00:00:00 2001 From: Anton Arnautov Date: Tue, 4 Nov 2025 15:04:24 +0100 Subject: [PATCH 3/4] Post-rebase fixes --- examples/nextjs-ai-chatbot/app/globals.css | 28 +- examples/nextjs-ai-chatbot/app/rsh-dark.css | 304 +++++++++--------- .../nextjs-ai-chatbot/components/messages.tsx | 266 +++++++-------- packages/react-sdk/package.json | 134 ++++---- .../react-sdk/src/components/ai-markdown.tsx | 256 +++++++-------- packages/react-sdk/src/index.ts | 2 +- packages/react-sdk/tsconfig.lib.json | 22 +- pnpm-lock.yaml | 232 +++++-------- 8 files changed, 578 insertions(+), 666 deletions(-) diff --git a/examples/nextjs-ai-chatbot/app/globals.css b/examples/nextjs-ai-chatbot/app/globals.css index fc4d00d..a204c6c 100644 --- a/examples/nextjs-ai-chatbot/app/globals.css +++ b/examples/nextjs-ai-chatbot/app/globals.css @@ -1,29 +1,29 @@ -@import url("./rsh-dark.css") layer(syntax-highlighter); -@import "tailwindcss"; +@import url('./rsh-dark.css') layer(syntax-highlighter); +@import 'tailwindcss'; @plugin "@tailwindcss/typography"; @plugin "daisyui" { - themes: dim; + themes: dim; } :root { - --primary: blue; + --primary: blue; } .overflow-y-auto { - scrollbar-gutter: stable; + scrollbar-gutter: stable; } .ai-thinking { - background: linear-gradient(90deg, #00ffe0, #0072ff, #00ffe0); - background-size: 200% auto; - color: transparent; - background-clip: text; - -webkit-background-clip: text; - animation: shimmer 2s linear infinite; + background: linear-gradient(90deg, #00ffe0, #0072ff, #00ffe0); + background-size: 200% auto; + color: transparent; + background-clip: text; + -webkit-background-clip: text; + animation: shimmer 2s linear infinite; } @keyframes shimmer { - to { - background-position: 200% center; - } + to { + background-position: 200% center; + } } diff --git a/examples/nextjs-ai-chatbot/app/rsh-dark.css b/examples/nextjs-ai-chatbot/app/rsh-dark.css index 0a632f1..61a6d9e 100644 --- a/examples/nextjs-ai-chatbot/app/rsh-dark.css +++ b/examples/nextjs-ai-chatbot/app/rsh-dark.css @@ -1,152 +1,152 @@ -code[class*="language-"] { - color: white; - hyphens: none; - tab-size: 4; - font-size: 1em; - word-wrap: normal; - background: none; - text-align: left; - word-break: normal; - -ms-hyphens: none; - -o-tab-size: 4; - font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace; - line-height: 1.5; - text-shadow: 0 -0.1em 0.2em black; - white-space: pre; - -moz-hyphens: none; - word-spacing: normal; - -moz-tab-size: 4; - -webkit-hyphens: none; - } - pre[class*="language-"] { - color: white; - border: 0.3em solid hsl(30, 20%, 40%); - margin: 0.5em 0; - hyphens: none; - padding: 1em; - overflow: auto; - tab-size: 4; - font-size: 1em; - word-wrap: normal; - background: hsl(30, 20%, 25%); - box-shadow: 1px 1px 0.5em black inset; - text-align: left; - word-break: normal; - -ms-hyphens: none; - -o-tab-size: 4; - font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace; - line-height: 1.5; - text-shadow: 0 -0.1em 0.2em black; - white-space: pre; - -moz-hyphens: none; - word-spacing: normal; - -moz-tab-size: 4; - border-radius: 0.5em; - -webkit-hyphens: none; - } - :not(pre) > code[class*="language-"] { - border: 0.13em solid hsl(30, 20%, 40%); - padding: 0.15em 0.2em 0.05em; - background: hsl(30, 20%, 25%); - box-shadow: 1px 1px 0.3em -0.1em black inset; - white-space: normal; - border-radius: 0.3em; - } - .comment { - color: hsl(30, 20%, 50%); - } - .prolog { - color: hsl(30, 20%, 50%); - } - .doctype { - color: hsl(30, 20%, 50%); - } - .cdata { - color: hsl(30, 20%, 50%); - } - .punctuation { - -opacity: 0.7; - } - .namespace { - -opacity: 0.7; - } - .property { - color: hsl(350, 40%, 70%); - } - .tag { - color: hsl(350, 40%, 70%); - } - .boolean { - color: hsl(350, 40%, 70%); - } - .number { - color: hsl(350, 40%, 70%); - } - .constant { - color: hsl(350, 40%, 70%); - } - .symbol { - color: hsl(350, 40%, 70%); - } - .selector { - color: hsl(75, 70%, 60%); - } - .attr-name { - color: hsl(75, 70%, 60%); - } - .string { - color: hsl(75, 70%, 60%); - } - .char { - color: hsl(75, 70%, 60%); - } - .builtin { - color: hsl(75, 70%, 60%); - } - .inserted { - color: hsl(75, 70%, 60%); - } - .operator { - color: hsl(40, 90%, 60%); - } - .entity { - color: hsl(40, 90%, 60%); - cursor: help; - } - .url { - color: hsl(40, 90%, 60%); - } - .language-css .token.string { - color: hsl(40, 90%, 60%); - } - .style .token.string { - color: hsl(40, 90%, 60%); - } - .variable { - color: hsl(40, 90%, 60%); - } - .atrule { - color: hsl(350, 40%, 70%); - } - .attr-value { - color: hsl(350, 40%, 70%); - } - .keyword { - color: hsl(350, 40%, 70%); - } - .regex { - color: #e90; - } - .important { - color: #e90; - font-weight: bold; - } - .bold { - font-weight: bold; - } - .italic { - font-style: italic; - } - .deleted { - color: red; - } \ No newline at end of file +code[class*='language-'] { + color: white; + hyphens: none; + tab-size: 4; + font-size: 1em; + word-wrap: normal; + background: none; + text-align: left; + word-break: normal; + -ms-hyphens: none; + -o-tab-size: 4; + font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; + line-height: 1.5; + text-shadow: 0 -0.1em 0.2em black; + white-space: pre; + -moz-hyphens: none; + word-spacing: normal; + -moz-tab-size: 4; + -webkit-hyphens: none; +} +pre[class*='language-'] { + color: white; + border: 0.3em solid hsl(30, 20%, 40%); + margin: 0.5em 0; + hyphens: none; + padding: 1em; + overflow: auto; + tab-size: 4; + font-size: 1em; + word-wrap: normal; + background: hsl(30, 20%, 25%); + box-shadow: 1px 1px 0.5em black inset; + text-align: left; + word-break: normal; + -ms-hyphens: none; + -o-tab-size: 4; + font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; + line-height: 1.5; + text-shadow: 0 -0.1em 0.2em black; + white-space: pre; + -moz-hyphens: none; + word-spacing: normal; + -moz-tab-size: 4; + border-radius: 0.5em; + -webkit-hyphens: none; +} +:not(pre) > code[class*='language-'] { + border: 0.13em solid hsl(30, 20%, 40%); + padding: 0.15em 0.2em 0.05em; + background: hsl(30, 20%, 25%); + box-shadow: 1px 1px 0.3em -0.1em black inset; + white-space: normal; + border-radius: 0.3em; +} +.comment { + color: hsl(30, 20%, 50%); +} +.prolog { + color: hsl(30, 20%, 50%); +} +.doctype { + color: hsl(30, 20%, 50%); +} +.cdata { + color: hsl(30, 20%, 50%); +} +.punctuation { + -opacity: 0.7; +} +.namespace { + -opacity: 0.7; +} +.property { + color: hsl(350, 40%, 70%); +} +.tag { + color: hsl(350, 40%, 70%); +} +.boolean { + color: hsl(350, 40%, 70%); +} +.number { + color: hsl(350, 40%, 70%); +} +.constant { + color: hsl(350, 40%, 70%); +} +.symbol { + color: hsl(350, 40%, 70%); +} +.selector { + color: hsl(75, 70%, 60%); +} +.attr-name { + color: hsl(75, 70%, 60%); +} +.string { + color: hsl(75, 70%, 60%); +} +.char { + color: hsl(75, 70%, 60%); +} +.builtin { + color: hsl(75, 70%, 60%); +} +.inserted { + color: hsl(75, 70%, 60%); +} +.operator { + color: hsl(40, 90%, 60%); +} +.entity { + color: hsl(40, 90%, 60%); + cursor: help; +} +.url { + color: hsl(40, 90%, 60%); +} +.language-css .token.string { + color: hsl(40, 90%, 60%); +} +.style .token.string { + color: hsl(40, 90%, 60%); +} +.variable { + color: hsl(40, 90%, 60%); +} +.atrule { + color: hsl(350, 40%, 70%); +} +.attr-value { + color: hsl(350, 40%, 70%); +} +.keyword { + color: hsl(350, 40%, 70%); +} +.regex { + color: #e90; +} +.important { + color: #e90; + font-weight: bold; +} +.bold { + font-weight: bold; +} +.italic { + font-style: italic; +} +.deleted { + color: red; +} diff --git a/examples/nextjs-ai-chatbot/components/messages.tsx b/examples/nextjs-ai-chatbot/components/messages.tsx index b7b89ac..0255dbb 100644 --- a/examples/nextjs-ai-chatbot/components/messages.tsx +++ b/examples/nextjs-ai-chatbot/components/messages.tsx @@ -1,142 +1,142 @@ 'use client'; -import { UIMessage } from "@ai-sdk/react"; -import { Sparkles } from "lucide-react"; -import { Markdown } from "@/components/markdown"; -import { useApp } from "@/contexts/app"; -import { useEffect, useRef } from "react"; -import { useParams } from "next/navigation"; -import Image from "next/image"; -import ToolsOutput from "./tools"; -import { AIMarkdown } from "@stream-io/ai-components-react"; -import Weather from "./tools/weather"; -import clsx from "clsx"; +import { UIMessage } from '@ai-sdk/react'; +import { Sparkles } from 'lucide-react'; +import { Markdown } from '@/components/markdown'; +import { useApp } from '@/contexts/app'; +import { useEffect, useRef } from 'react'; +import { useParams } from 'next/navigation'; +import Image from 'next/image'; +import ToolsOutput from './tools'; +import { AIMarkdown } from '@stream-io/ai-components-react'; +import Weather from './tools/weather'; +import clsx from 'clsx'; export default function Messages() { - const { messages, status, isLoadingMessages } = useApp(); - const messagesEndRef = useRef(null); - const { id } = useParams(); - const scrollToBottom = () => { - messagesEndRef.current?.scrollIntoView(); - }; + const { messages, status, isLoadingMessages } = useApp(); + const messagesEndRef = useRef(null); + const { id } = useParams(); + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView(); + }; - useEffect(() => { - scrollToBottom(); - }, [messages, isLoadingMessages]); + useEffect(() => { + scrollToBottom(); + }, [messages, isLoadingMessages]); - if (isLoadingMessages) { - return ( -
    - -
    - ); - } + if (isLoadingMessages) { + return ( +
    + +
    + ); + } - if (!id) { - return ( -
    -

    - Welcome to AI Assistant -

    -

    - Ready to help you with any questions or tasks. How can I assist you - today? -

    -
    - ); - } + if (!id) { + return ( +
    +

    + Welcome to AI Assistant +

    +

    + Ready to help you with any questions or tasks. How can I assist you + today? +

    +
    + ); + } - return ( -
    - {messages.map((m: UIMessage) => ( -
    -
    - {m.parts.map( - (part, index) => - part.type === "file" && - part.url && ( - - {part.filename - - ) - )} -
    -
    -
    {children}
    , - code: ({ children, ...rest }) => ( - - {children} - - ), - ol: ({ node, children, ...props }) => ( -
      - {children} -
    - ), - li: ({ node, children, ...props }) => { - return ( -
  • - {children} -
  • - ); - }, - ul: ({ node, children, ...props }) => { - return ( -
      - {children} -
    - ); - }, - }} - > - {m.parts - .map((part: any) => (part.type === "text" ? part.text : "")) - .join("")} -
    - -
    -
    - ))} - {status === "submitted" && ( -
    - - Thinking -
    - )} -
    -
    - ); + return ( +
    + {messages.map((m: UIMessage) => ( +
    +
    + {m.parts.map( + (part, index) => + part.type === 'file' && + part.url && ( + + {part.filename + + ), + )} +
    +
    +
    {children}
    , + code: ({ children, ...rest }) => ( + + {children} + + ), + ol: ({ node, children, ...props }) => ( +
      + {children} +
    + ), + li: ({ node, children, ...props }) => { + return ( +
  • + {children} +
  • + ); + }, + ul: ({ node, children, ...props }) => { + return ( +
      + {children} +
    + ); + }, + }} + > + {m.parts + .map((part: any) => (part.type === 'text' ? part.text : '')) + .join('')} +
    + +
    +
    + ))} + {status === 'submitted' && ( +
    + + Thinking +
    + )} +
    +
    + ); } diff --git a/packages/react-sdk/package.json b/packages/react-sdk/package.json index 6ff0df8..f8ef9f7 100644 --- a/packages/react-sdk/package.json +++ b/packages/react-sdk/package.json @@ -1,69 +1,69 @@ { - "name": "@stream-io/ai-components-react", - "version": "1.0.0", - "private": true, - "description": "React SDK for AI Components", - "main": "./dist/cjs/index.js", - "types": "./dist/types/index.d.ts", - "exports": { - ".": { - "types": "./dist/types/index.d.ts", - "browser": { - "import": "./dist/es/index.mjs", - "require": "./dist/cjs/index.js" - }, - "default": "./dist/cjs/index.js" - }, - "./stream": { - "types": "./dist/types/stream/index.d.ts", - "browser": { - "import": "./dist/es/stream/index.mjs", - "require": "./dist/cjs/stream/index.js" - }, - "default": "./dist/cjs/stream/index.js" - } - }, - "files": [ - "dist", - "src", - "!src/*.test.ts" - ], - "scripts": { - "build": "rimraf ./dist && concurrently 'vite build' 'tsc -p ./tsconfig.lib.json'", - "dev": "concurrently 'vite build --watch' 'tsc -p ./tsconfig.lib.json' --watch", - "prepare": "pnpm run build", - "test": "" - }, - "keywords": [ - "ai", - "react", - "components", - "sdk" - ], - "author": { - "name": "GetStream.io, Inc.", - "url": "https://getstream.io/team/" - }, - "license": "SEE LICENSE IN LICENSE", - "packageManager": "pnpm@10.13.1", - "peerDependencies": { - "react": "^17 || ^18 || ^19", - "react-dom": "^17 || ^18 || ^19" - }, - "devDependencies": { - "@types/node": "^24", - "@types/react": "^18.3.26", - "@types/react-dom": "^18.3.7", - "@types/react-syntax-highlighter": "^15.5.13", - "concurrently": "catalog:", - "rimraf": "^6.0.1", - "typescript": "catalog:", - "vite": "catalog:" - }, - "dependencies": { - "clsx": "^2.1.1", - "react-markdown": "^10.1.0", - "react-syntax-highlighter": "15.5.0", - "remark-gfm": "^4.0.1" - } + "name": "@stream-io/ai-components-react", + "version": "1.0.0", + "private": true, + "description": "React SDK for AI Components", + "main": "./dist/cjs/index.js", + "types": "./dist/types/index.d.ts", + "exports": { + ".": { + "types": "./dist/types/index.d.ts", + "browser": { + "import": "./dist/es/index.mjs", + "require": "./dist/cjs/index.js" + }, + "default": "./dist/cjs/index.js" + }, + "./stream": { + "types": "./dist/types/stream/index.d.ts", + "browser": { + "import": "./dist/es/stream/index.mjs", + "require": "./dist/cjs/stream/index.js" + }, + "default": "./dist/cjs/stream/index.js" + } + }, + "files": [ + "dist", + "src", + "!src/*.test.ts" + ], + "scripts": { + "build": "rimraf ./dist && concurrently 'vite build' 'tsc -p ./tsconfig.lib.json'", + "dev": "concurrently 'vite build --watch' 'tsc -p ./tsconfig.lib.json' --watch", + "prepare": "pnpm run build", + "test": "" + }, + "keywords": [ + "ai", + "react", + "components", + "sdk" + ], + "author": { + "name": "GetStream.io, Inc.", + "url": "https://getstream.io/team/" + }, + "license": "SEE LICENSE IN LICENSE", + "packageManager": "pnpm@10.13.1", + "peerDependencies": { + "react": "^17 || ^18 || ^19", + "react-dom": "^17 || ^18 || ^19" + }, + "devDependencies": { + "@types/node": "^24", + "@types/react": "^18.3.26", + "@types/react-dom": "^18.3.7", + "@types/react-syntax-highlighter": "^15.5.13", + "concurrently": "catalog:", + "rimraf": "^6.0.1", + "typescript": "catalog:", + "vite": "catalog:" + }, + "dependencies": { + "clsx": "^2.1.1", + "react-markdown": "^10.1.0", + "react-syntax-highlighter": "15.5.0", + "remark-gfm": "^4.0.1" + } } diff --git a/packages/react-sdk/src/components/ai-markdown.tsx b/packages/react-sdk/src/components/ai-markdown.tsx index b6f21b3..d648e53 100644 --- a/packages/react-sdk/src/components/ai-markdown.tsx +++ b/packages/react-sdk/src/components/ai-markdown.tsx @@ -1,176 +1,176 @@ import React, { - Children, - type ComponentProps, - type ComponentType, - type ElementType, - isValidElement, - useContext, - useMemo, -} from "react"; + Children, + type ComponentProps, + type ComponentType, + type ElementType, + isValidElement, + useContext, + useMemo, +} from 'react'; import ReactMarkdown, { - type Components, - type ExtraProps, -} from "react-markdown"; -import remarkGfm from "remark-gfm"; -import { Prism, type SyntaxHighlighterProps } from "react-syntax-highlighter"; -import clsx from "clsx"; + type Components, + type ExtraProps, +} from 'react-markdown'; +import remarkGfm from 'remark-gfm'; +import { Prism, type SyntaxHighlighterProps } from 'react-syntax-highlighter'; +import clsx from 'clsx'; const SyntaxHighlighter = - Prism as unknown as ComponentType; + Prism as unknown as ComponentType; -const getToolOrLanguage = (className: string = "") => { - return className.match(/language-(?[\w-]+)/)?.groups?.["tool"]; +const getToolOrLanguage = (className: string = '') => { + return className.match(/language-(?[\w-]+)/)?.groups?.['tool']; }; type ToolComponents = Record>; type MarkdownComponents = Components; const AIMarkdownContext = React.createContext<{ - toolComponents: ToolComponents; + toolComponents: ToolComponents; }>({ toolComponents: {} }); -type BaseDefaultPreProps = ComponentProps<"pre"> & ExtraProps; +type BaseDefaultPreProps = ComponentProps<'pre'> & ExtraProps; type DefaultPreProps = BaseDefaultPreProps & { - Pre?: ComponentType | ElementType; + Pre?: ComponentType | ElementType; }; const DefaultPre = (props: DefaultPreProps) => { - const { children, Pre = "pre", ...restProps } = props; + const { children, Pre = 'pre', ...restProps } = props; - const { toolComponents } = useContext(AIMarkdownContext); + const { toolComponents } = useContext(AIMarkdownContext); - const [codeElement] = Children.toArray(children); + const [codeElement] = Children.toArray(children); - if ( - !isValidElement(codeElement) || - codeElement.props.node.tagName !== "code" - ) { - return
    {children}
    ; - } + if ( + !isValidElement(codeElement) || + codeElement.props.node.tagName !== 'code' + ) { + return
    {children}
    ; + } - const tool = getToolOrLanguage(codeElement.props.className); + const tool = getToolOrLanguage(codeElement.props.className); - // grab from pre-registered component set and render - const Component = typeof tool === "string" ? toolComponents[tool] : null; - if (Component) { - return ; - } + // grab from pre-registered component set and render + const Component = typeof tool === 'string' ? toolComponents[tool] : null; + if (Component) { + return ; + } - // render just a fragment with the code content - // which gets replaced by SyntaxHighlighter (it itself renders pre too) - if (tool) { - return <>{children}; - } + // render just a fragment with the code content + // which gets replaced by SyntaxHighlighter (it itself renders pre too) + if (tool) { + return <>{children}; + } - // treat as regular pre/code block if there's no tool/language - return
    {children}
    ; + // treat as regular pre/code block if there's no tool/language + return
    {children}
    ; }; const DefaultSyntaxHighlighter = ({ - children, - language, + children, + language, }: BaseDefaultCodeProps) => { - return ( - - {children as string} - - ); + return ( + + {children as string} + + ); }; -type BaseDefaultCodeProps = ComponentProps<"code"> & - ExtraProps & { language?: string; inline?: boolean }; +type BaseDefaultCodeProps = ComponentProps<'code'> & + ExtraProps & { language?: string; inline?: boolean }; type DefaultCodeProps = BaseDefaultCodeProps & { - Code?: ComponentType | ElementType; - SyntaxHighlighter?: ComponentType; + Code?: ComponentType | ElementType; + SyntaxHighlighter?: ComponentType; }; const DefaultCode = (props: DefaultCodeProps) => { - const { - node, - className, - children, - SyntaxHighlighter = DefaultSyntaxHighlighter, - Code = "code", - ...restProps - } = props; - - const language = getToolOrLanguage(className); - const inline = !language; - - const Component = inline ? Code : SyntaxHighlighter; - - return ( - - {children} - - ); + const { + node, + className, + children, + SyntaxHighlighter = DefaultSyntaxHighlighter, + Code = 'code', + ...restProps + } = props; + + const language = getToolOrLanguage(className); + const inline = !language; + + const Component = inline ? Code : SyntaxHighlighter; + + return ( + + {children} + + ); }; const DefaultComponents = { - pre: DefaultPre, - code: DefaultCode, + pre: DefaultPre, + code: DefaultCode, } as const; interface AIMarkdown { - (props: { - children: string; - toolComponents?: ToolComponents; - markdownComponents?: MarkdownComponents; - }): JSX.Element; - default: typeof DefaultComponents; + (props: { + children: string; + toolComponents?: ToolComponents; + markdownComponents?: MarkdownComponents; + }): JSX.Element; + default: typeof DefaultComponents; } export const AIMarkdown: AIMarkdown = (props) => { - const mergedMarkdownComponents: MarkdownComponents = useMemo( - () => ({ - ...DefaultComponents, - ...props.markdownComponents, - }), - [props.markdownComponents] - ); - - const mergedToolComponents: ToolComponents = useMemo( - () => ({ - ...props.toolComponents, - // ...DefaultTools, - }), - [props.toolComponents] - ); - - return ( - - - {props.children} - - - ); + const mergedMarkdownComponents: MarkdownComponents = useMemo( + () => ({ + ...DefaultComponents, + ...props.markdownComponents, + }), + [props.markdownComponents], + ); + + const mergedToolComponents: ToolComponents = useMemo( + () => ({ + ...props.toolComponents, + // ...DefaultTools, + }), + [props.toolComponents], + ); + + return ( + + + {props.children} + + + ); }; AIMarkdown.default = DefaultComponents; diff --git a/packages/react-sdk/src/index.ts b/packages/react-sdk/src/index.ts index bbe54e7..e1f5e19 100644 --- a/packages/react-sdk/src/index.ts +++ b/packages/react-sdk/src/index.ts @@ -1 +1 @@ -export * from './components/ai-markdown'; \ No newline at end of file +export * from './components/ai-markdown'; diff --git a/packages/react-sdk/tsconfig.lib.json b/packages/react-sdk/tsconfig.lib.json index d94599e..390b480 100644 --- a/packages/react-sdk/tsconfig.lib.json +++ b/packages/react-sdk/tsconfig.lib.json @@ -1,13 +1,13 @@ { - "extends": "../../tsconfig.root.json", - "compilerOptions": { - "jsx": "react-jsx", - "rootDir": "./src", - "outDir": "./dist/types", - "emitDeclarationOnly": true, - "declarationMap": true, - "verbatimModuleSyntax": true, - }, - "include": ["src"], - "exclude": ["src/**/*.test.{ts,tsx}"] + "extends": "../../tsconfig.root.json", + "compilerOptions": { + "jsx": "react-jsx", + "rootDir": "./src", + "outDir": "./dist/types", + "emitDeclarationOnly": true, + "declarationMap": true, + "verbatimModuleSyntax": true + }, + "include": ["src"], + "exclude": ["src/**/*.test.{ts,tsx}"] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a04d86f..55b56e4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,9 @@ importers: eslint-plugin-import: specifier: ^2.32.0 version: 2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.38.0(jiti@2.6.1)) + globals: + specifier: ^16.4.0 + version: 16.4.0 prettier: specifier: ^3.6.2 version: 3.6.2 @@ -61,7 +64,7 @@ importers: version: 2.0.86(react@19.2.0)(zod@4.1.12) '@ai-sdk/xai': specifier: ^2.0.27 - version: 2.0.29(zod@4.1.12) + version: 2.0.30(zod@4.1.12) '@stream-io/ai-components-react': specifier: workspace:^ version: link:../../packages/react-sdk @@ -73,7 +76,7 @@ importers: version: 0.5.19(tailwindcss@4.1.16) ai: specifier: ^5.0.76 - version: 5.0.81(zod@4.1.12) + version: 5.0.86(zod@4.1.12) animate.css: specifier: ^4.1.1 version: 4.1.1 @@ -144,9 +147,6 @@ importers: examples/react-example: dependencies: - '@stream-io/ai-components-react': - specifier: workspace:^ - version: link:../../packages/react-sdk react: specifier: ^19.1.1 version: 19.2.0 @@ -195,7 +195,7 @@ importers: dependencies: ai: specifier: ^5.0.0 - version: 5.0.81(zod@4.1.12) + version: 5.0.86(zod@4.1.12) stream-chat: specifier: ^9.24.0 version: 9.25.0 @@ -307,9 +307,6 @@ importers: vite: specifier: 'catalog:' version: 7.1.11(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1) - vitest: - specifier: 'catalog:' - version: 4.0.6(@types/debug@4.1.12)(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1) packages: @@ -321,12 +318,6 @@ packages: graphql: optional: true - '@ai-sdk/gateway@2.0.2': - resolution: {integrity: sha512-25F1qPqZxOw9IcV9OQCL29hV4HAFLw5bFWlzQLBi5aDhEZsTMT2rMi3umSqNaUxrrw+dLRtjOL7RbHC+WjbA/A==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/gateway@2.0.5': resolution: {integrity: sha512-5TTDSl0USWY6YGnb4QmJGplFZhk+p9OT7hZevAaER6OGiZ17LB1GypsGYDpNo/MiVMklk8kX4gk6p1/R/EiJ8Q==} engines: {node: '>=18'} @@ -351,12 +342,6 @@ packages: peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/provider-utils@3.0.13': - resolution: {integrity: sha512-aXFLBLRPTUYA853MJliItefSXeJPl+mg0KSjbToP41kJ+banBmHO8ZPGLJhNqGlCU82o11TYN7G05EREKX8CkA==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/provider-utils@3.0.15': resolution: {integrity: sha512-kOc6Pxb7CsRlNt+sLZKL7/VGQUd7ccl3/tIK+Bqf5/QhHR0Qm3qRBMz1IwU1RmjJEZA73x+KB5cUckbDl2WF7Q==} engines: {node: '>=18'} @@ -400,10 +385,6 @@ packages: resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} - '@babel/compat-data@7.28.4': - resolution: {integrity: sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==} - engines: {node: '>=6.9.0'} - '@babel/compat-data@7.28.5': resolution: {integrity: sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==} engines: {node: '>=6.9.0'} @@ -412,10 +393,6 @@ packages: resolution: {integrity: sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==} engines: {node: '>=6.9.0'} - '@babel/generator@7.28.3': - resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} - engines: {node: '>=6.9.0'} - '@babel/generator@7.28.5': resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} engines: {node: '>=6.9.0'} @@ -491,10 +468,6 @@ packages: resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.27.1': - resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} - engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.28.5': resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} @@ -515,11 +488,6 @@ packages: resolution: {integrity: sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw==} engines: {node: '>=6.9.0'} - '@babel/parser@7.28.4': - resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==} - engines: {node: '>=6.0.0'} - hasBin: true - '@babel/parser@7.28.5': resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} engines: {node: '>=6.0.0'} @@ -1096,18 +1064,10 @@ packages: resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.28.4': - resolution: {integrity: sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==} - engines: {node: '>=6.9.0'} - '@babel/traverse@7.28.5': resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==} engines: {node: '>=6.9.0'} - '@babel/types@7.28.4': - resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} - engines: {node: '>=6.9.0'} - '@babel/types@7.28.5': resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} engines: {node: '>=6.9.0'} @@ -2236,6 +2196,9 @@ packages: '@types/react@19.2.2': resolution: {integrity: sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==} + '@types/stack-utils@2.0.3': + resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -2485,12 +2448,6 @@ packages: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} engines: {node: '>=8'} - ai@5.0.81: - resolution: {integrity: sha512-SB7oMC9QSpIu1VLswFTZuhhpfQfrGtFBUbWLtHBkhjWZIQskjtcdEhB+N4yO9hscdc2wYtjw/tacgoxX93QWFw==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.25.76 || ^4.1.8 - ai@5.0.86: resolution: {integrity: sha512-ooHwNTkLdedFf98iQhtSc5btc/P4UuXuOpYneoifq0190vqosLunNdW8Hs6CiE0Am7YOGNplDK56JIPlHZIL4w==} engines: {node: '>=18'} @@ -2887,6 +2844,10 @@ packages: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} @@ -6112,13 +6073,6 @@ snapshots: '@0no-co/graphql.web@1.2.0': {} - '@ai-sdk/gateway@2.0.2(zod@4.1.12)': - dependencies: - '@ai-sdk/provider': 2.0.0 - '@ai-sdk/provider-utils': 3.0.13(zod@4.1.12) - '@vercel/oidc': 3.0.3 - zod: 4.1.12 - '@ai-sdk/gateway@2.0.5(zod@4.1.12)': dependencies: '@ai-sdk/provider': 2.0.0 @@ -6144,13 +6098,6 @@ snapshots: '@ai-sdk/provider-utils': 3.0.15(zod@4.1.12) zod: 4.1.12 - '@ai-sdk/provider-utils@3.0.13(zod@4.1.12)': - dependencies: - '@ai-sdk/provider': 2.0.0 - '@standard-schema/spec': 1.0.0 - eventsource-parser: 3.0.6 - zod: 4.1.12 - '@ai-sdk/provider-utils@3.0.15(zod@4.1.12)': dependencies: '@ai-sdk/provider': 2.0.0 @@ -6193,25 +6140,23 @@ snapshots: '@babel/code-frame@7.27.1': dependencies: - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/compat-data@7.28.4': {} - '@babel/compat-data@7.28.5': {} '@babel/core@7.28.4': dependencies: '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.3 + '@babel/generator': 7.28.5 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) '@babel/helpers': 7.28.4 - '@babel/parser': 7.28.4 + '@babel/parser': 7.28.5 '@babel/template': 7.27.2 - '@babel/traverse': 7.28.4 - '@babel/types': 7.28.4 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 debug: 4.4.3 @@ -6221,14 +6166,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/generator@7.28.3': - dependencies: - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - jsesc: 3.1.0 - '@babel/generator@7.28.5': dependencies: '@babel/parser': 7.28.5 @@ -6239,11 +6176,11 @@ snapshots: '@babel/helper-annotate-as-pure@7.27.3': dependencies: - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 '@babel/helper-compilation-targets@7.27.2': dependencies: - '@babel/compat-data': 7.28.4 + '@babel/compat-data': 7.28.5 '@babel/helper-validator-option': 7.27.1 browserslist: 4.27.0 lru-cache: 5.1.1 @@ -6291,8 +6228,8 @@ snapshots: '@babel/helper-module-imports@7.27.1': dependencies: - '@babel/traverse': 7.28.4 - '@babel/types': 7.28.4 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 transitivePeerDependencies: - supports-color @@ -6300,8 +6237,8 @@ snapshots: dependencies: '@babel/core': 7.28.4 '@babel/helper-module-imports': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 - '@babel/traverse': 7.28.4 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color @@ -6316,7 +6253,7 @@ snapshots: '@babel/core': 7.28.4 '@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-wrap-function': 7.28.3 - '@babel/traverse': 7.28.4 + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color @@ -6325,21 +6262,19 @@ snapshots: '@babel/core': 7.28.4 '@babel/helper-member-expression-to-functions': 7.28.5 '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/traverse': 7.28.4 + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color '@babel/helper-skip-transparent-expression-wrappers@7.27.1': dependencies: - '@babel/traverse': 7.28.4 + '@babel/traverse': 7.28.5 '@babel/types': 7.28.5 transitivePeerDependencies: - supports-color '@babel/helper-string-parser@7.27.1': {} - '@babel/helper-validator-identifier@7.27.1': {} - '@babel/helper-validator-identifier@7.28.5': {} '@babel/helper-validator-option@7.27.1': {} @@ -6347,7 +6282,7 @@ snapshots: '@babel/helper-wrap-function@7.28.3': dependencies: '@babel/template': 7.27.2 - '@babel/traverse': 7.28.4 + '@babel/traverse': 7.28.5 '@babel/types': 7.28.5 transitivePeerDependencies: - supports-color @@ -6355,19 +6290,15 @@ snapshots: '@babel/helpers@7.28.4': dependencies: '@babel/template': 7.27.2 - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 '@babel/highlight@7.25.9': dependencies: - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 chalk: 2.4.2 js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/parser@7.28.4': - dependencies: - '@babel/types': 7.28.4 - '@babel/parser@7.28.5': dependencies: '@babel/types': 7.28.5 @@ -6551,7 +6482,7 @@ snapshots: '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.4) - '@babel/traverse': 7.28.4 + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color @@ -6598,7 +6529,7 @@ snapshots: '@babel/helper-globals': 7.28.0 '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.4) - '@babel/traverse': 7.28.4 + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color @@ -6675,7 +6606,7 @@ snapshots: '@babel/core': 7.28.4 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-plugin-utils': 7.27.1 - '@babel/traverse': 7.28.4 + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color @@ -6761,7 +6692,7 @@ snapshots: '@babel/helper-plugin-utils': 7.27.1 '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.28.4) '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.28.4) - '@babel/traverse': 7.28.4 + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color @@ -6842,7 +6773,7 @@ snapshots: '@babel/helper-module-imports': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.4) - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 transitivePeerDependencies: - supports-color @@ -7058,20 +6989,8 @@ snapshots: '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 - - '@babel/traverse@7.28.4': - dependencies: - '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.3 - '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.28.4 - '@babel/template': 7.27.2 - '@babel/types': 7.28.4 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 '@babel/traverse@7.28.5': dependencies: @@ -7085,11 +7004,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/types@7.28.4': - dependencies: - '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 - '@babel/types@7.28.5': dependencies: '@babel/helper-string-parser': 7.27.1 @@ -7575,7 +7489,7 @@ snapshots: dependencies: '@babel/code-frame': 7.27.1 '@babel/core': 7.28.4 - '@babel/generator': 7.28.3 + '@babel/generator': 7.28.5 '@expo/config': 12.0.10 '@expo/env': 2.0.7 '@expo/json-file': 10.0.7 @@ -7980,7 +7894,7 @@ snapshots: '@react-native/babel-plugin-codegen@0.81.5(@babel/core@7.28.4)': dependencies: - '@babel/traverse': 7.28.4 + '@babel/traverse': 7.28.5 '@react-native/codegen': 0.81.5(@babel/core@7.28.4) transitivePeerDependencies: - '@babel/core' @@ -8049,7 +7963,7 @@ snapshots: '@react-native/codegen@0.82.1(@babel/core@7.28.4)': dependencies: '@babel/core': 7.28.4 - '@babel/parser': 7.28.4 + '@babel/parser': 7.28.5 glob: 7.2.3 hermes-parser: 0.32.0 invariant: 2.2.4 @@ -8333,24 +8247,24 @@ snapshots: '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 '@types/babel__generator': 7.27.0 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.28.0 '@types/babel__generator@7.27.0': dependencies: - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 '@types/babel__traverse@7.28.0': dependencies: - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 '@types/chai@5.2.3': dependencies: @@ -8447,6 +8361,8 @@ snapshots: dependencies: csstype: 3.1.3 + '@types/stack-utils@2.0.3': {} + '@types/unist@2.0.11': {} '@types/unist@3.0.3': {} @@ -8704,14 +8620,6 @@ snapshots: clean-stack: 2.2.0 indent-string: 4.0.0 - ai@5.0.81(zod@4.1.12): - dependencies: - '@ai-sdk/gateway': 2.0.2(zod@4.1.12) - '@ai-sdk/provider': 2.0.0 - '@ai-sdk/provider-utils': 3.0.13(zod@4.1.12) - '@opentelemetry/api': 1.9.0 - zod: 4.1.12 - ai@5.0.86(zod@4.1.12): dependencies: '@ai-sdk/gateway': 2.0.5(zod@4.1.12) @@ -8911,7 +8819,7 @@ snapshots: babel-plugin-polyfill-corejs2@0.4.14(@babel/core@7.28.4): dependencies: - '@babel/compat-data': 7.28.4 + '@babel/compat-data': 7.28.5 '@babel/core': 7.28.4 '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.4) semver: 6.3.1 @@ -8935,7 +8843,7 @@ snapshots: babel-plugin-react-compiler@1.0.0: dependencies: - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 babel-plugin-react-native-web@0.21.2: {} @@ -9182,6 +9090,8 @@ snapshots: clone@1.0.4: {} + clsx@2.1.1: {} + color-convert@1.9.3: dependencies: color-name: 1.1.3 @@ -11181,9 +11091,9 @@ snapshots: metro-source-map@0.83.2: dependencies: - '@babel/traverse': 7.28.4 + '@babel/traverse': 7.28.5 '@babel/traverse--for-generate-function-map': '@babel/traverse@7.28.5' - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 flow-enums-runtime: 0.0.6 invariant: 2.2.4 metro-symbolicate: 0.83.2 @@ -11196,9 +11106,9 @@ snapshots: metro-source-map@0.83.3: dependencies: - '@babel/traverse': 7.28.4 + '@babel/traverse': 7.28.5 '@babel/traverse--for-generate-function-map': '@babel/traverse@7.28.5' - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 flow-enums-runtime: 0.0.6 invariant: 2.2.4 metro-symbolicate: 0.83.3 @@ -11234,9 +11144,9 @@ snapshots: metro-transform-plugins@0.83.2: dependencies: '@babel/core': 7.28.4 - '@babel/generator': 7.28.3 + '@babel/generator': 7.28.5 '@babel/template': 7.27.2 - '@babel/traverse': 7.28.4 + '@babel/traverse': 7.28.5 flow-enums-runtime: 0.0.6 nullthrows: 1.1.1 transitivePeerDependencies: @@ -11247,7 +11157,7 @@ snapshots: '@babel/core': 7.28.4 '@babel/generator': 7.28.5 '@babel/template': 7.27.2 - '@babel/traverse': 7.28.4 + '@babel/traverse': 7.28.5 flow-enums-runtime: 0.0.6 nullthrows: 1.1.1 transitivePeerDependencies: @@ -11256,9 +11166,9 @@ snapshots: metro-transform-worker@0.83.2: dependencies: '@babel/core': 7.28.4 - '@babel/generator': 7.28.3 - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 + '@babel/generator': 7.28.5 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 flow-enums-runtime: 0.0.6 metro: 0.83.2 metro-babel-transformer: 0.83.2 @@ -11297,11 +11207,11 @@ snapshots: dependencies: '@babel/code-frame': 7.27.1 '@babel/core': 7.28.4 - '@babel/generator': 7.28.3 - '@babel/parser': 7.28.4 + '@babel/generator': 7.28.5 + '@babel/parser': 7.28.5 '@babel/template': 7.27.2 - '@babel/traverse': 7.28.4 - '@babel/types': 7.28.4 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 accepts: 1.3.8 chalk: 4.1.2 ci-info: 2.0.0 @@ -11344,11 +11254,11 @@ snapshots: dependencies: '@babel/code-frame': 7.27.1 '@babel/core': 7.28.4 - '@babel/generator': 7.28.3 - '@babel/parser': 7.28.4 + '@babel/generator': 7.28.5 + '@babel/parser': 7.28.5 '@babel/template': 7.27.2 - '@babel/traverse': 7.28.4 - '@babel/types': 7.28.4 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 accepts: 1.3.8 chalk: 4.1.2 ci-info: 2.0.0 @@ -12033,6 +11943,8 @@ snapshots: react-is@16.13.1: {} + react-is@18.3.1: {} + react-markdown@10.1.0(@types/react@18.3.26)(react@19.2.0): dependencies: '@types/hast': 3.0.4 From 218652f7d256e94595823825172f4b32f2a2c162 Mon Sep 17 00:00:00 2001 From: Anton Arnautov Date: Tue, 18 Nov 2025 17:19:41 +0100 Subject: [PATCH 4/4] Add support for charts --- packages/react-sdk/package.json | 8 +- .../src/components/ErrorBoundary.tsx | 27 +++++++ .../react-sdk/src/components/ai-markdown.tsx | 61 +++++++++------ .../src/components/tools/charts/charts.tsx | 78 +++++++++++++++++++ .../src/components/tools/charts/suspended.tsx | 14 ++++ pnpm-lock.yaml | 33 ++++++++ 6 files changed, 197 insertions(+), 24 deletions(-) create mode 100644 packages/react-sdk/src/components/ErrorBoundary.tsx create mode 100644 packages/react-sdk/src/components/tools/charts/charts.tsx create mode 100644 packages/react-sdk/src/components/tools/charts/suspended.tsx diff --git a/packages/react-sdk/package.json b/packages/react-sdk/package.json index f8ef9f7..cb07a81 100644 --- a/packages/react-sdk/package.json +++ b/packages/react-sdk/package.json @@ -29,7 +29,8 @@ "!src/*.test.ts" ], "scripts": { - "build": "rimraf ./dist && concurrently 'vite build' 'tsc -p ./tsconfig.lib.json'", + "build": "rimraf ./dist && concurrently 'vite build' 'tsc -p ./tsconfig.lib.json' 'pnpm run build:styles'", + "build:styles": "exit 0", "dev": "concurrently 'vite build --watch' 'tsc -p ./tsconfig.lib.json' --watch", "prepare": "pnpm run build", "test": "" @@ -58,10 +59,13 @@ "concurrently": "catalog:", "rimraf": "^6.0.1", "typescript": "catalog:", - "vite": "catalog:" + "vite": "catalog:", + "vitest": "catalog:" }, "dependencies": { + "chart.js": "^4.5.1", "clsx": "^2.1.1", + "react-chartjs-2": "^5.3.1", "react-markdown": "^10.1.0", "react-syntax-highlighter": "15.5.0", "remark-gfm": "^4.0.1" diff --git a/packages/react-sdk/src/components/ErrorBoundary.tsx b/packages/react-sdk/src/components/ErrorBoundary.tsx new file mode 100644 index 0000000..82f31c5 --- /dev/null +++ b/packages/react-sdk/src/components/ErrorBoundary.tsx @@ -0,0 +1,27 @@ +import { Component, type PropsWithChildren, type ReactNode } from 'react'; + +type ErrorBoundaryProps = PropsWithChildren<{ fallback: ReactNode }>; + +export default class ErrorBoundary extends Component< + ErrorBoundaryProps, + { hasError: boolean } +> { + constructor(props: ErrorBoundaryProps) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(error: unknown) { + // Update state so the next render will show the fallback UI. + return { hasError: true }; + } + + override render() { + if (this.state.hasError) { + // You can render any custom fallback UI + return this.props.fallback; + } + + return this.props.children; + } +} diff --git a/packages/react-sdk/src/components/ai-markdown.tsx b/packages/react-sdk/src/components/ai-markdown.tsx index d648e53..7dd80e4 100644 --- a/packages/react-sdk/src/components/ai-markdown.tsx +++ b/packages/react-sdk/src/components/ai-markdown.tsx @@ -15,6 +15,8 @@ import remarkGfm from 'remark-gfm'; import { Prism, type SyntaxHighlighterProps } from 'react-syntax-highlighter'; import clsx from 'clsx'; +import { SuspendedChart } from './tools/charts/suspended'; + const SyntaxHighlighter = Prism as unknown as ComponentType; @@ -22,7 +24,12 @@ const getToolOrLanguage = (className: string = '') => { return className.match(/language-(?[\w-]+)/)?.groups?.['tool']; }; -type ToolComponents = Record>; +type ToolComponents = { + [key in string]?: ComponentType<{ + data: string; + }>; +}; + type MarkdownComponents = Components; const AIMarkdownContext = React.createContext<{ @@ -36,35 +43,45 @@ type DefaultPreProps = BaseDefaultPreProps & { }; const DefaultPre = (props: DefaultPreProps) => { - const { children, Pre = 'pre', ...restProps } = props; + const { children, className, Pre = 'pre', ...restProps } = props; const { toolComponents } = useContext(AIMarkdownContext); const [codeElement] = Children.toArray(children); if ( - !isValidElement(codeElement) || - codeElement.props.node.tagName !== 'code' + isValidElement(codeElement) && + codeElement.props.node.tagName === 'code' ) { - return
    {children}
    ; - } - - const tool = getToolOrLanguage(codeElement.props.className); - - // grab from pre-registered component set and render - const Component = typeof tool === 'string' ? toolComponents[tool] : null; - if (Component) { - return ; - } - - // render just a fragment with the code content - // which gets replaced by SyntaxHighlighter (it itself renders pre too) - if (tool) { - return <>{children}; + const toolOrLanguage = getToolOrLanguage(codeElement.props.className); + + // grab from pre-registered component set and render + const Component = + typeof toolOrLanguage === 'string' + ? toolComponents[toolOrLanguage] + : null; + + console.log(codeElement.props.node); + + if (Component) { + // TODO: forward metadata + // TODO: allow fallthrough if render fails/errors-out (display raw code block instead) + return ; + } + + // render just a fragment with the code content + // which gets replaced by SyntaxHighlighter (it itself renders pre too) + if (toolOrLanguage) { + return <>{children}; + } } // treat as regular pre/code block if there's no tool/language - return
    {children}
    ; + return ( +
    +      {children}
    +    
    + ); }; const DefaultSyntaxHighlighter = ({ @@ -113,7 +130,7 @@ const DefaultCode = (props: DefaultCodeProps) => { return ( { const mergedToolComponents: ToolComponents = useMemo( () => ({ + chartjs: SuspendedChart, ...props.toolComponents, - // ...DefaultTools, }), [props.toolComponents], ); diff --git a/packages/react-sdk/src/components/tools/charts/charts.tsx b/packages/react-sdk/src/components/tools/charts/charts.tsx new file mode 100644 index 0000000..b0761fd --- /dev/null +++ b/packages/react-sdk/src/components/tools/charts/charts.tsx @@ -0,0 +1,78 @@ +import { useMemo, type ComponentProps } from 'react'; +import { + Chart as ChartJS, + ArcElement, + Tooltip, + Legend, + CategoryScale, + LinearScale, + BarElement, + LineElement, + PointElement, + Title as TitlePlugin, +} from 'chart.js'; +import { + Pie, + Bar, + Line, + Bubble, + Doughnut, + PolarArea, + Radar, + Scatter, +} from 'react-chartjs-2'; + +ChartJS.register( + ArcElement, + Tooltip, + Legend, + CategoryScale, + LinearScale, + BarElement, + LineElement, + PointElement, + TitlePlugin, +); + +const components = { + pie: Pie, + bar: Bar, + line: Line, + bubble: Bubble, + doughnut: Doughnut, + polarArea: PolarArea, + radar: Radar, + scatter: Scatter, + unknown: () =>
    Unknown chart type
    , +} as const; + +const Chart = ({ data }: { data: string }) => { + const parsedData = useMemo(() => { + try { + return JSON.parse(data); + } catch (error) { + return new Error('Invalid JSON data for Chart.js'); + } + }, [data]); + + if (parsedData instanceof Error) { + return
    {parsedData.message}
    ; + } + + const Component = + components[parsedData.type as keyof typeof components] ?? + components.unknown; + + return ( +
    + +
    + ); +}; + +export type ChartProps = ComponentProps; + +export default Chart; + +// Context for usage: +// the application rendering these messages is capable of rendering charts using Chart.js, to render a chart return a markdown code block with language "chartjs" which contains JSON of Chart.js compatible data diff --git a/packages/react-sdk/src/components/tools/charts/suspended.tsx b/packages/react-sdk/src/components/tools/charts/suspended.tsx new file mode 100644 index 0000000..d148f30 --- /dev/null +++ b/packages/react-sdk/src/components/tools/charts/suspended.tsx @@ -0,0 +1,14 @@ +import { lazy, Suspense } from 'react'; +import ErrorBoundary from '../../ErrorBoundary'; + +const Chart = lazy(() => import('./charts')); + +export const SuspendedChart = (props: { data: string }) => { + return ( + Failed to load chart.
    }> + Loading chart...
    }> + + + + ); +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 55b56e4..8d69a8b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -264,12 +264,18 @@ importers: packages/react-sdk: dependencies: + chart.js: + specifier: ^4.5.1 + version: 4.5.1 clsx: specifier: ^2.1.1 version: 2.1.1 react: specifier: ^17 || ^18 || ^19 version: 19.2.0 + react-chartjs-2: + specifier: ^5.3.1 + version: 5.3.1(chart.js@4.5.1)(react@19.2.0) react-dom: specifier: ^17 || ^18 || ^19 version: 19.2.0(react@19.2.0) @@ -307,6 +313,9 @@ importers: vite: specifier: 'catalog:' version: 7.1.11(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1) + vitest: + specifier: 'catalog:' + version: 4.0.6(@types/debug@4.1.12)(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1) packages: @@ -1678,6 +1687,9 @@ packages: react: ^18.2.0 react-dom: ^18.2.0 + '@kurkle/color@0.3.4': + resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==} + '@manypkg/find-root@1.1.0': resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} @@ -2802,6 +2814,10 @@ packages: chardet@2.1.0: resolution: {integrity: sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==} + chart.js@4.5.1: + resolution: {integrity: sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==} + engines: {pnpm: '>=8'} + chownr@3.0.0: resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} engines: {node: '>=18'} @@ -5038,6 +5054,12 @@ packages: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true + react-chartjs-2@5.3.1: + resolution: {integrity: sha512-h5IPXKg9EXpjoBzUfyWJvllMjG2mQ4EiuHQFhms/AjUm0XSZHhyRy2xVmLXHKrtcdrPO4mnGqRtYoD0vp95A0A==} + peerDependencies: + chart.js: ^4.1.1 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-devtools-core@6.1.5: resolution: {integrity: sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==} @@ -7818,6 +7840,8 @@ snapshots: react: 19.1.1 react-dom: 19.2.0(react@19.1.1) + '@kurkle/color@0.3.4': {} + '@manypkg/find-root@1.1.0': dependencies: '@babel/runtime': 7.28.4 @@ -9046,6 +9070,10 @@ snapshots: chardet@2.1.0: {} + chart.js@4.5.1: + dependencies: + '@kurkle/color': 0.3.4 + chownr@3.0.0: {} chrome-launcher@0.15.2: @@ -11923,6 +11951,11 @@ snapshots: minimist: 1.2.8 strip-json-comments: 2.0.1 + react-chartjs-2@5.3.1(chart.js@4.5.1)(react@19.2.0): + dependencies: + chart.js: 4.5.1 + react: 19.2.0 + react-devtools-core@6.1.5: dependencies: shell-quote: 1.8.3