[
  {
    "path": ".gitignore",
    "content": "# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore\n\n# Logs\n\nlogs\n_.log\nnpm-debug.log_\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n.pnpm-debug.log*\n\n# Caches\n\n.cache\n\n# Diagnostic reports (https://nodejs.org/api/report.html)\n\nreport.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json\n\n# Runtime data\n\npids\n_.pid\n_.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\n\nlib-cov\n\n# Coverage directory used by tools like istanbul\n\ncoverage\n*.lcov\n\n# nyc test coverage\n\n.nyc_output\n\n# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)\n\n.grunt\n\n# Bower dependency directory (https://bower.io/)\n\nbower_components\n\n# node-waf configuration\n\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\n\nbuild/Release\n\n# Dependency directories\n\nnode_modules/\njspm_packages/\n\n# Snowpack dependency directory (https://snowpack.dev/)\n\nweb_modules/\n\n# TypeScript cache\n\n*.tsbuildinfo\n\n# Optional npm cache directory\n\n.npm\n\n# Optional eslint cache\n\n.eslintcache\n\n# Optional stylelint cache\n\n.stylelintcache\n\n# Microbundle cache\n\n.rpt2_cache/\n.rts2_cache_cjs/\n.rts2_cache_es/\n.rts2_cache_umd/\n\n# Optional REPL history\n\n.node_repl_history\n\n# Output of 'npm pack'\n\n*.tgz\n\n# Yarn Integrity file\n\n.yarn-integrity\n\n# dotenv environment variable files\n\n.env\n.env.development.local\n.env.test.local\n.env.production.local\n.env.local\n\n# parcel-bundler cache (https://parceljs.org/)\n\n.parcel-cache\n\n# Next.js build output\n\n.next\nout\n\n# Nuxt.js build / generate output\n\n.nuxt\ndist\n\n# Gatsby files\n\n# Comment in the public line in if your project uses Gatsby and not Next.js\n\n# https://nextjs.org/blog/next-9-1#public-directory-support\n\n# public\n\n# vuepress build output\n\n.vuepress/dist\n\n# vuepress v2.x temp and cache directory\n\n.temp\n\n# Docusaurus cache and generated files\n\n.docusaurus\n\n# Serverless directories\n\n.serverless/\n\n# FuseBox cache\n\n.fusebox/\n\n# DynamoDB Local files\n\n.dynamodb/\n\n# TernJS port file\n\n.tern-port\n\n# Stores VSCode versions used for testing VSCode extensions\n\n.vscode-test\n\n# yarn v2\n\n.yarn/cache\n.yarn/unplugged\n.yarn/build-state.yml\n.yarn/install-state.gz\n.pnp.*\n\n# IntelliJ based IDEs\n.idea\n\n# Finder (MacOS) folder config\n.DS_Store\n"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"arrowParens\": \"always\",\n  \"bracketSameLine\": false,\n  \"bracketSpacing\": true,\n  \"semi\": true,\n  \"singleQuote\": false,\n  \"jsxSingleQuote\": false,\n  \"trailingComma\": \"all\",\n  \"singleAttributePerLine\": false,\n  \"importOrderSeparation\": true,\n  \"importOrderSortSpecifiers\": true,\n  \"importOrderBuiltinModulesToTop\": true,\n  \"tailwindFunctions\": [\"clsx\"],\n  \"plugins\": [\"prettier-plugin-tailwindcss\"]\n}\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2024 Upstash\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "README.md",
    "content": "# RAG Chat Component\n\nA customizable Reach chat component that combines Upstash Vector for similarity search, Together AI for LLM, and Vercel AI SDK for streaming responses. This ready-to-use component provides an out of the box solution for adding RAG-Powered chat interfaces to your Next.js application.\n\n<table>\n  <tr>\n    <td align=\"center\">\n      <img src=\"https://raw.githubusercontent.com/upstash/rag-chat-component/refs/heads/master/public/images/widget-closed.png\" alt=\"RAG Chat Component - Closed State\" width=\"300\"/><br/>\n      <em>Closed State</em>\n    </td>\n    <td align=\"center\">\n      <img src=\"https://raw.githubusercontent.com/upstash/rag-chat-component/refs/heads/master/public/images/widget-open.png\" alt=\"RAG Chat Component - Open State\" width=\"300\"/><br/>\n      <em>Open State</em>\n    </td>\n  </tr>\n</table>\n\n## Features\n\n⚡ Streaming responses support\n\n💻 Server actions\n\n📱 Responsive design\n\n🔍 Real-time context retrieval\n\n💾 Persistent chat history\n\n🎨 Fully customizable UI components\n\n🎨 Dark/light mode support\n\n## Installation\n\n```bash\n# Using npm\nnpm install @upstash/rag-chat-component\n\n# Using pnpm\npnpm add @upstash/rag-chat-component\n\n# Using yarn\nyarn add @upstash/rag-chat-component\n```\n\n## Quick Start\n\n### 1. Environment Variables\n\nCreate an Upstash Vector database and set up the environment variables as below. If you don't have an account, you can start by going to [Upstash Console](https://console.upstash.com).\n\nChoose an embedding model when creating an index in Upstash Vector.\n\n```\nUPSTASH_VECTOR_REST_URL=\nUPSTASH_VECTOR_REST_TOKEN=\n\n# Optional for persistent chat history\nUPSTASH_REDIS_REST_URL=\nUPSTASH_REDIS_REST_TOKEN=\n\nOPENAI_API_KEY=\n\nTOGETHER_API_KEY=\n\n# Optional\nTOGETHER_MODEL=\n```\n\n### 2. Configure Styles\n\nIn your `tailwind.config.ts` file, add the configuration below:\n\n```ts\nimport type { Config } from \"tailwindcss\";\n\nexport default {\n  content: [\"./node_modules/@upstash/rag-chat-component/**/*.{js,mjs}\"],\n} satisfies Config;\n```\n\n### 3. Implementation\n\nThe RAG Chat Component can be integrated into your application using two straightforward approaches. Choose the method that best fits your project structure:\n\n#### 1. Using a Dedicated Component File (Recommended)\n\nCreate a seperate component file with the `use client` directive, then import and use it anywhere in your application.\n\n```jsx\n// components/chat.tsx\n\"use client\";\n\nimport { ChatComponent } from \"@upstash/rag-chat-component\";\n\nexport const Chat = () => {\n  return <ChatComponent />;\n};\n```\n\n```jsx\n// page.tsx\nimport { Chat } from \"./components/chat\";\n\nexport default function Home() {\n  return (\n    <>\n      <Chat />\n      <p>Home</p>\n    </>\n  );\n}\n```\n\n#### 2. Direct Integration in Client Components\n\nAlternatively, import and use the **ChatComponent** directly in your client-side pages.\n\n```jsx\n// page.tsx\n\"use client\";\nimport { ChatComponent } from \"@upstash/rag-chat-component\";\n\nexport default function Home() {\n  return (\n    <>\n      <ChatComponent />\n      <p>Home</p>\n    </>\n  );\n}\n```\n\n### 4. Choosing Chat Model\n\nIt's possible to choose one of the [together.ai](https://www.together.ai/) models for the chat.\nDefault model is `meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo`. You can configure it in the environment variables.\n\n```\nTOGETHER_MODEL=\"deepseek-ai/DeepSeek-V3\"\n```\n\n### 5. Additional Notes\n\nIf you're deploying on Vercel and experiencing timeout issues, you can increase the function execution time limit by adding the following configuration to your `vercel.json`:\n\n```\n{\n  \"functions\": {\n    \"app/**/*\": {\n      \"maxDuration\": 30\n    }\n  }\n}\n```\n\nThis extends the function timeout to 30 seconds, allowing more time for RAG operations to complete on serverless functions.\n\n</details>\n\n## Adding Content\n\nYou can add content to your RAG Chat component in several ways:\n\n<details>\n<summary>1. Using Upstash Vector SDK</summary>\n\nUpstash has Vector SDKs in JS and Python. You can use those SDK to insert data to your Vector index.\n\n[Vector JS SDK](https://github.com/upstash/vector-js)\n\n[Vector Python SDK](https://github.com/upstash/vector-py)\n\nFor other languages you can use [Vector REST API](https://upstash.com/docs/vector/api/get-started).\n\n</details>\n\n<details>\n<summary>2. Using Upstash Vector UI</summary>\n\nFor testing purpose, you can add your data directly through the Upstash Vector Console:\n\n1. Navigate to [Upstash Console](http://console.upstash.com/vector).\n2. Go to details page of the Vector database.\n3. Navigate to **Data Browser Tab**.\n4. Here, you can upsert data or upload a PDF.\n\n<img src=\"./public/images/vector-databrowser.png\" alt=\"Vector Databrowser\"/><br/>\n\n</details>\n\n<details>\n<summary>3. docs2vector tool</summary>\n\nIf you are planning to insert your documentation (markdown files) to your Vector index, then you can use [docs2vector](https://github.com/upstash/docs2vector/) tool.\n\n</details>\n\n## Development\n\nYou can use the playground for development, by basically running the command in the root.\n\n```bash\nbun run playground\n```\n\n## Roadmap\n\n- Integration with [QStash](https://upstash.com/docs/qstash/overall/getstarted) for infinite timout for serverless functions\n\n## Contributing\n\nWe welcome contributions! Please see our contributing guidelines for more details.\n\n## License\n\nMIT License - see the LICENSE file for details.\n"
  },
  {
    "path": "components.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"default\",\n  \"rsc\": true,\n  \"tsx\": true,\n  \"tailwind\": {\n    \"config\": \"tailwind.config.ts\",\n    \"css\": \"src/app/globals.css\",\n    \"baseColor\": \"slate\",\n    \"cssVariables\": true,\n    \"prefix\": \"\"\n  },\n  \"aliases\": {\n    \"components\": \"@/components\",\n    \"utils\": \"@/lib/utils\"\n  }\n}\n"
  },
  {
    "path": "examples/nextjs/.eslintrc.json",
    "content": "{\n  \"extends\": [\"next/core-web-vitals\", \"next/typescript\"]\n}\n"
  },
  {
    "path": "examples/nextjs/.gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.*\n.yarn/*\n!.yarn/patches\n!.yarn/plugins\n!.yarn/releases\n!.yarn/versions\n\n# testing\n/coverage\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# env files (can opt-in for committing if needed)\n.env*\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\nnext-env.d.ts\n"
  },
  {
    "path": "examples/nextjs/README.md",
    "content": "This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).\n\n## Getting Started\n\nFirst, run the development server:\n\n```bash\nnpm run dev\n# or\nyarn dev\n# or\npnpm dev\n# or\nbun dev\n```\n\nOpen [http://localhost:3000](http://localhost:3000) with your browser to see the result.\n\nYou can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.\n\nThis project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.\n\n## Learn More\n\nTo learn more about Next.js, take a look at the following resources:\n\n- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.\n- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.\n\nYou can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!\n\n## Deploy on Vercel\n\nThe easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.\n\nCheck out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.\n"
  },
  {
    "path": "examples/nextjs/app/components/chat.tsx",
    "content": "\"use client\";\n\nimport { ChatComponent } from \"@upstash/rag-chat-component\";\n\nexport const Chat = () => {\n  return <ChatComponent />;\n};\n"
  },
  {
    "path": "examples/nextjs/app/components/features.tsx",
    "content": "export default function Features() {\n  return (\n    <div className=\"sc mt-16 grid gap-4 sm:grid-cols-2\">\n      <FeatureCard\n        title=\"Streaming Responses\"\n        description=\"Real-time AI responses with token-by-token streaming for a natural chat experience.\"\n      />\n      <FeatureCard\n        title=\"RAG Integration\"\n        description=\"Built-in support for Retrieval-Augmented Generation to provide context-aware responses.\"\n      />\n      <FeatureCard\n        title=\"Fully Customizable\"\n        description=\"Easily customize the appearance and behavior to match your application's design.\"\n      />\n      <FeatureCard\n        title=\"Server Actions\"\n        description=\"Built for Next.js App Router with Server Actions.\"\n      />\n    </div>\n  );\n}\n\nexport function FeatureCard({\n  title,\n  description,\n}: {\n  title: string;\n  description: string;\n}) {\n  return (\n    <div className=\"rounded-3xl border border-emerald-700/20 bg-white/60 px-8 py-6 shadow-sm backdrop-blur\">\n      <h3 className=\"font-display text-lg font-semibold\">{title}</h3>\n      <p className=\"text-balance opacity-80\">{description}</p>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/nextjs/app/components/footer.tsx",
    "content": "import { ArrowUpRight } from \"lucide-react\";\n\nexport default function Footer() {\n  return (\n    <footer className=\"mt-16 flex flex-wrap items-center justify-center gap-6\">\n      <a\n        href=\"https://github.com/upstash/rag-chat-component\"\n        target=\"_blank\"\n        rel=\"noopener noreferrer\"\n        className=\"flex items-center gap-1 opacity-80 hover:underline\"\n      >\n        GitHub <ArrowUpRight size={14} />\n      </a>\n      <a\n        href=\"https://github.com/upstash/rag-chat-component?tab=readme-ov-file#rag-chat-component\"\n        className=\"flex items-center gap-1 opacity-80 hover:underline\"\n        target=\"_blank\"\n        rel=\"noopener noreferrer\"\n      >\n        Documentation <ArrowUpRight size={14} />\n      </a>\n      <a\n        href=\"https://github.com/upstash/rag-chat-component/tree/master/examples\"\n        className=\"flex items-center gap-1 opacity-80 hover:underline\"\n        target=\"_blank\"\n        rel=\"noopener noreferrer\"\n      >\n        Examples <ArrowUpRight size={14} />\n      </a>\n    </footer>\n  );\n}\n"
  },
  {
    "path": "examples/nextjs/app/components/header.tsx",
    "content": "export default function Header() {\n  return (\n    <header className=\"\">\n      <p className=\"mb-6 inline-block rounded-lg bg-emerald-100 px-3 py-1 text-sm text-emerald-700\">\n        Powered by{\" \"}\n        <a\n          href=\"http://upstash.com\"\n          target=\"_blank\"\n          className=\"font-semibold underline\"\n        >\n          Upstash\n        </a>{\" \"}\n        ,{\" \"}\n        <a\n          href=\"http://together.ai\"\n          target=\"_blank\"\n          className=\"font-semibold underline\"\n        >\n          TogetherAI\n        </a>{\" \"}\n        and{\" \"}\n        <a\n          href=\"https://sdk.vercel.ai/\"\n          target=\"_blank\"\n          className=\"font-semibold underline\"\n        >\n          Vercel AI SDK\n        </a>\n      </p>\n\n      <h1 className=\"text-balance font-display text-4xl font-bold tracking-tight sm:text-5xl\">\n        AI Chat Component for Next.js\n      </h1>\n\n      <h3 className=\"mt-3 px-12 font-display text-xl opacity-80 sm:px-32\">\n        A modern, customizable chat interface with streaming responses and RAG\n        capabilities\n      </h3>\n\n      <div className=\"mt-8 flex flex-col items-center justify-center gap-2 md:flex-row\">\n        <a\n          className=\"flex h-10 items-center justify-center gap-2 rounded-full bg-emerald-900 px-4 font-medium text-white hover:opacity-60\"\n          href=\"https://github.com/upstash/rag-chat-component\"\n          target=\"_blank\"\n          rel=\"noopener noreferrer\"\n        >\n          <svg\n            height=\"24\"\n            viewBox=\"0 0 24 24\"\n            version=\"1.1\"\n            className=\"fill-current\"\n          >\n            <path d=\"M12.5.75C6.146.75 1 5.896 1 12.25c0 5.089 3.292 9.387 7.863 10.91.575.101.79-.244.79-.546 0-.273-.014-1.178-.014-2.142-2.889.532-3.636-.704-3.866-1.35-.13-.331-.69-1.352-1.18-1.625-.402-.216-.977-.748-.014-.762.906-.014 1.553.834 1.769 1.179 1.035 1.74 2.688 1.25 3.349.948.1-.747.402-1.25.733-1.538-2.559-.287-5.232-1.279-5.232-5.678 0-1.25.445-2.285 1.178-3.09-.115-.288-.517-1.467.115-3.048 0 0 .963-.302 3.163 1.179.92-.259 1.897-.388 2.875-.388.977 0 1.955.13 2.875.388 2.2-1.495 3.162-1.179 3.162-1.179.633 1.581.23 2.76.115 3.048.733.805 1.179 1.825 1.179 3.09 0 4.413-2.688 5.39-5.247 5.678.417.36.776 1.05.776 2.128 0 1.538-.014 2.774-.014 3.162 0 .302.216.662.79.547C20.709 21.637 24 17.324 24 12.25 24 5.896 18.854.75 12.5.75Z\"></path>\n          </svg>\n          View on GitHub\n        </a>\n        <a\n          className=\"flex h-10 items-center justify-center rounded-full border-2 border-emerald-900/20 px-4 font-medium hover:border-emerald-900\"\n          href=\"https://github.com/upstash/rag-chat-component?tab=readme-ov-file#rag-chat-component\"\n          target=\"_blank\"\n          rel=\"noopener noreferrer\"\n        >\n          Documentation\n        </a>\n      </div>\n    </header>\n  );\n}\n"
  },
  {
    "path": "examples/nextjs/app/globals.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@keyframes anim-zoom {\n  0% {\n    width: 64px;\n    height: 64px;\n    opacity: 1;\n  }\n  99% {\n    width: 2000px;\n    height: 2000px;\n    opacity: 0;\n  }\n  100% {\n    width: 64px;\n    height: 64px;\n    opacity: 0;\n  }\n}\n\n.pulse {\n  @apply absolute left-0 right-0 size-16 -translate-x-1/2 -translate-y-1/2 rounded-full border-[60px] border-emerald-500/20 opacity-0;\n  animation: anim-zoom 40s ease infinite;\n}\n"
  },
  {
    "path": "examples/nextjs/app/layout.tsx",
    "content": "import type { Metadata } from \"next\";\nimport { Inter, Inter_Tight } from \"next/font/google\";\nimport \"./globals.css\";\n\nconst defaultFont = Inter({\n  variable: \"--font-sans\",\n  subsets: [\"latin\"],\n});\n\nconst displayFont = Inter_Tight({\n  variable: \"--font-display\",\n  subsets: [\"latin\"],\n});\n\nexport const metadata: Metadata = {\n  title: \"RAG Component\",\n  description: \"Streaming Chat Component with Persistent History\",\n  icons: {\n    icon: [{ url: \"upstash.png\" }],\n  },\n};\n\nexport default function RootLayout({\n  children,\n}: Readonly<{\n  children: React.ReactNode;\n}>) {\n  return (\n    <html\n      lang=\"en\"\n      className={`antialiased ${defaultFont.variable} ${displayFont.variable}`}\n    >\n      <body className=\"bg-gradient-to-br from-emerald-50 font-sans text-emerald-900\">\n        {children}\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "examples/nextjs/app/page.tsx",
    "content": "import Features from \"@/app/components/features\";\nimport Footer from \"@/app/components/footer\";\nimport Header from \"@/app/components/header\";\nimport { Chat } from \"./components/chat\";\n\nexport default function Home() {\n  return (\n    <div className=\"flex min-h-screen flex-col items-center justify-center px-8 py-12 text-center sm:px-12 sm:py-16\">\n      {/* page */}\n      <main className=\"flex max-w-screen-md flex-col items-center\">\n        <Header />\n        <Features />\n        <Footer />\n      </main>\n\n      {/* chat */}\n      <Chat />\n      <div className=\"pointer-events-none fixed bottom-0 right-0 -z-10 translate-x-1/2 translate-y-1/2\">\n        <div className=\"-translate-x-14 -translate-y-14\">\n          {[0, 5, 10, 15, 20, 25, 30, 35].map((o) => (\n            <span\n              key={o}\n              style={{ animationDelay: `${o}s` }}\n              className=\"pulse\"\n            />\n          ))}\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/nextjs/next.config.ts",
    "content": "import type { NextConfig } from \"next\";\n\nconst nextConfig: NextConfig = {\n  /* config options here */\n};\n\nexport default nextConfig;\n"
  },
  {
    "path": "examples/nextjs/package.json",
    "content": "{\n  \"name\": \"nextjs\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"build\": \"next build\",\n    \"start\": \"next start\",\n    \"lint\": \"next lint\",\n    \"format\": \"pnpm prettier --write .\"\n  },\n  \"dependencies\": {\n    \"@upstash/rag-chat-component\": \"^0.2.2\",\n    \"next\": \"15.1.11\",\n    \"react\": \"^19.0.0\",\n    \"react-dom\": \"^19.0.0\"\n  },\n  \"devDependencies\": {\n    \"@ianvs/prettier-plugin-sort-imports\": \"^4.4.1\",\n    \"@types/node\": \"^20\",\n    \"@types/react\": \"^19\",\n    \"@types/react-dom\": \"^19\",\n    \"postcss\": \"^8\",\n    \"prettier\": \"^3.4.2\",\n    \"prettier-plugin-tailwindcss\": \"^0.6.9\",\n    \"tailwindcss\": \"^3.4.1\",\n    \"typescript\": \"^5\"\n  }\n}\n"
  },
  {
    "path": "examples/nextjs/postcss.config.mjs",
    "content": "/** @type {import('postcss-load-config').Config} */\nconst config = {\n  plugins: {\n    tailwindcss: {},\n  },\n};\n\nexport default config;\n"
  },
  {
    "path": "examples/nextjs/prettier.config.js",
    "content": "module.exports = {\n  arrowParens: \"always\",\n  bracketSameLine: false,\n  bracketSpacing: true,\n  semi: true,\n  singleQuote: false,\n  jsxSingleQuote: false,\n  trailingComma: \"all\",\n  singleAttributePerLine: false,\n  importOrderSeparation: true,\n  importOrderSortSpecifiers: true,\n  importOrderBuiltinModulesToTop: true,\n  tailwindFunctions: [\"clsx\"],\n  plugins: [\n    \"@ianvs/prettier-plugin-sort-imports\",\n    \"prettier-plugin-tailwindcss\",\n  ],\n};\n"
  },
  {
    "path": "examples/nextjs/tailwind.config.ts",
    "content": "import type { Config } from \"tailwindcss\";\n\nexport default {\n  content: [\n    \"./pages/**/*.{js,ts,jsx,tsx,mdx}\",\n    \"./components/**/*.{js,ts,jsx,tsx,mdx}\",\n    \"./app/**/*.{js,ts,jsx,tsx,mdx}\",\n    \"./node_modules/@upstash/rag-chat-component/**/*.{js,mjs}\",\n  ],\n  theme: {\n    extend: {\n      fontFamily: {\n        sans: [\"var(--font-sans)\"],\n        display: [\"var(--font-display)\"],\n      },\n    },\n  },\n} satisfies Config;\n"
  },
  {
    "path": "examples/nextjs/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"incremental\": true,\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ],\n    \"paths\": {\n      \"@/*\": [\"./*\"]\n    }\n  },\n  \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\", \".next/types/**/*.ts\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "examples/nextjs/vercel.json",
    "content": "{\n  \"functions\": {\n    \"app/**/*\": {\n      \"maxDuration\": 30\n    }\n  }\n}\n"
  },
  {
    "path": "index.ts",
    "content": "console.log(\"Hello via Bun!\");\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"@upstash/rag-chat-component\",\n  \"description\": \"Streaming Chat Component with Persistent History\",\n  \"version\": \"0.2.2\",\n  \"module\": \"index.ts\",\n  \"main\": \"./dist/client/index.mjs\",\n  \"types\": \"./dist/client/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"import\": \"./dist/client/index.mjs\"\n    },\n    \"./styles.css\": \"./dist/client/styles.mjs\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"peerDependencies\": {\n    \"next\": \"^14 || ^15\",\n    \"react\": \"^18 || ^19\",\n    \"typescript\": \"^5\"\n  },\n  \"scripts\": {\n    \"build\": \"tsup\",\n    \"playground\": \"cd playground && npm run dev\"\n  },\n  \"dependencies\": {\n    \"@ai-sdk/openai\": \"^1.0.18\",\n    \"@radix-ui/react-scroll-area\": \"^1.2.2\",\n    \"@radix-ui/react-slot\": \"^1.1.1\",\n    \"@upstash/redis\": \"^1.34.3\",\n    \"@upstash/vector\": \"^1.2.0\",\n    \"ai\": \"^4.0.33\",\n    \"class-variance-authority\": \"^0.7.1\",\n    \"clsx\": \"^2.1.1\",\n    \"lodash.debounce\": \"^4.0.8\",\n    \"lucide-react\": \"^0.471.0\",\n    \"react\": \"^19\",\n    \"react-dom\": \"^19\",\n    \"react-textarea-autosize\": \"^8.5.7\",\n    \"tailwind-merge\": \"^2.6.0\",\n    \"tailwindcss\": \"^3.4.1\",\n    \"tailwindcss-animate\": \"^1.0.7\",\n    \"together-ai\": \"^0.11.1\"\n  },\n  \"devDependencies\": {\n    \"@types/bun\": \"latest\",\n    \"@types/lodash.debounce\": \"^4.0.9\",\n    \"@types/node\": \"^22\",\n    \"@types/react\": \"^19\",\n    \"@types/react-dom\": \"^19\",\n    \"autoprefixer\": \"^10.4.20\",\n    \"esbuild-fix-imports-plugin\": \"^1.0.10\",\n    \"postcss\": \"^8\",\n    \"postcss-prefix-selector\": \"^2.1.0\",\n    \"prettier\": \"^3.4.2\",\n    \"prettier-plugin-tailwindcss\": \"^0.6.9\",\n    \"tsup\": \"^8.2.0\"\n  },\n  \"homepage\": \"https://github.com/upstash/rag-chat-component#readme\",\n  \"author\": \"Fahreddin Ozcan\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/upstash/rag-chat-component/issues\"\n  }\n}\n"
  },
  {
    "path": "playground/.gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.*\n.yarn/*\n!.yarn/patches\n!.yarn/plugins\n!.yarn/releases\n!.yarn/versions\n\n# testing\n/coverage\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.pnpm-debug.log*\n\n# env files (can opt-in for committing if needed)\n.env*\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\nnext-env.d.ts\n"
  },
  {
    "path": "playground/README.md",
    "content": "This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).\n\n## Getting Started\n\nFirst, run the development server:\n\n```bash\nnpm run dev\n# or\nyarn dev\n# or\npnpm dev\n# or\nbun dev\n```\n\nOpen [http://localhost:3000](http://localhost:3000) with your browser to see the result.\n\nYou can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.\n\nThis project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.\n\n## Learn More\n\nTo learn more about Next.js, take a look at the following resources:\n\n- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.\n- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.\n\nYou can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!\n\n## Deploy on Vercel\n\nThe easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.\n\nCheck out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.\n"
  },
  {
    "path": "playground/eslint.config.mjs",
    "content": "import { dirname } from \"path\";\nimport { fileURLToPath } from \"url\";\nimport { FlatCompat } from \"@eslint/eslintrc\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\n\nconst compat = new FlatCompat({\n  baseDirectory: __dirname,\n});\n\nconst eslintConfig = [\n  ...compat.extends(\"next/core-web-vitals\", \"next/typescript\"),\n];\n\nexport default eslintConfig;\n"
  },
  {
    "path": "playground/next.config.ts",
    "content": "import type { NextConfig } from \"next\";\n\nconst nextConfig: NextConfig = {\n  /* config options here */\n};\n\nexport default nextConfig;\n"
  },
  {
    "path": "playground/package.json",
    "content": "{\n  \"name\": \"playground\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"build\": \"next build\",\n    \"start\": \"next start\",\n    \"lint\": \"next lint\"\n  },\n  \"dependencies\": {\n    \"react\": \"^19.0.0\",\n    \"react-dom\": \"^19.0.0\",\n    \"next\": \"15.1.6\"\n  },\n  \"exports\": {\n    \".\": {\n      \"import\": \"./src/client/index.ts\"\n    }\n  },\n  \"devDependencies\": {\n    \"typescript\": \"^5\",\n    \"@types/node\": \"^20\",\n    \"@types/react\": \"^19\",\n    \"@types/react-dom\": \"^19\",\n    \"postcss\": \"^8\",\n    \"tailwindcss\": \"^3.4.1\",\n    \"eslint\": \"^9\",\n    \"eslint-config-next\": \"15.1.6\",\n    \"@eslint/eslintrc\": \"^3\"\n  }\n}\n"
  },
  {
    "path": "playground/postcss.config.mjs",
    "content": "/** @type {import('postcss-load-config').Config} */\nconst config = {\n  plugins: {\n    tailwindcss: {},\n  },\n};\n\nexport default config;\n"
  },
  {
    "path": "playground/src/app/globals.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n:root {\n  --background: #ffffff;\n  --foreground: #171717;\n}\n\n@media (prefers-color-scheme: dark) {\n  :root {\n    --background: #0a0a0a;\n    --foreground: #ededed;\n  }\n}\n\nbody {\n  color: var(--foreground);\n  background: var(--background);\n  font-family: Arial, Helvetica, sans-serif;\n}\n"
  },
  {
    "path": "playground/src/app/layout.tsx",
    "content": "import type { Metadata } from \"next\";\nimport { Geist, Geist_Mono } from \"next/font/google\";\nimport \"./globals.css\";\n\nconst geistSans = Geist({\n  variable: \"--font-geist-sans\",\n  subsets: [\"latin\"],\n});\n\nconst geistMono = Geist_Mono({\n  variable: \"--font-geist-mono\",\n  subsets: [\"latin\"],\n});\n\nexport const metadata: Metadata = {\n  title: \"Create Next App\",\n  description: \"Generated by create next app\",\n};\n\nexport default function RootLayout({\n  children,\n}: Readonly<{\n  children: React.ReactNode;\n}>) {\n  return (\n    <html lang=\"en\">\n      <body\n        className={`${geistSans.variable} ${geistMono.variable} antialiased`}\n      >\n        {children}\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "playground/src/app/page.tsx",
    "content": "import { ChatComponent } from \"@/client/components/chat-component\";\n\nexport default function Home() {\n  return (\n    <main className=\"min-h-screen p-8\">\n      <h1 className=\"mb-8 text-2xl font-bold\">Component Playground</h1>\n      <div className=\"mx-auto max-w-3xl\">\n        <ChatComponent />\n      </div>\n    </main>\n  );\n}\n"
  },
  {
    "path": "playground/tailwind.config.ts",
    "content": "import type { Config } from \"tailwindcss\";\n\nexport default {\n  content: [\n    \"./src/pages/**/*.{js,ts,jsx,tsx,mdx}\",\n    \"./src/components/**/*.{js,ts,jsx,tsx,mdx}\",\n    \"./src/app/**/*.{js,ts,jsx,tsx,mdx}\",\n    '../src/**/*.{js,ts,jsx,tsx,mdx}',\n  ],\n  theme: {\n    extend: {\n      colors: {\n        background: \"var(--background)\",\n        foreground: \"var(--foreground)\",\n      },\n    },\n  },\n  plugins: [],\n} satisfies Config;\n"
  },
  {
    "path": "playground/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"incremental\": true,\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ],\n    \"paths\": {\n      \"@/*\": [\"../src/*\"],\n      \"@playground/*\": [\"./src/*\"]\n    }\n  },\n  \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\", \".next/types/**/*.ts\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "postcss.config.mjs",
    "content": "/** @type {import(\"postcss-load-config\").Config} */\nconst config = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n    \"postcss-prefix-selector\": {\n      prefix: \".ups-chat\",\n    },\n  },\n};\n\nexport default config;\n"
  },
  {
    "path": "src/client/components/chat-component.tsx",
    "content": "\"use client\";\n\nimport { ScrollArea } from \"@radix-ui/react-scroll-area\";\n\nimport * as React from \"react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { serverChat } from \"../../server/actions/chat\";\nimport { deleteHistory, getHistory } from \"../../server/actions/history\";\n\nimport type { Message } from \"../../server/lib/types\";\nimport { cn } from \"./lib/utils\";\nimport { Button } from \"./ui/button\";\nimport { ArrowUp, Bot, Loader2, X } from \"lucide-react\";\nimport TextareaAutosize from \"react-textarea-autosize\";\nimport { readStreamableValue } from \"ai/rsc\";\n\ntype ChatComponentProps = {\n  theme?: {\n    triggerButtonIcon?: React.ReactNode;\n    triggerButtonColor?: string;\n  };\n};\n\nexport const ChatComponent = ({ theme }: ChatComponentProps) => {\n  const [conversation, setConversation] = useState<Message[]>([]);\n  const [sessionId, setSessionId] = useState<string>(\"\");\n  const [input, setInput] = useState<string>(\"\");\n  const [isLoading, setIsLoading] = useState(false);\n  const [isOpen, setIsOpen] = useState(false);\n  const [isStreaming, setIsStreaming] = useState(false);\n  const scrollAreaRef = useRef<HTMLDivElement>(null);\n  const lastMessageRef = useRef<HTMLDivElement>(null);\n  const inputRef = useRef<HTMLTextAreaElement>(null);\n\n  const toggleChat = () => {\n    setIsOpen(!isOpen);\n    if (!isOpen) {\n      setTimeout(() => inputRef.current?.focus(), 100);\n    }\n  };\n\n  const scrollToBottom = () => {\n    if (lastMessageRef.current) {\n      lastMessageRef.current.scrollIntoView({ behavior: \"smooth\" });\n    }\n  };\n\n  useEffect(() => {\n    let id = localStorage.getItem(\"chat_session_id\");\n    if (!id) {\n      id = \"session_\" + Math.random().toString(36).substr(2, 9);\n      localStorage.setItem(\"chat_session_id\", id);\n    }\n    setSessionId(id);\n\n    const fetchHistory = async () => {\n      try {\n        setIsLoading(true);\n        const { messages } = await getHistory(id);\n        if (messages.length > 0) {\n          setConversation(messages);\n        }\n      } catch (error) {\n        console.error(\"Error fetching chat history:\", error);\n      } finally {\n        setIsLoading(false);\n      }\n    };\n\n    fetchHistory();\n  }, []);\n\n  useEffect(() => {\n    scrollToBottom();\n  }, [conversation]);\n\n  useEffect(() => {\n    if (isStreaming) {\n      const intervalId = setInterval(scrollToBottom, 100);\n      return () => clearInterval(intervalId);\n    }\n  }, [isStreaming]);\n\n  const handleSubmit = async (e: React.FormEvent) => {\n    e.preventDefault();\n    if (!input.trim()) return;\n\n    const userMessage: Message = {\n      content: input,\n      role: \"user\",\n      id: Date.now().toString(),\n    };\n\n    const newConversation = [...conversation, userMessage];\n    setConversation(newConversation);\n    setInput(\"\");\n    setIsLoading(true);\n\n    try {\n      const { output } = await serverChat({\n        messages: newConversation,\n        sessionId,\n      });\n      setIsStreaming(true);\n      let aiMessage: Message = {\n        content: \"\",\n        role: \"assistant\",\n        id: (Date.now() + 1).toString(),\n      };\n      setConversation((prev) => [...prev, aiMessage]);\n\n      let messageReceived = false;\n      for await (const delta of readStreamableValue(output)) {\n        if (delta) {\n          messageReceived = true;\n        }\n        aiMessage.content += delta;\n        setConversation((prev) =>\n          prev.map((msg) =>\n            msg.id === aiMessage.id\n              ? { ...msg, content: aiMessage.content }\n              : msg,\n          ),\n        );\n      }\n\n      if (!messageReceived || !aiMessage.content.trim()) {\n        setConversation((prev) => [\n          ...prev.slice(0, -1),\n          {\n            content: \"No response received. Please try again.\",\n            role: \"error\",\n            id: (Date.now() + 2).toString(),\n          },\n        ]);\n      }\n    } catch (error) {\n      console.error(\"Error in AI response:\", error);\n      setConversation((prev) => [\n        ...prev.slice(0, -1),\n        {\n          content: \"An error occurred. Please try again.\",\n          role: \"error\",\n          id: (Date.now() + 3).toString(),\n        },\n      ]);\n    } finally {\n      setIsLoading(false);\n      setIsStreaming(false);\n    }\n  };\n\n  const handleClearHistory = async () => {\n    if (!sessionId || isLoading) return;\n\n    setIsLoading(true);\n    try {\n      const { success } = await deleteHistory(sessionId);\n      if (success) {\n        setConversation([]);\n      }\n    } catch (error) {\n      console.error(\"Error clearing chat history:\", error);\n    } finally {\n      setIsLoading(false);\n    }\n  };\n\n  const renderMessage = (message: Message, index: number) => {\n    const isLastMessage = index === conversation.length - 1;\n    const showDots = isLastMessage && isStreaming;\n    const isUser = message.role === \"user\";\n    const isError = message.role === \"error\";\n\n    return (\n      <div\n        key={index}\n        ref={isLastMessage ? lastMessageRef : null}\n        className={cn(\"mb-2 flex\", isUser ? \"justify-end\" : \"justify-start\")}\n      >\n        {isUser ? (\n          // User message\n          <div className=\"rounded-2xl bg-black px-4 py-2 text-white\">\n            {message.content}\n          </div>\n        ) : isError ? (\n          <div className=\"flex max-w-[90%] items-start gap-3\">\n            <Bot size={28} strokeWidth={1.5} className=\"mt-2 shrink-0\" />\n            <div className=\"relative rounded-2xl bg-red-50 px-4 py-2 text-red-600\">\n              <span className=\"absolute -left-1 top-4 size-4 rotate-45 bg-inherit\" />\n              {message.content}\n            </div>\n          </div>\n        ) : (\n          // Assistant message\n          <div className=\"flex max-w-[90%] items-start gap-3\">\n            <Bot size={28} strokeWidth={1.5} className=\"mt-2 shrink-0\" />\n            <div className=\"relative rounded-2xl bg-zinc-100 px-4 py-2\">\n              <span className=\"absolute -left-1 top-4 size-4 rotate-45 bg-inherit\" />\n              {message.content}\n              {showDots && (\n                <span className=\"ml-1 inline-block\">\n                  <span className=\"dots animate-pulse\">...</span>\n                </span>\n              )}\n            </div>\n          </div>\n        )}\n      </div>\n    );\n  };\n\n  const hasMessages = conversation.length > 0;\n\n  return (\n    <div className=\"ups-chat rcc\">\n      {/* >>> Trigger Button */}\n      <Button\n        onClick={toggleChat}\n        className={cn(\n          \"fixed bottom-8 right-8 z-[9999] size-12\",\n          \"flex items-center justify-center p-0\",\n          \"rounded-full text-white shadow-xl\",\n          \"transition-all duration-300 ease-in-out\",\n          isOpen ? \"scale-0 opacity-0\" : \"scale-100 opacity-100\",\n        )}\n        style={{\n          backgroundColor: theme?.triggerButtonColor ?? \"#10b981\", // default: emerald-500\n        }}\n      >\n        {theme?.triggerButtonIcon ?? <Bot size={28} />}\n      </Button>\n\n      {/* >>> Chat Modal */}\n      <div\n        className={cn(\n          \"fixed z-50 antialiased\",\n          // Mobile (default)\n          \"bottom-0 left-0 right-0 w-full\",\n          \"rounded-t-2xl border-2 border-zinc-500\",\n          // Desktop\n          \"sm:!bottom-8 sm:!left-auto sm:!right-8 sm:!h-auto sm:!w-[420px]\",\n          \"sm:rounded-2xl sm:border-2\",\n          \"bg-white text-left text-black shadow-2xl\",\n          \"transition-all duration-300\",\n          isOpen\n            ? \"pointer-events-auto translate-y-0 opacity-100\"\n            : \"pointer-events-none translate-y-full opacity-0\",\n        )}\n      >\n        {/* Chat Header */}\n        <header className=\"flex items-center rounded-t-2xl border-b border-b-zinc-100 bg-zinc-50 px-6 py-5\">\n          <h3 className=\"grow text-lg font-semibold\">Chat Assistant</h3>\n\n          <div className=\"ml-auto flex shrink-0 items-center justify-end\">\n            {/* clear button */}\n            <Button\n              variant=\"ghost\"\n              size=\"sm\"\n              className=\"text-sm text-zinc-400 hover:bg-zinc-200 hover:text-red-500\"\n              disabled={!hasMessages}\n              onClick={() => {\n                handleClearHistory();\n              }}\n            >\n              Clear\n            </Button>\n            {/* close button */}\n\n            <Button\n              variant=\"ghost\"\n              size=\"icon\"\n              className=\"opacity-30 hover:bg-zinc-300 hover:opacity-100\"\n              onClick={toggleChat}\n            >\n              <X size={20} strokeWidth=\"1.5\" />\n            </Button>\n          </div>\n        </header>\n\n        {/* Chat Body */}\n        <ScrollArea\n          className=\"h-[40vh] overflow-auto overscroll-contain p-6 sm:h-[420px]\"\n          ref={scrollAreaRef}\n        >\n          {/* empty message */}\n          {!hasMessages && !isLoading && (\n            <div className=\"flex h-full items-center justify-center text-center text-sm\">\n              <span className=\"rounded-xl bg-zinc-50 p-4 text-zinc-500\">\n                Chat with the AI assistant\n              </span>\n            </div>\n          )}\n\n          {/* chat bubbles */}\n          <div className=\"flex flex-col gap-4\">\n            {conversation.map(renderMessage)}\n            {isLoading && !isStreaming && (\n              <div className=\"flex items-center justify-center py-2\">\n                <Loader2 className=\"h-6 w-6 animate-spin\" />\n              </div>\n            )}\n          </div>\n        </ScrollArea>\n\n        {/* Chat Form */}\n        <form onSubmit={handleSubmit} className=\"relative p-6\">\n          <TextareaAutosize\n            className={cn(\n              \"flex w-full rounded-3xl border bg-transparent px-4 py-3\",\n              \"focus:ring-primary focus:outline-none focus:ring-2\",\n              \"disabled:cursor-not-allowed disabled:opacity-50\",\n              \"resize-none\",\n            )}\n            rows={1}\n            maxRows={5}\n            value={input}\n            onChange={(e) => setInput(e.target.value)}\n            onKeyDown={(e) => {\n              if (e.key === \"Enter\" && !e.shiftKey) {\n                e.preventDefault();\n                handleSubmit(e);\n              }\n            }}\n            placeholder=\"Ask a question...\"\n            disabled={isLoading || isStreaming}\n            ref={inputRef}\n          />\n\n          <Button\n            type=\"submit\"\n            size=\"icon\"\n            className={cn(\n              \"absolute bottom-8 right-8 z-10 bg-black\",\n              (isLoading || isStreaming) && \"cursor-not-allowed opacity-50\",\n            )}\n            disabled={isLoading || isStreaming}\n          >\n            {isLoading || isStreaming ? (\n              <Loader2 size={20} className=\"animate-spin\" />\n            ) : (\n              <ArrowUp size={20} className=\"\" />\n            )}\n          </Button>\n        </form>\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "src/client/components/lib/utils.ts",
    "content": "import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}"
  },
  {
    "path": "src/client/components/styles.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n"
  },
  {
    "path": "src/client/components/ui/button.tsx",
    "content": "import * as React from \"react\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"../lib/utils\";\n\nconst buttonVariants = cva(\n  \"inline-flex items-center justify-center whitespace-nowrap rounded-md disabled:pointer-events-none disabled:opacity-50\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-black text-white\",\n        secondary: \"bg-black text-white\",\n        ghost: \"\",\n      },\n      size: {\n        default: \"h-10 px-4 py-2\",\n        sm: \"h-8 rounded-md px-3\",\n        icon: \"size-8 rounded-full p-0\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  },\n);\n\nexport interface ButtonProps\n  extends React.ButtonHTMLAttributes<HTMLButtonElement>,\n    VariantProps<typeof buttonVariants> {\n  asChild?: boolean;\n}\n\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n  ({ className, variant, size, asChild = false, ...props }, ref) => {\n    const Comp = asChild ? Slot : \"button\";\n    return (\n      <Comp\n        className={cn(buttonVariants({ variant, size, className }))}\n        ref={ref}\n        {...props}\n      />\n    );\n  },\n);\nButton.displayName = \"Button\";\n\nexport { Button, buttonVariants };\n"
  },
  {
    "path": "src/client/components/ui/scroll-area.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as ScrollAreaPrimitive from \"@radix-ui/react-scroll-area\";\n\nimport { cn } from \"../lib/utils\";\n\nconst ScrollArea = React.forwardRef<\n  React.ElementRef<typeof ScrollAreaPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>\n>(({ className, children, ...props }, ref) => (\n  <ScrollAreaPrimitive.Root\n    ref={ref}\n    className={cn(\"relative overflow-hidden\", className)}\n    {...props}\n  >\n    <ScrollAreaPrimitive.Viewport className=\"h-full w-full rounded-[inherit]\">\n      {children}\n    </ScrollAreaPrimitive.Viewport>\n    <ScrollBar />\n    <ScrollAreaPrimitive.Corner />\n  </ScrollAreaPrimitive.Root>\n));\nScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;\n\nconst ScrollBar = React.forwardRef<\n  React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,\n  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>\n>(({ className, orientation = \"vertical\", ...props }, ref) => (\n  <ScrollAreaPrimitive.ScrollAreaScrollbar\n    ref={ref}\n    orientation={orientation}\n    className={cn(\n      \"flex touch-none select-none transition-colors\",\n      orientation === \"vertical\" &&\n        \"h-full w-2.5 border-l border-l-transparent p-[1px]\",\n      orientation === \"horizontal\" &&\n        \"h-2.5 flex-col border-t border-t-transparent p-[1px]\",\n      className,\n    )}\n    {...props}\n  >\n    <ScrollAreaPrimitive.ScrollAreaThumb className=\"bg-border relative flex-1 rounded-full\" />\n  </ScrollAreaPrimitive.ScrollAreaScrollbar>\n));\nScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;\n\nexport { ScrollArea, ScrollBar };\n"
  },
  {
    "path": "src/client/index.ts",
    "content": "export { ChatComponent } from \"./components/chat-component\";\n"
  },
  {
    "path": "src/server/actions/chat.ts",
    "content": "\"use server\";\n\nimport { Index } from \"@upstash/vector\"\nimport { createOpenAI } from \"@ai-sdk/openai\"\nimport { streamText } from \"ai\"\nimport { createStreamableValue, type StreamableValue } from \"ai/rsc\";\nimport { DEFAULT_PROMPT } from \"../constants\";\nimport type { Message } from \"../lib/types\";\nimport { getHistoryClient } from \"../lib/history/get-client\";\n\ntype StreamMessage = {\n  role: 'user' | 'assistant';\n  content: string;\n}\n\nconst vectorIndex = new Index()\n\nconst together = createOpenAI({\n  apiKey: process.env.TOGETHER_API_KEY ?? \"\",\n  baseURL: \"https://api.together.xyz/v1\",\n\n})\n\nconst searchSimilarDocs = async (data: string, topK: number) => {\n  const results = await vectorIndex.query({\n    data,\n    topK: topK ? topK : 5,\n    includeMetadata: true,\n    includeData: true,\n  });\n\n  return results\n}\n\nconst history = getHistoryClient()\n\nexport const serverChat = async ({\n  messages,\n  sessionId,\n}: {\n  messages: Message[];\n  sessionId: string;\n}): Promise<{ output: StreamableValue<string> }> => {\n  const userMessage = messages[messages.length - 1]\n\n  await history.addMessage({\n    message: userMessage,\n    sessionId\n  })\n\n  const serverMessages = messages\n    .filter(msg => msg.role !== 'error')\n    .map(msg => ({\n      role: msg.role,\n      content: msg.content\n    })) as StreamMessage[];\n\n  const similarDocs = await searchSimilarDocs(userMessage.content, 5)\n\n  const context = similarDocs.map(doc => doc.data).join(\"\\n\")\n\n  const chatMessages = messages.map(message => message.content).join(\"\\n\")\n\n  const system = DEFAULT_PROMPT({ context, question: userMessage.content, chatMessages })\n\n  const stream = createStreamableValue(\"\");\n\n  (async () => {\n    const { textStream } = streamText({\n      model: together(process.env.TOGETHER_MODEL ?? \"deepseek-ai/DeepSeek-V3\"),\n      system,\n      messages: serverMessages,\n\n      async onFinish({ text }) {\n        await history.addMessage({\n          message: {\n            role: \"assistant\",\n            content: text,\n            id: Date.now().toString(),\n          },\n          sessionId\n        })\n      },\n\n    })\n\n    for await (const delta of textStream) {\n      stream.update(delta);\n    }\n\n    stream.done();\n  })();\n\n  return { output: stream.value }\n};\n"
  },
  {
    "path": "src/server/actions/history.ts",
    "content": "'use server'\n\nimport { getHistoryClient } from \"../lib/history/get-client\";\nimport type { Message } from \"../lib/types\";\n\nconst history = getHistoryClient();\n\nexport async function getHistory(sessionId: string): Promise<{ messages: Message[] }> {\n\ttry {\n\t\tconst messages = await history.getMessages({ sessionId });\n\t\treturn { messages };\n\t} catch (error) {\n\t\tconsole.error(\"Failed to fetch chat history:\", error);\n\t\treturn { messages: [] };\n\t}\n}\n\nexport async function deleteHistory(sessionId: string): Promise<{ success: boolean }> {\n\ttry {\n\t\tawait history.deleteMessages({ sessionId });\n\t\treturn { success: true };\n\t} catch (error) {\n\t\tconsole.error(\"Failed to delete chat history:\", error);\n\t\treturn { success: false };\n\t}\n}"
  },
  {
    "path": "src/server/constants.ts",
    "content": "type PromptParameters = {\n\tchatMessages?: string;\n\tquestion: string;\n\tcontext: string;\n};\n\ntype Prompt = ({ question, chatMessages, context }: PromptParameters) => string;\n\nexport const DEFAULT_PROMPT: Prompt = ({ context, question, chatMessages }) =>\n\t`You are a concise AI assistant helping users on a website. Provide brief, clear answers in 1-2 sentences when possible.\n  \n  Context and chat history are provided to help you answer questions accurately. Only use information from these sources.\n  \n  ${context ? `Context: ${context}\\n` : ''}${chatMessages ? `Previous messages: ${chatMessages}\\n` : ''}\n  Q: ${question}\n  A:`;\n\nexport const DEFAULT_CHAT_SESSION_ID = \"upstash-rag-chat-session\";\n\nexport const DEFAULT_HISTORY_LENGTH = 5;"
  },
  {
    "path": "src/server/index.ts",
    "content": "export { serverChat } from \"./actions/chat\";\n"
  },
  {
    "path": "src/server/lib/history/get-client.ts",
    "content": "import type { BaseMessageHistory } from \"../types\";\nimport { RedisHistory } from \"./redis\";\nimport { InMemoryHistory } from \"./in-memory\";\n\nexport const getHistoryClient = (): BaseMessageHistory => {\n\tconst redisUrl = process.env.UPSTASH_REDIS_REST_URL;\n\tconst redisToken = process.env.UPSTASH_REDIS_REST_TOKEN;\n\n\tif (redisUrl && redisToken) {\n\t\treturn new RedisHistory({\n\t\t\tconfig: {\n\t\t\t\turl: redisUrl,\n\t\t\t\ttoken: redisToken,\n\t\t\t}\n\t\t});\n\t}\n\n\treturn new InMemoryHistory();\n}"
  },
  {
    "path": "src/server/lib/history/in-memory.ts",
    "content": "import type { Message, BaseMessageHistory } from \"../types\";\nimport { DEFAULT_CHAT_SESSION_ID, DEFAULT_HISTORY_LENGTH } from \"../../constants\";\n\ndeclare global {\n\tvar store: Record<string, { messages: Message[] }>;\n}\nexport class InMemoryHistory implements BaseMessageHistory {\n\tconstructor() {\n\t\tif (!global.store) global.store = {};\n\t}\n\n\tasync addMessage(params: {\n\t\tmessage: Message;\n\t\tsessionId: string;\n\t\tsessionTTL?: number;\n\t}): Promise<void> {\n\t\tconst { message, sessionId = DEFAULT_CHAT_SESSION_ID } = params;\n\t\tif (!global.store[sessionId]) {\n\t\t\tglobal.store[sessionId] = { messages: [] };\n\t\t}\n\n\t\tconst oldMessages = global.store[sessionId].messages || [];\n\t\tconst newMessages = [message, ...oldMessages];\n\t\tglobal.store[sessionId].messages = newMessages;\n\t}\n\n\tasync deleteMessages({ sessionId }: { sessionId: string }): Promise<void> {\n\t\tif (!global.store[sessionId]) {\n\t\t\treturn;\n\t\t}\n\t\tglobal.store[sessionId].messages = [];\n\t}\n\n\tasync getMessages({\n\t\tsessionId = DEFAULT_CHAT_SESSION_ID,\n\t\tamount = DEFAULT_HISTORY_LENGTH,\n\t\tstartIndex = 0,\n\t}): Promise<Message[]> {\n\t\tif (!global.store[sessionId]) {\n\t\t\tglobal.store[sessionId] = { messages: [] };\n\t\t}\n\n\t\tconst messages = global.store[sessionId]?.messages ?? [];\n\t\tconst slicedMessages = messages.slice(startIndex, startIndex + amount);\n\t\treturn slicedMessages.reverse();\n\t}\n}\n"
  },
  {
    "path": "src/server/lib/history/redis.ts",
    "content": "import { Redis, type RedisConfigNodejs } from \"@upstash/redis\";\nimport { DEFAULT_CHAT_SESSION_ID, DEFAULT_HISTORY_LENGTH } from \"../../constants\";\nimport type { Message, BaseMessageHistory } from \"../types\";\n\nexport type RedisHistoryConfig = {\n\tconfig?: RedisConfigNodejs;\n\tclient?: Redis;\n};\n\nexport class RedisHistory implements BaseMessageHistory {\n\tpublic client: Redis;\n\n\tconstructor(config: RedisHistoryConfig) {\n\t\tconst { config: redisConfig, client } = config;\n\n\t\tif (client) {\n\t\t\tthis.client = client;\n\t\t} else if (redisConfig) {\n\t\t\tthis.client = new Redis(redisConfig);\n\t\t} else {\n\t\t\tthrow new Error(\n\t\t\t\t\"Redis message stores require either a config object or a pre-configured client.\"\n\t\t\t);\n\t\t}\n\t}\n\n\tasync addMessage(params: {\n\t\tmessage: Message;\n\t\tsessionId: string;\n\t\tsessionTTL?: number;\n\t}): Promise<void> {\n\t\tconst { message, sessionId = DEFAULT_CHAT_SESSION_ID, sessionTTL } = params;\n\t\tawait this.client.lpush(sessionId, JSON.stringify(message));\n\t\tif (sessionTTL) {\n\t\t\tawait this.client.expire(sessionId, sessionTTL);\n\t\t}\n\t}\n\n\tasync deleteMessages({ sessionId }: { sessionId: string }): Promise<void> {\n\t\tawait this.client.del(sessionId);\n\t}\n\n\tasync getMessages({\n\t\tsessionId = DEFAULT_CHAT_SESSION_ID,\n\t\tamount = DEFAULT_HISTORY_LENGTH,\n\t\tstartIndex = 0,\n\t}): Promise<Message[]> {\n\t\tconst endIndex = startIndex + amount - 1;\n\t\tconst messages = await this.client.lrange<Message>(\n\t\t\tsessionId,\n\t\t\tstartIndex,\n\t\t\tendIndex\n\t\t);\n\n\t\treturn messages.reverse();\n\t}\n}"
  },
  {
    "path": "src/server/lib/types.ts",
    "content": "export type Message = {\n\trole: \"user\" | \"assistant\" | \"error\"\n\tcontent: string;\n\tid: string\n}\n\nexport interface BaseMessageHistory {\n\taddMessage(params: {\n\t\tmessage: Message;\n\t\tsessionId: string;\n\t\tsessionTTL?: number;\n\t}): Promise<void>;\n\n\tdeleteMessages(params: {\n\t\tsessionId: string\n\t}): Promise<void>;\n\n\tgetMessages(params: {\n\t\tsessionId: string;\n\t\tamount?: number;\n\t\tstartIndex?: number;\n\t}): Promise<Message[]>;\n}"
  },
  {
    "path": "tailwind.config.ts",
    "content": "import type { Config } from \"tailwindcss\";\n\nconst config = {\n  content: [\"./src/**/*.{ts,tsx}\"],\n  theme: {\n    screens: {\n      'sm': '640px',\n      'md': '768px',\n      'lg': '1024px',\n      'xl': '1280px',\n      '2xl': '1536px',\n    },\n    extend: {\n      animation: {\n        \"accordion-down\": \"accordion-down 0.2s ease-out\",\n        \"accordion-up\": \"accordion-up 0.2s ease-out\",\n      },\n    },\n  },\n  plugins: [require(\"tailwindcss-animate\")],\n} satisfies Config;\n\nexport default config;"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"lib\": [\"ESNext\", \"DOM\"],\n    \"declarationDir\": \"dist\",\n    \"declaration\": true,\n    \"target\": \"ESNext\",\n    \"module\": \"ES2022\",\n    \"moduleDetection\": \"force\",\n    \"jsx\": \"react-jsx\",\n    \"allowJs\": true,\n    \"outDir\": \"./dist\",\n    \"types\": [\"react/experimental\", \"node\"],\n    \"jsxImportSource\": \"react\",\n    \"allowSyntheticDefaultImports\": true,\n    \"resolveJsonModule\": true,\n    \"skipDefaultLibCheck\": true,\n    \"moduleResolution\": \"Bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"noEmit\": true,\n    \"strict\": true,\n    \"skipLibCheck\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"noUnusedLocals\": false,\n    \"noUnusedParameters\": false,\n    \"noPropertyAccessFromIndexSignature\": false\n  },\n  \"include\": [\"src/**/*\", \"./package.json\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "tsup.config.ts",
    "content": "import { defineConfig } from \"tsup\";\nimport { fixExtensionsPlugin } from \"esbuild-fix-imports-plugin\";\n\nexport default defineConfig([\n  {\n    entry: [\"src/client\"],\n    outDir: \"dist/client\",\n    external: [\"react\", \"next\"],\n\n    // 👇 important: cjs doesn't work well\n    format: \"esm\",\n    splitting: false,\n    sourcemap: false,\n    clean: true,\n    dts: true,\n\n    // 👇 important: do not bundle\n    bundle: false,\n    minify: false,\n    treeshake: false,\n    injectStyle: true,\n    esbuildPlugins: [fixExtensionsPlugin()],\n  },\n  {\n    entry: [\"src/server\"],\n    outDir: \"dist/server\",\n    external: [\"react\", \"next\"],\n\n    // 👇 important: cjs doesn't work well\n    format: \"esm\",\n    splitting: false,\n    sourcemap: false,\n    clean: true,\n    dts: true,\n\n    // 👇 important: do not bundle\n    bundle: false,\n    minify: false,\n    esbuildPlugins: [fixExtensionsPlugin()],\n  },\n]);\n"
  }
]