From b4f87080ba16b86e4b5f425d59edee16cf5dcc40 Mon Sep 17 00:00:00 2001 From: Simon Farshid Date: Wed, 9 Oct 2024 12:44:06 -0700 Subject: [PATCH] feat: Trieve integration (#942) --- examples/with-trieve/.env.example | 3 + examples/with-trieve/.eslintrc.json | 3 + examples/with-trieve/.gitignore | 1 + examples/with-trieve/README.md | 2 + examples/with-trieve/app/globals.css | 76 +++++ examples/with-trieve/app/layout.tsx | 18 + examples/with-trieve/app/page.tsx | 79 +++++ examples/with-trieve/components.json | 17 + examples/with-trieve/lib/utils.ts | 6 + examples/with-trieve/next-env.d.ts | 5 + examples/with-trieve/next.config.mjs | 4 + examples/with-trieve/package.json | 34 ++ examples/with-trieve/postcss.config.mjs | 8 + examples/with-trieve/tailwind.config.ts | 87 +++++ examples/with-trieve/tsconfig.json | 22 ++ packages/react-trieve/.eslintrc.json | 3 + packages/react-trieve/README.md | 3 + packages/react-trieve/package.json | 97 ++++++ packages/react-trieve/postcss.config.mjs | 10 + packages/react-trieve/scripts/build.mts | 39 +++ packages/react-trieve/src/index.ts | 16 + .../src/runtime/TrieveCitation.tsx | 49 +++ .../runtime/TrieveCitationHoverContent.tsx | 122 +++++++ .../src/runtime/TrieveMarkdown.tsx | 89 +++++ .../src/runtime/useTrieveRuntime.tsx | 319 ++++++++++++++++++ .../src/styles/tailwindcss/trieve.css | 19 ++ .../react-trieve/src/tailwindcss/index.ts | 9 + .../react-trieve/src/trieve/TrieveMessage.tsx | 10 + .../react-trieve/src/trieve/trieveStream.ts | 107 ++++++ packages/react-trieve/src/ui/hover-card.tsx | 25 ++ .../react-trieve/src/ui/trieve-composer.tsx | 53 +++ .../src/ui/trieve-thread-welcome.tsx | 51 +++ packages/react-trieve/tailwind.config.ts | 58 ++++ packages/react-trieve/tsconfig.json | 11 + .../react/src/styles/tailwindcss/thread.css | 2 +- packages/react/src/ui/thread-config.tsx | 29 +- packages/react/src/ui/thread.tsx | 10 +- pnpm-lock.yaml | 221 ++++++++++++ 38 files changed, 1700 insertions(+), 17 deletions(-) create mode 100644 examples/with-trieve/.env.example create mode 100644 examples/with-trieve/.eslintrc.json create mode 100644 examples/with-trieve/.gitignore create mode 100644 examples/with-trieve/README.md create mode 100644 examples/with-trieve/app/globals.css create mode 100644 examples/with-trieve/app/layout.tsx create mode 100644 examples/with-trieve/app/page.tsx create mode 100644 examples/with-trieve/components.json create mode 100644 examples/with-trieve/lib/utils.ts create mode 100644 examples/with-trieve/next-env.d.ts create mode 100644 examples/with-trieve/next.config.mjs create mode 100644 examples/with-trieve/package.json create mode 100644 examples/with-trieve/postcss.config.mjs create mode 100644 examples/with-trieve/tailwind.config.ts create mode 100644 examples/with-trieve/tsconfig.json create mode 100644 packages/react-trieve/.eslintrc.json create mode 100644 packages/react-trieve/README.md create mode 100644 packages/react-trieve/package.json create mode 100644 packages/react-trieve/postcss.config.mjs create mode 100644 packages/react-trieve/scripts/build.mts create mode 100644 packages/react-trieve/src/index.ts create mode 100644 packages/react-trieve/src/runtime/TrieveCitation.tsx create mode 100644 packages/react-trieve/src/runtime/TrieveCitationHoverContent.tsx create mode 100644 packages/react-trieve/src/runtime/TrieveMarkdown.tsx create mode 100644 packages/react-trieve/src/runtime/useTrieveRuntime.tsx create mode 100644 packages/react-trieve/src/styles/tailwindcss/trieve.css create mode 100644 packages/react-trieve/src/tailwindcss/index.ts create mode 100644 packages/react-trieve/src/trieve/TrieveMessage.tsx create mode 100644 packages/react-trieve/src/trieve/trieveStream.ts create mode 100644 packages/react-trieve/src/ui/hover-card.tsx create mode 100644 packages/react-trieve/src/ui/trieve-composer.tsx create mode 100644 packages/react-trieve/src/ui/trieve-thread-welcome.tsx create mode 100644 packages/react-trieve/tailwind.config.ts create mode 100644 packages/react-trieve/tsconfig.json diff --git a/examples/with-trieve/.env.example b/examples/with-trieve/.env.example new file mode 100644 index 000000000..17c7c8af3 --- /dev/null +++ b/examples/with-trieve/.env.example @@ -0,0 +1,3 @@ +NEXT_PUBLIC_TRIEVE_API_URL= +NEXT_PUBLIC_TRIEVE_API_KEY= +NEXT_PUBLIC_TRIEVE_DATASET_ID= diff --git a/examples/with-trieve/.eslintrc.json b/examples/with-trieve/.eslintrc.json new file mode 100644 index 000000000..bffb357a7 --- /dev/null +++ b/examples/with-trieve/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/examples/with-trieve/.gitignore b/examples/with-trieve/.gitignore new file mode 100644 index 000000000..e985853ed --- /dev/null +++ b/examples/with-trieve/.gitignore @@ -0,0 +1 @@ +.vercel diff --git a/examples/with-trieve/README.md b/examples/with-trieve/README.md new file mode 100644 index 000000000..791c60c23 --- /dev/null +++ b/examples/with-trieve/README.md @@ -0,0 +1,2 @@ +# Trieve Example + diff --git a/examples/with-trieve/app/globals.css b/examples/with-trieve/app/globals.css new file mode 100644 index 000000000..0f0ff2aa2 --- /dev/null +++ b/examples/with-trieve/app/globals.css @@ -0,0 +1,76 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 240 10% 3.9%; + + --card: 0 0% 100%; + --card-foreground: 240 10% 3.9%; + + --popover: 0 0% 100%; + --popover-foreground: 240 10% 3.9%; + + --primary: 240 5.9% 10%; + --primary-foreground: 0 0% 98%; + + --secondary: 240 4.8% 95.9%; + --secondary-foreground: 240 5.9% 10%; + + --muted: 240 4.8% 95.9%; + --muted-foreground: 240 3.8% 46.1%; + + --accent: 240 4.8% 95.9%; + --accent-foreground: 240 5.9% 10%; + + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + + --border: 240 5.9% 90%; + --input: 240 5.9% 90%; + --ring: 240 10% 3.9%; + + --radius: 0.5rem; + } + + .dark { + --background: 240 10% 3.9%; + --foreground: 0 0% 98%; + + --card: 240 10% 3.9%; + --card-foreground: 0 0% 98%; + + --popover: 240 10% 3.9%; + --popover-foreground: 0 0% 98%; + + --primary: 0 0% 98%; + --primary-foreground: 240 5.9% 10%; + + --secondary: 240 3.7% 15.9%; + --secondary-foreground: 0 0% 98%; + + --muted: 240 3.7% 15.9%; + --muted-foreground: 240 5% 64.9%; + + --accent: 240 3.7% 15.9%; + --accent-foreground: 0 0% 98%; + + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + + --border: 240 3.7% 15.9%; + --input: 240 3.7% 15.9%; + --ring: 240 4.9% 83.9%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/examples/with-trieve/app/layout.tsx b/examples/with-trieve/app/layout.tsx new file mode 100644 index 000000000..5245d66a2 --- /dev/null +++ b/examples/with-trieve/app/layout.tsx @@ -0,0 +1,18 @@ +import "./globals.css"; + +import { cn } from "@/lib/utils"; +import { Montserrat } from "next/font/google"; + +const montserrat = Montserrat({ subsets: ["latin"] }); + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + {children} + + ); +} diff --git a/examples/with-trieve/app/page.tsx b/examples/with-trieve/app/page.tsx new file mode 100644 index 000000000..f52d9a7e4 --- /dev/null +++ b/examples/with-trieve/app/page.tsx @@ -0,0 +1,79 @@ +"use client"; + +import { AssistantRuntimeProvider, Thread } from "@assistant-ui/react"; +import { + makeTrieveMarkdownText, + TrieveComposer, + TrieveThreadWelcome, + useTrieveExtras, + useTrieveRuntime, +} from "@assistant-ui/react-trieve"; +import { TrieveSDK } from "trieve-ts-sdk"; + +const TrieveMarkdownText = makeTrieveMarkdownText(); + +const trieve = new TrieveSDK({ + baseUrl: process.env["NEXT_PUBLIC_TRIEVE_API_URL"]!, + apiKey: process.env["NEXT_PUBLIC_TRIEVE_API_KEY"]!, + datasetId: process.env["NEXT_PUBLIC_TRIEVE_DATASET_ID"]!, +}); + +const RuntimeProvider = () => { + const runtime = useTrieveRuntime({ + trieve, + ownerId: "abcd", + tags: [ + { + name: "Stories", + value: "story", + }, + { + name: "Comments", + value: "comment", + }, + { + name: "Ask HN", + value: "ask", + }, + { + name: "Show HN", + value: "show", + }, + { + name: "Jobs", + value: "job", + }, + { + name: "Polls", + value: "poll", + }, + ], + }); + + return ( + + + + ); +}; + +export default RuntimeProvider; + +function MyAssistant() { + const { title } = useTrieveExtras(); + + return ( +
+

{title}

+
+ +
+
+ ); +} diff --git a/examples/with-trieve/components.json b/examples/with-trieve/components.json new file mode 100644 index 000000000..0b78c3bf5 --- /dev/null +++ b/examples/with-trieve/components.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "app/globals.css", + "baseColor": "zinc", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils" + } +} diff --git a/examples/with-trieve/lib/utils.ts b/examples/with-trieve/lib/utils.ts new file mode 100644 index 000000000..365058ceb --- /dev/null +++ b/examples/with-trieve/lib/utils.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/examples/with-trieve/next-env.d.ts b/examples/with-trieve/next-env.d.ts new file mode 100644 index 000000000..40c3d6809 --- /dev/null +++ b/examples/with-trieve/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/examples/with-trieve/next.config.mjs b/examples/with-trieve/next.config.mjs new file mode 100644 index 000000000..4678774e6 --- /dev/null +++ b/examples/with-trieve/next.config.mjs @@ -0,0 +1,4 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = {}; + +export default nextConfig; diff --git a/examples/with-trieve/package.json b/examples/with-trieve/package.json new file mode 100644 index 000000000..a845dac41 --- /dev/null +++ b/examples/with-trieve/package.json @@ -0,0 +1,34 @@ +{ + "name": "with-trieve", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@assistant-ui/react": "workspace:*", + "@assistant-ui/react-markdown": "workspace:*", + "@assistant-ui/react-trieve": "workspace:*", + "clsx": "^2.1.1", + "next": "14.2.14", + "react": "^18", + "react-dom": "^18", + "tailwind-merge": "^2.5.2", + "tailwindcss-animate": "^1.0.7", + "trieve-ts-sdk": "^0.0.12" + }, + "devDependencies": { + "@assistant-ui/tsconfig": "workspace:*", + "@types/node": "^22", + "@types/react": "^18", + "@types/react-dom": "^18", + "eslint": "^8", + "eslint-config-next": "14.2.14", + "postcss": "^8", + "tailwindcss": "^3.4.13", + "typescript": "^5.6.2" + } +} diff --git a/examples/with-trieve/postcss.config.mjs b/examples/with-trieve/postcss.config.mjs new file mode 100644 index 000000000..1a69fd2a4 --- /dev/null +++ b/examples/with-trieve/postcss.config.mjs @@ -0,0 +1,8 @@ +/** @type {import('postcss-load-config').Config} */ +const config = { + plugins: { + tailwindcss: {}, + }, +}; + +export default config; diff --git a/examples/with-trieve/tailwind.config.ts b/examples/with-trieve/tailwind.config.ts new file mode 100644 index 000000000..2dfb9a708 --- /dev/null +++ b/examples/with-trieve/tailwind.config.ts @@ -0,0 +1,87 @@ +import type { Config } from "tailwindcss"; + +const config = { + darkMode: ["class"], + content: [ + "./pages/**/*.{ts,tsx}", + "./components/**/*.{ts,tsx}", + "./app/**/*.{ts,tsx}", + "./src/**/*.{ts,tsx}", + ], + prefix: "", + theme: { + container: { + center: true, + padding: { + xs: "2rem", + }, + screens: { + xs: "460px", + }, + }, + extend: { + colors: { + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", + }, + secondary: { + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))", + }, + destructive: { + DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))", + }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", + }, + accent: { + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", + }, + popover: { + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))", + }, + card: { + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))", + }, + }, + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", + }, + keyframes: { + "accordion-down": { + from: { height: "0" }, + to: { height: "var(--radix-accordion-content-height)" }, + }, + "accordion-up": { + from: { height: "var(--radix-accordion-content-height)" }, + to: { height: "0" }, + }, + }, + animation: { + "accordion-down": "accordion-down 0.2s ease-out", + "accordion-up": "accordion-up 0.2s ease-out", + }, + }, + }, + plugins: [ + require("tailwindcss-animate"), + require("@assistant-ui/react/tailwindcss")({ shadcn: true }), + require("@assistant-ui/react-markdown/tailwindcss"), + require("@assistant-ui/react-trieve/tailwindcss"), + ], +} satisfies Config; + +export default config; diff --git a/examples/with-trieve/tsconfig.json b/examples/with-trieve/tsconfig.json new file mode 100644 index 000000000..4945b6755 --- /dev/null +++ b/examples/with-trieve/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "@assistant-ui/tsconfig/base.json", + "compilerOptions": { + "target": "ES6", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"], + "@assistant-ui/*": ["../../packages/*/src"], + "@assistant-ui/react/*": ["../../packages/react/src/*"] + }, + "allowJs": true, + "strictNullChecks": true, + "jsx": "preserve" + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/packages/react-trieve/.eslintrc.json b/packages/react-trieve/.eslintrc.json new file mode 100644 index 000000000..bffb357a7 --- /dev/null +++ b/packages/react-trieve/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/packages/react-trieve/README.md b/packages/react-trieve/README.md new file mode 100644 index 000000000..f7992fca3 --- /dev/null +++ b/packages/react-trieve/README.md @@ -0,0 +1,3 @@ +# `@assistant-ui/react-trieve` + +Trieve integration for `@assistant-ui/react`. diff --git a/packages/react-trieve/package.json b/packages/react-trieve/package.json new file mode 100644 index 000000000..68d2cf54d --- /dev/null +++ b/packages/react-trieve/package.json @@ -0,0 +1,97 @@ +{ + "name": "@assistant-ui/react-trieve", + "version": "0.0.1", + "license": "MIT", + "exports": { + ".": { + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "./tailwindcss": { + "import": { + "types": "./dist/tailwindcss/index.d.mts", + "default": "./dist/tailwindcss/index.mjs" + }, + "require": { + "types": "./dist/tailwindcss/index.d.ts", + "default": "./dist/tailwindcss/index.js" + } + }, + "./styles/*": { + "default": "./dist/styles/*" + } + }, + "source": "./src/index.ts", + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "files": [ + "dist", + "README.md" + ], + "sideEffects": false, + "scripts": { + "build": "tsx scripts/build.mts" + }, + "dependencies": { + "@radix-ui/react-hover-card": "^1.1.2", + "@radix-ui/react-use-callback-ref": "^1.1.0", + "classnames": "^2.5.1", + "lucide-react": "^0.447.0", + "rehype-parse": "^9.0.1", + "rehype-react": "^8.0.0", + "rehype-sanitize": "^6.0.0", + "remark-gfm": "^4.0.0", + "trieve-ts-sdk": "^0.0.12", + "unified": "^11.0.5", + "unist-util-visit": "^5.0.0" + }, + "peerDependencies": { + "@assistant-ui/react": "^0.5.68", + "@assistant-ui/react-markdown": "^0.2.18", + "@types/react": "*", + "react": "^18", + "tailwindcss": "^3.4.4" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "tailwindcss": { + "optional": true + } + }, + "devDependencies": { + "@assistant-ui/tailwindcss-transformer": "workspace:*", + "@assistant-ui/tsconfig": "workspace:*", + "@types/hast": "^3.0.4", + "@types/mdast": "^4.0.4", + "@types/node": "^22.7.4", + "autoprefixer": "^10.4.20", + "eslint": "^8", + "eslint-config-next": "14.2.14", + "postcss": "^8.4.47", + "tailwindcss": "^3.4.13", + "tailwindcss-animate": "^1.0.7", + "tsup": "8.3.0", + "tsx": "^4.19.1" + }, + "publishConfig": { + "access": "public", + "provenance": true + }, + "homepage": "https://assistant-ui.com/", + "repository": { + "type": "git", + "url": "git+https://github.com/Yonom/assistant-ui.git" + }, + "bugs": { + "url": "https://github.com/Yonom/assistant-ui/issues" + } +} diff --git a/packages/react-trieve/postcss.config.mjs b/packages/react-trieve/postcss.config.mjs new file mode 100644 index 000000000..3e6a865dd --- /dev/null +++ b/packages/react-trieve/postcss.config.mjs @@ -0,0 +1,10 @@ +/** @type {import('postcss-load-config').Config} */ +const config = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + "@assistant-ui/tailwindcss-transformer": {}, + }, +}; + +export default config; diff --git a/packages/react-trieve/scripts/build.mts b/packages/react-trieve/scripts/build.mts new file mode 100644 index 000000000..7149cd221 --- /dev/null +++ b/packages/react-trieve/scripts/build.mts @@ -0,0 +1,39 @@ +import { build } from "tsup"; +import { copyFileSync, mkdirSync } from "node:fs"; + +// JS +await build({ + entry: ["src/index.ts"], + format: ["cjs", "esm"], + dts: true, + sourcemap: true, + clean: true, + esbuildOptions: (options, { format }) => { + if (format === "esm") { + options.banner = { + js: '"use client";', + }; + } + }, +}); + +await build({ + entry: ["src/tailwindcss/index.ts"], + outDir: "dist/tailwindcss", + format: ["cjs", "esm"], + dts: true, + sourcemap: true, +}); + +// css + +await build({ + entry: ["src/styles/tailwindcss/trieve.css"], + outDir: "dist/styles", +}); + +mkdirSync("dist/styles/tailwindcss", { recursive: true }); +copyFileSync( + "src/styles/tailwindcss/trieve.css", + "dist/styles/tailwindcss/trieve.css", +); diff --git a/packages/react-trieve/src/index.ts b/packages/react-trieve/src/index.ts new file mode 100644 index 000000000..7ed9d7353 --- /dev/null +++ b/packages/react-trieve/src/index.ts @@ -0,0 +1,16 @@ +export { + useTrieveRuntime, + useTrieveExtras, + type TrieveExtras, +} from "./runtime/useTrieveRuntime"; +export { makeTrieveMarkdownText } from "./runtime/TrieveMarkdown"; +export { default as TrieveComposer } from "./ui/trieve-composer"; +export { default as TrieveThreadWelcome } from "./ui/trieve-thread-welcome"; +export { + TrieveCitationHoverContent, + type CitationHoverContentProps, +} from "./runtime/TrieveCitationHoverContent"; +export { + TrieveCitation, + type TrieveCitationProps, +} from "./runtime/TrieveCitation"; diff --git a/packages/react-trieve/src/runtime/TrieveCitation.tsx b/packages/react-trieve/src/runtime/TrieveCitation.tsx new file mode 100644 index 000000000..4f02e95ae --- /dev/null +++ b/packages/react-trieve/src/runtime/TrieveCitation.tsx @@ -0,0 +1,49 @@ +import { getExternalStoreMessage, useMessage } from "@assistant-ui/react"; +import { ComponentType, FC } from "react"; +import { + HoverCard, + HoverCardContent, + HoverCardTrigger, +} from "../ui/hover-card"; +import { + CitationHoverContentProps, + TrieveCitationHoverContent, +} from "./TrieveCitationHoverContent"; +import { TrieveMessage } from "../trieve/TrieveMessage"; + +export type TrieveCitationProps = { + node?: any; + CitationHoverContent?: ComponentType | undefined; +}; + +export const TrieveCitation: FC = ({ + node, + CitationHoverContent = TrieveCitationHoverContent, + ...rest +}) => { + const assistantUiMessage = useMessage(); + + const indexString = node.children[0].children[0].value; + let index; + try { + index = parseInt(indexString.replace(/[^0-9]/g, ""), 10); + } catch { + return ; + } + + const message = getExternalStoreMessage(assistantUiMessage); + const citation = message?.citations?.[index]; + + if (citation === undefined) return ; + + return ( + + + + + + + + + ); +}; diff --git a/packages/react-trieve/src/runtime/TrieveCitationHoverContent.tsx b/packages/react-trieve/src/runtime/TrieveCitationHoverContent.tsx new file mode 100644 index 000000000..3bc151b71 --- /dev/null +++ b/packages/react-trieve/src/runtime/TrieveCitationHoverContent.tsx @@ -0,0 +1,122 @@ +import { Root as HastRoot } from "hast"; +import { createElement, FC, Fragment, useEffect, useState } from "react"; +import { unified } from "unified"; +import rehypeParse from "rehype-parse"; +import rehypeSanitize from "rehype-sanitize"; +import rehypeReact from "rehype-react"; +import * as prod from "react/jsx-runtime"; +import { visit } from "unist-util-visit"; +import { ChunkMetadata } from "trieve-ts-sdk"; +import { useTrieveExtras } from "./useTrieveRuntime"; + +function rehypeWrapFirstHeading( + options: { headerUrl?: string | undefined } = {}, +) { + const { headerUrl = "#" } = options; // Default to '#' if no URL is provided + + return (tree: HastRoot) => { + let firstHeadingFound = false; + + visit(tree, "element", (node, index, parent) => { + if (!parent || index === undefined) return; + if (!firstHeadingFound && /^h[1-6]$/.test(node.tagName)) { + firstHeadingFound = true; + + // Create a new link node with the specified headerUrl + const linkNode = { + type: "element", + tagName: "a", + properties: { + href: headerUrl, + }, + children: [Object.assign({}, node)], + }; + + // Replace the heading node with the link node + parent.children[index] = linkNode as any; + } + }); + + if (!firstHeadingFound) { + // If no heading was found, insert a "Source" link at the top of the document + const sourceHeadingNode = { + type: "element", + tagName: "h2", + children: [ + { + type: "element", + tagName: "a", + properties: { + href: headerUrl, + }, + children: [ + { + type: "text", + value: "Source", + }, + ], + }, + ], + }; + tree.children.unshift(sourceHeadingNode as any); + } + }; +} + +const TrieveLink = ({ + citation, + position, + ...props +}: CitationHoverContentProps) => { + const trackLinkClick = useTrieveExtras((t) => t.trackLinkClick); + return ( + { + trackLinkClick(citation, position); + }} + {...props} + /> + ); +}; + +export type CitationHoverContentProps = { + citation: ChunkMetadata; + position: number; +}; + +export const TrieveCitationHoverContent: FC = ({ + citation, + position, +}) => { + const [Content, setContent] = useState( + createElement(Fragment), + ); + + useEffect(() => { + (async function () { + if (!citation.chunk_html) return; + + const file = await unified() + .use(rehypeParse, { fragment: true }) + .use(rehypeSanitize) + .use(rehypeWrapFirstHeading, { headerUrl: citation.link ?? undefined }) + .use(rehypeReact, { + Fragment: prod.Fragment, + jsx: prod.jsx as any, + jsxs: prod.jsxs as any, + components: { + a: ({ node, ...rest }) => ( + + ), + }, + }) + .process(citation.chunk_html); + + setContent(file.result); + })(); + }, [citation, position]); + + return <>{Content}; +}; diff --git a/packages/react-trieve/src/runtime/TrieveMarkdown.tsx b/packages/react-trieve/src/runtime/TrieveMarkdown.tsx new file mode 100644 index 000000000..2bf83c324 --- /dev/null +++ b/packages/react-trieve/src/runtime/TrieveMarkdown.tsx @@ -0,0 +1,89 @@ +"use client"; + +import { + getExternalStoreMessage, + TextContentPartProvider, + useContentPartText, + useMessage, +} from "@assistant-ui/react"; +import { makeMarkdownText } from "@assistant-ui/react-markdown"; +import { visit, SKIP } from "unist-util-visit"; +import remarkGfm from "remark-gfm"; +import { TrieveMessage } from "../trieve/TrieveMessage"; +import { Root } from "mdast"; +import { MakeMarkdownTextProps } from "@assistant-ui/react-markdown"; +import { CitationHoverContentProps } from "./TrieveCitationHoverContent"; +import { ComponentType } from "react"; +import { TrieveCitation } from "./TrieveCitation"; + +function removeFootnoteDefinitions() { + return (tree: Root) => { + visit(tree, "footnoteDefinition", (_, index, parent) => { + if (parent && index !== undefined) { + parent.children.splice(index, 1); + return [SKIP, index]; + } + return undefined; + }); + }; +} + +function generateDummyCitations(citationCount: number) { + return Array.from( + { length: citationCount }, + (_, i) => `\n[^${i}]: dummy`, + ).join(""); +} + +export type TrieveMarkdownTextProps = Omit< + MakeMarkdownTextProps, + "components" +> & { + components?: NonNullable & { + CitationHoverContent?: ComponentType; + }; + smooth?: boolean; +}; + +export const makeTrieveMarkdownText = (options?: TrieveMarkdownTextProps) => { + const { CitationHoverContent, ...components } = options?.components ?? {}; + const MarkdownText = makeMarkdownText({ + ...options, + remarkPlugins: [ + remarkGfm, + removeFootnoteDefinitions, + { + plugins: options?.remarkPlugins ?? [], + }, + ], + + components: { + sup: (props) => ( + + ), + ...components, + }, + }); + + const MarkdownTextWithFootnotes = () => { + const message = useMessage(); + const citationCount = + getExternalStoreMessage(message)?.citations?.length ?? 0; + + const { text, status } = useContentPartText(); + const appendText = "\n\n" + generateDummyCitations(citationCount); + return ( + + + + ); + }; + + return MarkdownTextWithFootnotes; +}; diff --git a/packages/react-trieve/src/runtime/useTrieveRuntime.tsx b/packages/react-trieve/src/runtime/useTrieveRuntime.tsx new file mode 100644 index 000000000..21ea80d5a --- /dev/null +++ b/packages/react-trieve/src/runtime/useTrieveRuntime.tsx @@ -0,0 +1,319 @@ +"use client"; + +import { + AssistantRuntime, + ThreadSuggestion, + ThreadMessageLike, + ThreadRuntime, + ThreadState, + useExternalStoreRuntime, + useThread, +} from "@assistant-ui/react"; + +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { toTrieveStream, TrieveStreamPart } from "../trieve/trieveStream"; +import { ChunkMetadata, TrieveSDK } from "trieve-ts-sdk"; +import { TrieveMessage } from "../trieve/TrieveMessage"; +import { useCallbackRef } from "@radix-ui/react-use-callback-ref"; + +const symbolTrieveExtras = Symbol("trieve-extras"); + +export type TrieveExtras = { + [symbolTrieveExtras]: true; + title: string; + tags: TrieveTag[] | undefined; + selectedTag: string | undefined; + setSelectedTag: (tag: string | undefined) => void; + trackLinkClick: (citation: ChunkMetadata, position: number) => void; + refreshSuggestions: () => Promise; +}; + +const convertMessage = (message: TrieveMessage): ThreadMessageLike => { + return { + id: message.sort_order.toString(), + role: message.role === "user" ? "user" : "assistant", + content: message.content, + }; +}; + +// const fetchMessages = async ( +// trieve: TrieveSDK, +// messagesTopicId: string | undefined, +// ) => { +// if (!messagesTopicId) return []; +// return trieve.getAllMessagesForTopic({ +// messagesTopicId, +// }); +// }; + +const sliceMessagesUntil = ( + messages: TrieveMessage[], + messageId: string | null, +) => { + if (messageId == null) return []; + + const messageIdx = messages.findIndex( + (m) => m.sort_order.toString() === messageId, + ); + if (messageIdx === -1) + throw new Error(`Message with id ${messageId} not found`); + + return messages.slice(0, messageIdx + 1); +}; + +type AssistantRuntimeWithExtras = Omit & { + thread: Omit & { + getState(): ThreadState & { + extras: TExtras; + }; + }; +}; + +type TrieveTag = { + name: string; + value: string; +}; + +export const useTrieveRuntime = ({ + trieve, + ownerId, + tags, +}: { + trieve: TrieveSDK; + ownerId: string; + tags?: TrieveTag[]; +}) => { + const [title, setTitle] = useState(""); + const threadIdRef = useRef(); + const [isRunning, setIsRunning] = useState(false); + const [messages, setMessages] = useState([]); + const [suggestions, setSuggestions] = useState([]); + + const getFilter = () => { + return !selectedTag + ? null + : { + must: [ + { + field: "tag_set", + match_all: [selectedTag], + }, + ], + }; + }; + + const withRunning = useCallback(async (promise: Promise) => { + setSuggestions([]); + setIsRunning(true); + try { + return await promise; + } finally { + setIsRunning(false); + } + }, []); + + const fetchSuggestions = useCallbackRef(async (query: string | null) => { + const suggestions = ( + await trieve.suggestedQueries({ + query, + }) + ).queries; + setSuggestions(suggestions.slice(0, 3).map((d) => ({ prompt: d }))); + }); + + useEffect(() => { + // fetchMessages(trieve, undefined).then((data) => { + // setMessages(data); + // }); + fetchSuggestions(null); + }, [fetchSuggestions]); + + const handleStream = useCallback( + async (stream: AsyncIterable) => { + let assistantMessage = ""; + for await (const chunk of stream) { + if (chunk.type === "text-delta") { + assistantMessage += chunk.textDelta; + setMessages((prev) => [ + ...prev.slice(0, -1), + { + ...prev[prev.length - 1]!, + content: assistantMessage, + }, + ]); + } else if (chunk.type === "citations") { + setMessages((prev) => [ + ...prev.slice(0, -1), + { + ...prev[prev.length - 1]!, + citations: chunk.citations, + }, + ]); + } + } + }, + [], + ); + + const [selectedTag, setSelectedTag] = useState(); + + const trackLinkClick = useCallbackRef( + (citation: ChunkMetadata, position: number) => { + trieve.sendCTRAnalytics({ + ctr_type: "search", + position: position, + request_id: "", + clicked_chunk_id: citation.id, + clicked_chunk_tracking_id: citation.tracking_id ?? null, + }); + }, + ); + + const refreshSuggestions = useCallbackRef(async () => { + await fetchSuggestions(null); + }); + + const runtime = useExternalStoreRuntime({ + isRunning, + messages, + extras: useMemo( + () => + ({ + [symbolTrieveExtras]: true, + title, + tags, + selectedTag, + setSelectedTag, + trackLinkClick, + refreshSuggestions, + }) satisfies TrieveExtras, + [title, tags, selectedTag, trackLinkClick, refreshSuggestions], + ), + convertMessage, + onNew: async ({ content }) => { + const userMessage = content + .filter((m) => m.type === "text") + .map((m) => m.text) + .join("\n\n"); + + setSuggestions([]); + setMessages((prev) => [ + ...prev, + { + sort_order: prev.length + 1, + role: "user", + content: userMessage, + }, + { + sort_order: prev.length + 2, + role: "assistant", + content: "", + }, + ]); + + if (!threadIdRef.current) { + const topicResponse = await withRunning( + trieve.createTopic({ + owner_id: ownerId, + first_user_message: userMessage, + }), + ); + threadIdRef.current = topicResponse.id; + setTitle(topicResponse.name); + } + + await withRunning( + trieve + .createMessageReader({ + topic_id: threadIdRef.current, + new_message_content: userMessage, + filters: getFilter(), + }) + .then(toTrieveStream) + .then(handleStream), + ); + fetchSuggestions(userMessage); + }, + onEdit: async ({ content, parentId }) => { + const userMessage = content + .filter((m) => m.type === "text") + .map((m) => m.text) + .join("\n\n"); + + let message_sort_order = 0; + setSuggestions([]); + setMessages((prev) => { + prev = sliceMessagesUntil(prev, parentId); + message_sort_order = prev.length + 1; + return [ + ...prev, + { + sort_order: prev.length + 1, + role: "user", + content: userMessage, + }, + { + sort_order: prev.length + 2, + role: "assistant", + content: "", + }, + ]; + }); + + await withRunning( + trieve + .editMessageReader({ + topic_id: threadIdRef.current!, + message_sort_order, + new_message_content: userMessage, + filters: getFilter(), + }) + .then(toTrieveStream) + .then(handleStream), + ); + + fetchSuggestions(userMessage); + }, + + onReload: async () => { + setMessages((prev) => [ + ...prev.slice(0, -1), + { + ...prev[prev.length - 1]!, + content: "", + citations: undefined, + filters: getFilter(), + }, + ]); + + await withRunning( + trieve + .regenerateMessageReader({ + topic_id: threadIdRef.current!, + }) + .then(toTrieveStream) + .then(handleStream), + ); + }, + + suggestions, + }); + + return runtime as AssistantRuntimeWithExtras; +}; + +export function useTrieveExtras(): TrieveExtras; +export function useTrieveExtras(selector: (extras: TrieveExtras) => T): T; +export function useTrieveExtras( + selector: (extras: TrieveExtras) => T = (extras) => extras as T, +): T { + return useThread((t) => { + const extras = t.extras as TrieveExtras; + if (!extras[symbolTrieveExtras]) + throw new Error( + "useTrieveExtras can only be used inside a Trieve runtime", + ); + + return selector(extras); + }); +} diff --git a/packages/react-trieve/src/styles/tailwindcss/trieve.css b/packages/react-trieve/src/styles/tailwindcss/trieve.css new file mode 100644 index 000000000..e56f5614d --- /dev/null +++ b/packages/react-trieve/src/styles/tailwindcss/trieve.css @@ -0,0 +1,19 @@ +.aui-hover-card { + @apply bg-aui-popover text-aui-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 rounded-md border p-4 shadow-md outline-none; +} + +.aui-trieve-composer-tag-selector { + @apply bg-aui-muted z-[-10] -mt-2 flex w-full flex-row gap-3 rounded-b-lg border px-3 pt-2; +} + +.aui-trieve-composer-tag-selector-tag { + @apply text-aui-muted-foreground flex items-center px-1.5 py-2 text-sm font-medium; +} + +.aui-trieve-composer-tag-selector-tag[data-selected] { + @apply text-aui-foreground border-aui-foreground -mb-[1px] border-b; +} + +.aui-trieve-thread-welcome-refresh-suggestions { + @apply self-end; +} diff --git a/packages/react-trieve/src/tailwindcss/index.ts b/packages/react-trieve/src/tailwindcss/index.ts new file mode 100644 index 000000000..a18ae1121 --- /dev/null +++ b/packages/react-trieve/src/tailwindcss/index.ts @@ -0,0 +1,9 @@ +import plugin from "tailwindcss/plugin.js"; + +const auiPlugin = plugin.withOptions<{}>(() => ({ addComponents }) => { + addComponents({ + '@import "@assistant-ui/react-trieve/styles/tailwindcss/trieve.css"': "", + }); +}); + +export default auiPlugin; diff --git a/packages/react-trieve/src/trieve/TrieveMessage.tsx b/packages/react-trieve/src/trieve/TrieveMessage.tsx new file mode 100644 index 000000000..680e3a465 --- /dev/null +++ b/packages/react-trieve/src/trieve/TrieveMessage.tsx @@ -0,0 +1,10 @@ +"use client"; + +import { ChunkMetadata } from "trieve-ts-sdk"; + +export interface TrieveMessage { + sort_order: number; + role: string; + content: string; + citations?: ChunkMetadata[] | undefined; +} diff --git a/packages/react-trieve/src/trieve/trieveStream.ts b/packages/react-trieve/src/trieve/trieveStream.ts new file mode 100644 index 000000000..169737b89 --- /dev/null +++ b/packages/react-trieve/src/trieve/trieveStream.ts @@ -0,0 +1,107 @@ +import { ChunkMetadata } from "trieve-ts-sdk"; + +export type TrieveStreamPart = + | { + type: "text-delta"; + textDelta: string; + } + | { + type: "citations"; + citations: ChunkMetadata[]; + }; + +function readerToReadableStream( + reader: ReadableStreamDefaultReader, +) { + return new ReadableStream({ + async start(controller) { + try { + while (true) { + const { done, value } = await reader.read(); + if (done) { + break; + } + controller.enqueue(value); + } + } catch (error) { + controller.error(error); + } finally { + controller.close(); + reader.releaseLock(); + } + }, + cancel() { + reader.releaseLock(); + }, + }); +} + +function trieveStream() { + let citationsJsonText = ""; + let isHandlingCitations = true; + + return new TransformStream({ + transform(chunk, controller) { + if (!isHandlingCitations) { + return controller.enqueue({ + type: "text-delta", + textDelta: chunk, + }); + } + + const chunkParts = chunk.split("||"); + while (chunkParts.length > 0) { + const citationPart = chunkParts.shift(); + citationsJsonText += citationPart; + + if (chunkParts.length > 0) { + // we got a || marker, try to parse citation + try { + const citations = JSON.parse(citationsJsonText); + isHandlingCitations = false; + + controller.enqueue({ + type: "citations", + citations: citations, + }); + + controller.enqueue({ + type: "text-delta", + textDelta: chunkParts.join("||"), + }); + + chunkParts.length = 0; + } catch (e) { + // not a valid json + } + } + } + }, + }); +} + +export type AsyncIterableStream = AsyncIterable & ReadableStream; +export function makeAsyncIterable( + source: ReadableStream, +): AsyncIterableStream { + (source as AsyncIterableStream)[Symbol.asyncIterator] = () => { + const reader = source.getReader(); + return { + async next(): Promise> { + const { done, value } = await reader.read(); + return done ? { done: true, value: undefined } : { done: false, value }; + }, + }; + }; + + return source as AsyncIterableStream; +} + +export const toTrieveStream = ( + reader: ReadableStreamDefaultReader, +) => + makeAsyncIterable( + readerToReadableStream(reader) + .pipeThrough(new TextDecoderStream()) + .pipeThrough(trieveStream()), + ); diff --git a/packages/react-trieve/src/ui/hover-card.tsx b/packages/react-trieve/src/ui/hover-card.tsx new file mode 100644 index 000000000..37f02381a --- /dev/null +++ b/packages/react-trieve/src/ui/hover-card.tsx @@ -0,0 +1,25 @@ +"use client"; + +import * as React from "react"; +import * as HoverCardPrimitive from "@radix-ui/react-hover-card"; +import classNames from "classnames"; + +const HoverCard = HoverCardPrimitive.Root; + +const HoverCardTrigger = HoverCardPrimitive.Trigger; + +const HoverCardContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( + +)); +HoverCardContent.displayName = HoverCardPrimitive.Content.displayName; + +export { HoverCard, HoverCardTrigger, HoverCardContent }; diff --git a/packages/react-trieve/src/ui/trieve-composer.tsx b/packages/react-trieve/src/ui/trieve-composer.tsx new file mode 100644 index 000000000..29aaaf02c --- /dev/null +++ b/packages/react-trieve/src/ui/trieve-composer.tsx @@ -0,0 +1,53 @@ +import { Composer } from "@assistant-ui/react"; +import { FC } from "react"; +import { useTrieveExtras } from "../runtime/useTrieveRuntime"; + +const TrieveComposerTagSelector: FC = () => { + const { tags, selectedTag, setSelectedTag } = useTrieveExtras(); + if (!tags?.length) return null; + + return ( +
+ + {tags.map((tag) => ( + + ))} +
+ ); +}; + +const TrieveComposer: FC = () => { + return ( + <> + + + + + + + ); +}; + +const exports = { + TagSelector: TrieveComposerTagSelector, +}; + +export default Object.assign(TrieveComposer, exports) as typeof TrieveComposer & + typeof exports; diff --git a/packages/react-trieve/src/ui/trieve-thread-welcome.tsx b/packages/react-trieve/src/ui/trieve-thread-welcome.tsx new file mode 100644 index 000000000..c9cca64ac --- /dev/null +++ b/packages/react-trieve/src/ui/trieve-thread-welcome.tsx @@ -0,0 +1,51 @@ +import { ThreadWelcome, INTERNAL } from "@assistant-ui/react"; +import { RefreshCwIcon } from "lucide-react"; +import { FC, useState } from "react"; +import { useTrieveExtras } from "../runtime/useTrieveRuntime"; + +const { TooltipIconButton } = INTERNAL; + +const TrieveRefreshSuggestions = () => { + const [isRefreshing, setIsRefreshing] = useState(false); + const refreshSuggestions = useTrieveExtras((t) => t.refreshSuggestions); + return ( + { + setIsRefreshing(true); + try { + await refreshSuggestions(); + } finally { + setIsRefreshing(false); + } + }} + > + + + ); +}; + +const TrieveThreadWelcome: FC = () => { + return ( + + + + + + + + + ); +}; + +const exports = { + RefreshSuggestions: TrieveRefreshSuggestions, +}; + +export default Object.assign( + TrieveThreadWelcome, + exports, +) as typeof TrieveThreadWelcome & typeof exports; diff --git a/packages/react-trieve/tailwind.config.ts b/packages/react-trieve/tailwind.config.ts new file mode 100644 index 000000000..f3a045f9c --- /dev/null +++ b/packages/react-trieve/tailwind.config.ts @@ -0,0 +1,58 @@ +import type { Config } from "tailwindcss"; +import animatePlugin from "tailwindcss-animate"; + +const config = { + content: ["./src/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}"], + corePlugins: { + backgroundOpacity: false, + touchAction: false, + scrollSnapType: false, + gradientColorStops: false, + fontVariantNumeric: false, + ringOffsetWidth: false, + ringOffsetColor: false, + ringOpacity: false, + boxShadowColor: false, + blur: false, + brightness: false, + contrast: false, + grayscale: false, + hueRotate: false, + invert: false, + saturate: false, + sepia: false, + dropShadow: false, + backdropBlur: false, + backdropBrightness: false, + backdropContrast: false, + backdropGrayscale: false, + backdropHueRotate: false, + backdropInvert: false, + backdropOpacity: false, + backdropSaturate: false, + backdropSepia: false, + container: false, + }, + theme: { + container: { + center: true, + padding: "2rem", + screens: { + "2xl": "1400px", + }, + }, + extend: { + borderRadius: { + lg: "var(--aui-radius)", + md: "calc(var(--aui-radius) - 2px)", + sm: "calc(var(--aui-radius) - 4px)", + }, + }, + }, + plugins: [ + animatePlugin, + require("@assistant-ui/react/tailwindcss")({ components: [] }), + ], +} satisfies Config; + +export default config; diff --git a/packages/react-trieve/tsconfig.json b/packages/react-trieve/tsconfig.json new file mode 100644 index 000000000..2bbf4a09d --- /dev/null +++ b/packages/react-trieve/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "@assistant-ui/tsconfig/base.json", + "compilerOptions": { + "paths": { + "@assistant-ui/*": ["../../packages/*/src"], + "@assistant-ui/react/*": ["../../packages/react/src/*"] + } + }, + "include": ["**/*.ts", "**/*.tsx"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/react/src/styles/tailwindcss/thread.css b/packages/react/src/styles/tailwindcss/thread.css index 269a85412..dd377fc99 100644 --- a/packages/react/src/styles/tailwindcss/thread.css +++ b/packages/react/src/styles/tailwindcss/thread.css @@ -39,7 +39,7 @@ } .aui-thread-welcome-suggestion-container { - @apply mt-4 flex w-full items-stretch justify-center gap-4; + @apply mt-3 flex w-full items-stretch justify-center gap-4; } .aui-thread-welcome-suggestion { diff --git a/packages/react/src/ui/thread-config.tsx b/packages/react/src/ui/thread-config.tsx index ac0caccea..e7fb1eec8 100644 --- a/packages/react/src/ui/thread-config.tsx +++ b/packages/react/src/ui/thread-config.tsx @@ -17,7 +17,7 @@ import { AssistantToolUI } from "../model-config"; import { useAssistantRuntime } from "../context/react/AssistantContext"; export type SuggestionConfig = { - text?: ReactNode; + text?: ReactNode | undefined; prompt: string; }; @@ -148,25 +148,28 @@ export type StringsConfig = { const ThreadConfigContext = createContext({}); export type ThreadConfig = { - runtime?: AssistantRuntime; + runtime?: AssistantRuntime | undefined; - assistantAvatar?: AvatarProps; + assistantAvatar?: AvatarProps | undefined; - welcome?: ThreadWelcomeConfig; - assistantMessage?: AssistantMessageConfig; - userMessage?: UserMessageConfig; + welcome?: ThreadWelcomeConfig | undefined; + assistantMessage?: AssistantMessageConfig | undefined; + userMessage?: UserMessageConfig | undefined; - branchPicker?: BranchPickerConfig; + branchPicker?: BranchPickerConfig | undefined; - composer?: ComposerConfig; + composer?: ComposerConfig | undefined; - strings?: StringsConfig; + strings?: StringsConfig | undefined; - tools?: AssistantToolUI[]; // TODO add AssistantTool support + tools?: AssistantToolUI[] | undefined; // TODO add AssistantTool support - components?: { - Composer: ComponentType; - }; + components?: + | { + Composer?: ComponentType | undefined; + ThreadWelcome?: ComponentType | undefined; + } + | undefined; }; export const useThreadConfig = (): Omit => { diff --git a/packages/react/src/ui/thread.tsx b/packages/react/src/ui/thread.tsx index dbc30dcfa..b0a56fb5d 100644 --- a/packages/react/src/ui/thread.tsx +++ b/packages/react/src/ui/thread.tsx @@ -23,12 +23,16 @@ import { ThreadPrimitive, ThreadPrimitiveRootProps } from "../primitives"; import { useThread } from "../context"; const Thread: FC = (config) => { - const { components: { Composer: ComposerComponent = Composer } = {} } = - config; + const { + components: { + Composer: ComposerComponent = Composer, + ThreadWelcome: ThreadWelcomeComponent = ThreadWelcome, + } = {}, + } = config; return ( - + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 19f2942a8..2b090344b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -850,6 +850,67 @@ importers: specifier: ^5.6.2 version: 5.6.2 + examples/with-trieve: + dependencies: + '@assistant-ui/react': + specifier: workspace:* + version: link:../../packages/react + '@assistant-ui/react-markdown': + specifier: workspace:* + version: link:../../packages/react-markdown + '@assistant-ui/react-trieve': + specifier: workspace:* + version: link:../../packages/react-trieve + clsx: + specifier: ^2.1.1 + version: 2.1.1 + next: + specifier: 14.2.14 + version: 14.2.14(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: + specifier: ^18 + version: 18.3.1 + react-dom: + specifier: ^18 + version: 18.3.1(react@18.3.1) + tailwind-merge: + specifier: ^2.5.2 + version: 2.5.2 + tailwindcss-animate: + specifier: ^1.0.7 + version: 1.0.7(tailwindcss@3.4.13) + trieve-ts-sdk: + specifier: ^0.0.12 + version: 0.0.12 + devDependencies: + '@assistant-ui/tsconfig': + specifier: workspace:* + version: link:../../packages/tsconfig + '@types/node': + specifier: ^22 + version: 22.7.4 + '@types/react': + specifier: ^18 + version: 18.3.11 + '@types/react-dom': + specifier: ^18 + version: 18.3.0 + eslint: + specifier: ^8 + version: 8.57.1 + eslint-config-next: + specifier: 14.2.14 + version: 14.2.14(eslint@8.57.1)(typescript@5.6.2) + postcss: + specifier: ^8 + version: 8.4.47 + tailwindcss: + specifier: ^3.4.13 + version: 3.4.13 + typescript: + specifier: ^5.6.2 + version: 5.6.2 + examples/with-vercel-ai-rsc: dependencies: '@ai-sdk/openai': @@ -1391,6 +1452,94 @@ importers: specifier: 8.3.0 version: 8.3.0(jiti@1.21.6)(postcss@8.4.47)(tsx@4.19.1)(typescript@5.6.2)(yaml@2.5.1) + packages/react-trieve: + dependencies: + '@assistant-ui/react': + specifier: ^0.5.68 + version: link:../react + '@assistant-ui/react-markdown': + specifier: ^0.2.18 + version: link:../react-markdown + '@radix-ui/react-hover-card': + specifier: ^1.1.2 + version: 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': + specifier: ^1.1.0 + version: 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@types/react': + specifier: '*' + version: 18.3.11 + classnames: + specifier: ^2.5.1 + version: 2.5.1 + lucide-react: + specifier: ^0.447.0 + version: 0.447.0(react@18.3.1) + react: + specifier: ^18 + version: 18.3.1 + rehype-parse: + specifier: ^9.0.1 + version: 9.0.1 + rehype-react: + specifier: ^8.0.0 + version: 8.0.0 + rehype-sanitize: + specifier: ^6.0.0 + version: 6.0.0 + remark-gfm: + specifier: ^4.0.0 + version: 4.0.0 + trieve-ts-sdk: + specifier: ^0.0.12 + version: 0.0.12 + unified: + specifier: ^11.0.5 + version: 11.0.5 + unist-util-visit: + specifier: ^5.0.0 + version: 5.0.0 + devDependencies: + '@assistant-ui/tailwindcss-transformer': + specifier: workspace:* + version: link:../tailwindcss-transformer + '@assistant-ui/tsconfig': + specifier: workspace:* + version: link:../tsconfig + '@types/hast': + specifier: ^3.0.4 + version: 3.0.4 + '@types/mdast': + specifier: ^4.0.4 + version: 4.0.4 + '@types/node': + specifier: ^22.7.4 + version: 22.7.4 + autoprefixer: + specifier: ^10.4.20 + version: 10.4.20(postcss@8.4.47) + eslint: + specifier: ^8 + version: 8.57.1 + eslint-config-next: + specifier: 14.2.14 + version: 14.2.14(eslint@8.57.1)(typescript@5.6.2) + postcss: + specifier: ^8.4.47 + version: 8.4.47 + tailwindcss: + specifier: ^3.4.13 + version: 3.4.13 + tailwindcss-animate: + specifier: ^1.0.7 + version: 1.0.7(tailwindcss@3.4.13) + tsup: + specifier: 8.3.0 + version: 8.3.0(jiti@1.21.6)(postcss@8.4.47)(tsx@4.19.1)(typescript@5.6.2)(yaml@2.5.1) + tsx: + specifier: ^4.19.1 + version: 4.19.1 + packages/shadcn-registry: devDependencies: '@assistant-ui/react': @@ -2529,6 +2678,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-hover-card@1.1.2': + resolution: {integrity: sha512-Y5w0qGhysvmqsIy6nQxaPa6mXNKznfoGjOfBgzOjocLxr2XlSjqBMYQQL+FfyogsMuX+m8cZyQGYhJxvxUzO4w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-icons@1.3.0': resolution: {integrity: sha512-jQxj/0LKgp+j9BiTXz3O3sgs26RNet2iLWmsPyRz2SIcR4q/4SbazXfnYwbAr+vLYKSfc7qxzyGQA1HLlYiuNw==} peerDependencies: @@ -4526,6 +4688,9 @@ packages: hast-util-parse-selector@4.0.0: resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} + hast-util-sanitize@5.0.1: + resolution: {integrity: sha512-IGrgWLuip4O2nq5CugXy4GI2V8kx4sFVy5Hd4vF7AR2gxS0N9s7nEAVUyeMtZKZvzrxVsHt73XdTsno1tClIkQ==} + hast-util-to-estree@3.1.0: resolution: {integrity: sha512-lfX5g6hqVh9kjS/B9E2gSkvHH4SZNiQFiqWS0x9fENzEl+8W12RqdRxX6d/Cwxi30tPQs3bIO+aolQJNp1bIyw==} @@ -5887,6 +6052,15 @@ packages: rehype-katex@7.0.1: resolution: {integrity: sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==} + rehype-parse@9.0.1: + resolution: {integrity: sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==} + + rehype-react@8.0.0: + resolution: {integrity: sha512-vzo0YxYbB2HE+36+9HWXVdxNoNDubx63r5LBzpxBGVWM8s9mdnMdbmuJBAX6TTyuGdZjZix6qU3GcSuKCIWivw==} + + rehype-sanitize@6.0.0: + resolution: {integrity: sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==} + remark-gfm@4.0.0: resolution: {integrity: sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA==} @@ -6263,6 +6437,9 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true + trieve-ts-sdk@0.0.12: + resolution: {integrity: sha512-n2zSBvAW/AIOS5DMXsU2LK3D9y61v5SlwhIUfwDhnkXWhZn2F3waVaiWInL7TRpsvfYmI9XQEQxeH9b2wBYsAg==} + trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -7734,6 +7911,23 @@ snapshots: '@types/react': 18.3.11 '@types/react-dom': 18.3.0 + '@radix-ui/react-hover-card@1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-popper': 1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.11)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.11 + '@types/react-dom': 18.3.0 + '@radix-ui/react-icons@1.3.0(react@18.3.1)': dependencies: react: 18.3.1 @@ -10174,6 +10368,12 @@ snapshots: dependencies: '@types/hast': 3.0.4 + hast-util-sanitize@5.0.1: + dependencies: + '@types/hast': 3.0.4 + '@ungap/structured-clone': 1.2.0 + unist-util-position: 5.0.0 + hast-util-to-estree@3.1.0: dependencies: '@types/estree': 1.0.6 @@ -11783,6 +11983,25 @@ snapshots: unist-util-visit-parents: 6.0.1 vfile: 6.0.3 + rehype-parse@9.0.1: + dependencies: + '@types/hast': 3.0.4 + hast-util-from-html: 2.0.3 + unified: 11.0.5 + + rehype-react@8.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-to-jsx-runtime: 2.3.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + rehype-sanitize@6.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-sanitize: 5.0.1 + remark-gfm@4.0.0: dependencies: '@types/mdast': 4.0.4 @@ -12271,6 +12490,8 @@ snapshots: tree-kill@1.2.2: {} + trieve-ts-sdk@0.0.12: {} + trim-lines@3.0.1: {} trough@2.2.0: {}