Repository: run-llama/LlamaIndexTS
Pull request #1828: feat: component dir in llamaindex server
Author: login: thucpn
Created: 2025-04-08T04:42:09Z | Merged: 2025-04-09T10:25:21Z



Diff:
diff --git a/.changeset/selfish-waves-accept.md b/.changeset/selfish-waves-accept.md
new file mode 100644
index 0000000000..fb2165e61a
--- /dev/null
+++ b/.changeset/selfish-waves-accept.md
@@ -0,0 +1,5 @@
+---
+"@llamaindex/server": patch
+---
+
+feat: component dir in llamaindex server
diff --git a/apps/next/src/content/docs/llamaindex/modules/ui/llamaindex-server.mdx b/apps/next/src/content/docs/llamaindex/modules/ui/llamaindex-server.mdx
new file mode 100644
index 0000000000..9b79cc97e5
--- /dev/null
+++ b/apps/next/src/content/docs/llamaindex/modules/ui/llamaindex-server.mdx
@@ -0,0 +1,159 @@
+---
+title: Using LlamaIndex Server
+description: Running LlamaIndex workflows with both API endpoints and a user interface for interaction
+---
+
+import { Tab, Tabs } from "fumadocs-ui/components/tabs";
+
+# LlamaIndex Server
+
+LlamaIndexServer is a Next.js-based application that allows you to quickly launch your [LlamaIndex Workflows](https://ts.llamaindex.ai/docs/llamaindex/modules/agents/workflows) and [Agent Workflows](https://ts.llamaindex.ai/docs/llamaindex/modules/agents/agent_workflow) as an API server with an optional chat UI. It provides a complete environment for running LlamaIndex workflows with both API endpoints and a user interface for interaction.
+
+## Features
+
+- Serving a workflow as a chatbot
+- Built on Next.js for high performance and easy API development
+- Optional built-in chat UI with extendable UI components
+- Prebuilt development code
+
+## Installation
+
+<Tabs groupId="install" items={["npm", "yarn", "pnpm"]} persist>
+	```shell tab="npm"
+	npm install @llamaindex/server
+	```
+
+	```shell tab="yarn"
+	yarn add @llamaindex/server
+	```
+
+	```shell tab="pnpm"
+	pnpm add @llamaindex/server
+	```
+</Tabs>
+
+## Quick Start
+
+Create index.ts file and add the following code:
+
+```ts
+import { LlamaIndexServer } from "@llamaindex/server";
+import { wiki } from "@llamaindex/tools"; // or any other tool
+
+const createWorkflow = () => agent({ tools: [wiki()] })
+
+new LlamaIndexServer({
+  workflow: createWorkflow,
+  appTitle: "LlamaIndex App",
+  starterQuestions: ["Who is the first president of the United States?"],
+}).start();
+```
+
+## Running the Server
+
+In the same directory as `index.ts`, run the following command to start the server:
+
+  ```bash
+  tsx index.ts
+  ```
+The server will start at `http://localhost:3000`
+
+You can also make a request to the server:
+
+  ```bash
+  curl -X POST "http://localhost:3000/api/chat" -H "Content-Type: application/json" -d '{"message": "Who is the first president of the United States?"}'
+  ```
+
+## Configuration Options
+
+The LlamaIndexServer accepts the following configuration
+
+- `workflow`: A callable function that creates a workflow instance for each request
+- `starterQuestions`: List of starter questions for the chat UI
+- `appTitle`: The title of the application
+- `componentsDir`: The directory for custom UI components rendering events emitted by the workflow. The default is undefined, which does not render custom UI components.
+
+LlamaIndexServer accepts all the configuration options from Nextjs Custom Server such as `port`, `hostname`, `dev`, etc.
+See all Nextjs Custom Server options [here](https://nextjs.org/docs/app/building-your-application/configuring/custom-server).
+
+## Default Endpoints and Features
+
+### Chat Endpoint
+
+The server includes a default chat endpoint at `/api/chat` for handling chat interactions.
+
+### Chat UI
+
+The server always provides a chat interface at the root path (`/`) with:
+
+- Configurable starter questions
+- Real-time chat interface
+- API endpoint integration
+
+### Static File Serving
+
+- The server automatically mounts the `data` and `output` folders at `{server_url}{api_prefix}/files/data` (default: `/api/files/data`) and `{server_url}{api_prefix}/files/output` (default: `/api/files/output`) respectively.
+- Your workflows can use both folders to store and access files. As a convention, the `data` folder is used for documents that are ingested and the `output` folder is used for documents that are generated by the workflow.
+
+
+## Custom UI Components
+
+The LlamaIndex server provides support for rendering workflow events using custom UI components, allowing you to extend and customize the chat interface.
+
+### Overview
+
+Custom UI components are a powerful feature that enables you to:
+
+- Add custom interface elements to the chat UI using React JSX or TSX files
+- Extend the default chat interface functionality
+- Create specialized visualizations or interactions
+
+### Configuration
+
+Your workflow must emit events that fit this structure, allowing the LlamaIndex server to display the right UI components based on the event type.
+
+```json
+{
+    "type": "<event_name>",
+    "data": <data model>
+}
+```
+
+### Server Setup
+
+1. Initialize the LlamaIndex server with a component directory:
+
+```ts
+new LlamaIndexServer({
+  workflow: workflowFactory,
+  appTitle: "LlamaIndex App",
+  componentsDir: "components",
+}).start();
+```
+
+2. Add the custom component code to the directory following the naming pattern:
+
+   - File Extension: `.jsx` and `.tsx` for React components
+   - File Name: Should match the event type from your workflow (e.g., `deep_research_event.jsx` for handling `deep_research_event` type that you defined in your workflow). If there are TSX and JSX files with the same name, the TSX file will be used.
+   - Component Name: Export a default React component named `Component` that receives props from the event data
+
+   Example component structure:
+
+   ```jsx
+   function Component({ events }) {
+       // Your component logic here
+       return (
+           // Your UI code here
+       );
+   }
+   ```
+
+## Best Practices
+
+1. Always provide a workflow factory that creates fresh workflow instances
+2. Use environment variables for sensitive configuration
+3. Use starter questions to guide users in the chat UI
+
+## Getting Started with a New Project
+
+Want to start a new project with LlamaIndexServer? Check out our [create-llama](https://github.com/run-llama/create-llama) tool to quickly generate a new project with LlamaIndexServer.
\ No newline at end of file
diff --git a/apps/next/src/content/docs/llamaindex/modules/ui/meta.json b/apps/next/src/content/docs/llamaindex/modules/ui/meta.json
index 9763537f8a..6e016b04b8 100644
--- a/apps/next/src/content/docs/llamaindex/modules/ui/meta.json
+++ b/apps/next/src/content/docs/llamaindex/modules/ui/meta.json
@@ -2,5 +2,5 @@
   "title": "Chat UI",
   "description": "Use chat-ui to add a chat interface to your LlamaIndexTS application.",
   "defaultOpen": false,
-  "pages": ["install", "chat", "rsc"]
+  "pages": ["install", "chat", "rsc", "llamaindex-server"]
 }
diff --git a/packages/server/next/app/components/chat-section.tsx b/packages/server/next/app/components/chat-section.tsx
index 580310e5a3..e4a991724c 100644
--- a/packages/server/next/app/components/chat-section.tsx
+++ b/packages/server/next/app/components/chat-section.tsx
@@ -4,12 +4,25 @@ import { ChatSection as ChatSectionUI } from "@llamaindex/chat-ui";
 import "@llamaindex/chat-ui/styles/markdown.css";
 import "@llamaindex/chat-ui/styles/pdf.css";
 import { useChat } from "ai/react";
+import { useEffect, useState } from "react";
 import Header from "./header";
 import CustomChatInput from "./ui/chat/chat-input";
 import CustomChatMessages from "./ui/chat/chat-messages";
+import {
+  ComponentDef,
+  fetchComponentDefinitions,
+} from "./ui/chat/dynamic-events";
 import { getConfig } from "./ui/lib/utils";
 
 export default function ChatSection() {
+  const [componentDefs, setComponentDefs] = useState<ComponentDef[]>([]);
+
+  // fetch component definitions and use Babel to tranform JSX code to JS code
+  // this is triggered only once when the page is initialised
+  useEffect(() => {
+    fetchComponentDefinitions().then(setComponentDefs);
+  }, []);
+
   const handler = useChat({
     api: getConfig("CHAT_API"),
     onError: (error: unknown) => {
@@ -28,7 +41,7 @@ export default function ChatSection() {
     <div className="flex h-[85vh] w-full flex-col gap-2">
       <Header />
       <ChatSectionUI handler={handler} className="min-h-0 w-full flex-1">
-        <CustomChatMessages />
+        <CustomChatMessages componentDefs={componentDefs} />
         <CustomChatInput />
       </ChatSectionUI>
     </div>
diff --git a/packages/server/next/app/components/ui/chat/chat-message-content.tsx b/packages/server/next/app/components/ui/chat/chat-message-content.tsx
index 43815839d3..f470e80c37 100644
--- a/packages/server/next/app/components/ui/chat/chat-message-content.tsx
+++ b/packages/server/next/app/components/ui/chat/chat-message-content.tsx
@@ -1,17 +1,21 @@
 "use client";
 
 import { ChatMessage } from "@llamaindex/chat-ui";
-import { DeepResearchCard } from "./custom/deep-research-card";
+import { ComponentDef, DynamicEvents } from "./dynamic-events";
 import { ToolAnnotations } from "./tools/chat-tools";
 
-export function ChatMessageContent() {
+export function ChatMessageContent({
+  componentDefs,
+}: {
+  componentDefs: ComponentDef[];
+}) {
   return (
     <ChatMessage.Content>
       <ChatMessage.Content.Event />
       <ChatMessage.Content.AgentEvent />
-      <DeepResearchCard />
       <ToolAnnotations />
       <ChatMessage.Content.Image />
+      <DynamicEvents componentDefs={componentDefs} />
       <ChatMessage.Content.Markdown />
       <ChatMessage.Content.DocumentFile />
       <ChatMessage.Content.Source />
diff --git a/packages/server/next/app/components/ui/chat/chat-messages.tsx b/packages/server/next/app/components/ui/chat/chat-messages.tsx
index 4de98ebfc8..6e15d20a64 100644
--- a/packages/server/next/app/components/ui/chat/chat-messages.tsx
+++ b/packages/server/next/app/components/ui/chat/chat-messages.tsx
@@ -4,9 +4,15 @@ import { ChatMessage, ChatMessages, useChatUI } from "@llamaindex/chat-ui";
 import { ChatMessageAvatar } from "./chat-avatar";
 import { ChatMessageContent } from "./chat-message-content";
 import { ChatStarter } from "./chat-starter";
+import { ComponentDef } from "./dynamic-events";
 
-export default function CustomChatMessages() {
+export default function CustomChatMessages({
+  componentDefs,
+}: {
+  componentDefs: ComponentDef[];
+}) {
   const { messages } = useChatUI();
+
   return (
     <ChatMessages className="rounded-xl shadow-xl">
       <ChatMessages.List>
@@ -17,7 +23,7 @@ export default function CustomChatMessages() {
             isLast={index === messages.length - 1}
           >
             <ChatMessageAvatar />
-            <ChatMessageContent />
+            <ChatMessageContent componentDefs={componentDefs} />
             <ChatMessage.Actions />
           </ChatMessage>
         ))}
diff --git a/packages/server/next/app/components/ui/chat/custom/deep-research-card.tsx b/packages/server/next/app/components/ui/chat/custom/deep-research-card.tsx
deleted file mode 100644
index 088bf4100e..0000000000
--- a/packages/server/next/app/components/ui/chat/custom/deep-research-card.tsx
+++ /dev/null
@@ -1,209 +0,0 @@
-"use client";
-
-import { getCustomAnnotation, useChatMessage } from "@llamaindex/chat-ui";
-import {
-  AlertCircle,
-  CheckCircle2,
-  CircleDashed,
-  Clock,
-  NotebookPen,
-  Search,
-} from "lucide-react";
-import { useMemo } from "react";
-import {
-  Accordion,
-  AccordionContent,
-  AccordionItem,
-  AccordionTrigger,
-} from "../../accordion";
-import { Card, CardContent, CardHeader, CardTitle } from "../../card";
-import { cn } from "../../lib/utils";
-import { Markdown } from "./markdown";
-
-// Streaming event types
-type EventState = "pending" | "inprogress" | "done" | "error";
-
-type DeepResearchEvent = {
-  type: "deep_research_event";
-  data: {
-    event: "retrieve" | "analyze" | "answer";
-    state: EventState;
-    id?: string;
-    question?: string;
-    answer?: string | null;
-  };
-};
-
-// UI state types
-type QuestionState = {
-  id: string;
-  question: string;
-  answer: string | null;
-  state: EventState;
-  isOpen: boolean;
-};
-
-type DeepResearchCardState = {
-  retrieve: {
-    state: EventState | null;
-  };
-  analyze: {
-    state: EventState | null;
-    questions: QuestionState[];
-  };
-};
-
-interface DeepResearchCardProps {
-  className?: string;
-}
-
-const stateIcon: Record<EventState, React.ReactNode> = {
-  pending: <Clock className="h-4 w-4 text-yellow-500" />,
-  inprogress: <CircleDashed className="h-4 w-4 animate-spin text-blue-500" />,
-  done: <CheckCircle2 className="h-4 w-4 text-green-500" />,
-  error: <AlertCircle className="h-4 w-4 text-red-500" />,
-};
-
-// Transform the state based on the event without mutations
-const transformState = (
-  state: DeepResearchCardState,
-  event: DeepResearchEvent,
-): DeepResearchCardState => {
-  switch (event.data.event) {
-    case "answer": {
-      const { id, question, answer } = event.data;
-      if (!id || !question) return state;
-
-      const updatedQuestions = state.analyze.questions.map((q) => {
-        if (q.id !== id) return q;
-        return {
-          ...q,
-          state: event.data.state,
-          answer: answer ?? q.answer,
-        };
-      });
-
-      const newQuestion = !state.analyze.questions.some((q) => q.id === id)
-        ? [
-            {
-              id,
-              question,
-              answer: answer ?? null,
-              state: event.data.state,
-              isOpen: false,
-            },
-          ]
-        : [];
-
-      return {
-        ...state,
-        analyze: {
-          ...state.analyze,
-          questions: [...updatedQuestions, ...newQuestion],
-        },
-      };
-    }
-
-    case "retrieve":
-    case "analyze":
-      return {
-        ...state,
-        [event.data.event]: {
-          ...state[event.data.event],
-          state: event.data.state,
-        },
-      };
-
-    default:
-      return state;
-  }
-};
-
-// Convert deep research events to state
-const deepResearchEventsToState = (
-  events: DeepResearchEvent[] | undefined,
-): DeepResearchCardState => {
-  if (!events?.length) {
-    return {
-      retrieve: { state: null },
-      analyze: { state: null, questions: [] },
-    };
-  }
-
-  const initialState: DeepResearchCardState = {
-    retrieve: { state: null },
-    analyze: { state: null, questions: [] },
-  };
-
-  return events.reduce(
-    (acc: DeepResearchCardState, event: DeepResearchEvent) =>
-      transformState(acc, event),
-    initialState,
-  );
-};
-
-export function DeepResearchCard({ className }: DeepResearchCardProps) {
-  const { message } = useChatMessage();
-
-  const state = useMemo(() => {
-    const deepResearchEvents = getCustomAnnotation<DeepResearchEvent>(
-      message.annotations,
-      (annotation) => annotation?.type === "deep_research_event",
-    );
-    if (!deepResearchEvents.length) return null;
-    return deepResearchEventsToState(deepResearchEvents);
-  }, [message.annotations]);
-
-  if (!state) return null;
-
-  return (
-    <Card className={cn("w-full", className)}>
-      <CardHeader className="space-y-4">
-        {state.retrieve.state !== null && (
-          <CardTitle className="flex items-center gap-2">
-            <Search className="h-5 w-5" />
-            {state.retrieve.state === "inprogress"
-              ? "Searching..."
-              : "Search completed"}
-          </CardTitle>
-        )}
-        {state.analyze.state !== null && (
-          <CardTitle className="flex items-center gap-2 border-t pt-4">
-            <NotebookPen className="h-5 w-5" />
-            {state.analyze.state === "inprogress" ? "Analyzing..." : "Analysis"}
-          </CardTitle>
-        )}
-      </CardHeader>
-
-      <CardContent>
-        {state.analyze.questions.length > 0 && (
-          <Accordion type="single" collapsible className="space-y-2">
-            {state.analyze.questions.map((question: QuestionState) => (
-              <AccordionItem
-                key={question.id}
-                value={question.id}
-                className="rounded-lg border [&[data-state=open]>div]:rounded-b-none"
-              >
-                <AccordionTrigger className="hover:bg-accent gap-2 px-3 py-3 hover:no-underline">
-                  <div className="flex w-full items-center gap-2">
-                    <div className="flex-shrink-0">
-                      {stateIcon[question.state]}
-                    </div>
-                    <span className="flex-1 text-left font-medium">
-                      {question.question}
-                    </span>
-                  </div>
-                </AccordionTrigger>
-                {question.answer && (
-                  <AccordionContent className="border-t px-3 py-3">
-                    <Markdown content={question.answer} />
-                  </AccordionContent>
-                )}
-              </AccordionItem>
-            ))}
-          </Accordion>
-        )}
-      </CardContent>
-    </Card>
-  );
-}
diff --git a/packages/server/next/app/components/ui/chat/dynamic-events.tsx b/packages/server/next/app/components/ui/chat/dynamic-events.tsx
new file mode 100644
index 0000000000..dc66fc5042
--- /dev/null
+++ b/packages/server/next/app/components/ui/chat/dynamic-events.tsx
@@ -0,0 +1,147 @@
+"use client";
+
+import * as Babel from "@babel/standalone"; // Import Babel standalone for runtime transpilation
+import {
+  getChatUIAnnotation,
+  JSONValue,
+  MessageAnnotation,
+  MessageAnnotationType,
+  useChatMessage,
+} from "@llamaindex/chat-ui";
+import React, { useEffect, useRef } from "react";
+import { getConfig } from "../lib/utils";
+
+export type ComponentDef = {
+  type: string; // eg. deep_research_event
+  code: string; // eg. export const DeepResearchEvent = () => {...}
+  filename: string; // eg. deep_research_event.tsx
+};
+
+type EventComponent = ComponentDef & {
+  events: JSONValue[];
+};
+
+// image, document_file, sources, events, suggested_questions, agent
+const BUILT_IN_CHATUI_COMPONENTS = Object.values(MessageAnnotationType);
+
+export const DynamicEvents = ({
+  componentDefs,
+}: {
+  componentDefs: ComponentDef[];
+}) => {
+  const {
+    message: { annotations },
+  } = useChatMessage();
+
+  const shownWarningsRef = useRef<Set<string>>(new Set()); // track warnings
+
+  // Check for missing components in annotations
+  useEffect(() => {
+    if (!annotations?.length) return;
+
+    const availableComponents = new Set(componentDefs.map((comp) => comp.type));
+
+    annotations.forEach((annotation: MessageAnnotation) => {
+      const type = annotation.type;
+      if (!type) return; // skip if annotation doesn't have a type
+
+      const events = getChatUIAnnotation(annotations, type);
+
+      // Skip if it's a built-in component or if we've already shown the warning
+      if (
+        BUILT_IN_CHATUI_COMPONENTS.includes(type) ||
+        shownWarningsRef.current.has(type)
+      ) {
+        return;
+      }
+
+      // If we have events for a type but no component definition, show a warning
+      if (events && !availableComponents.has(type)) {
+        console.warn(
+          `No component found for event type: ${type}. Please add a component file named ${type}.tsx or ${type}.jsx in your components directory.`,
+        );
+        shownWarningsRef.current.add(type);
+      }
+    });
+  }, [annotations, componentDefs]);
+
+  const components: EventComponent[] = componentDefs
+    .map((comp) => {
+      const events = getChatUIAnnotation(annotations, comp.type) as JSONValue[]; // get all event data by type
+      if (!events?.length) return null;
+      return { ...comp, events };
+    })
+    .filter((comp) => comp !== null);
+
+  if (components.length === 0) return null;
+
+  return (
+    <div className="components-container">
+      {components.map((component, index) => {
+        return (
+          <React.Fragment key={`${component.type}-${index}`}>
+            {renderEventComponent(component)}
+          </React.Fragment>
+        );
+      })}
+    </div>
+  );
+};
+
+export async function fetchComponentDefinitions(): Promise<ComponentDef[]> {
+  const endpoint = getConfig("COMPONENTS_API");
+  if (!endpoint) return [];
+
+  try {
+    const response = await fetch(endpoint);
+    const components = (await response.json()) as ComponentDef[];
+
+    // Only need to handle transpilation now
+    const transpiledComponents = components
+      .map((comp) => ({
+        ...comp,
+        code: transpileCode(comp.code, comp.filename),
+      }))
+      .filter((comp): comp is ComponentDef => comp.code !== null);
+
+    return transpiledComponents;
+  } catch (error) {
+    console.log("Error fetching dynamic components:", error);
+    return [];
+  }
+}
+
+// convert TSX code to JS code using Babel
+function transpileCode(code: string, filename: string): string | null {
+  try {
+    const transpiledCode = Babel.transform(code, {
+      presets: ["react", "typescript"],
+      filename,
+    }).code;
+
+    if (!transpiledCode) {
+      console.error("Transpiled code is empty");
+      return null;
+    }
+
+    return transpiledCode;
+  } catch (error) {
+    console.error("Error transpiling code:", error);
+    return null;
+  }
+}
+
+function renderEventComponent(component: EventComponent) {
+  try {
+    const Component = createComponentFromCode(component.code);
+    return React.createElement(Component, { events: component.events });
+  } catch (error) {
+    console.error(`Error rendering component ${component.type}:`, error);
+    return null;
+  }
+}
+
+function createComponentFromCode(code: string) {
+  const componentFn = new Function("React", `${code}; return Component;`);
+  return componentFn(React);
+}
diff --git a/packages/server/next/app/globals.css b/packages/server/next/app/globals.css
index 354568f585..6ea3d98870 100644
--- a/packages/server/next/app/globals.css
+++ b/packages/server/next/app/globals.css
@@ -211,3 +211,7 @@
     animation: slideIn 0.5s ease-out;
   }
 }
+
+.components-container * {
+  font-family: inherit !important;
+}
diff --git a/packages/server/package.json b/packages/server/package.json
index 789ca9c412..37f11f4a4c 100644
--- a/packages/server/package.json
+++ b/packages/server/package.json
@@ -43,6 +43,7 @@
     "@types/node": "^22.9.0",
     "@types/react": "^19",
     "@types/react-dom": "^19",
+    "@types/babel__standalone": "^7.1.9",
     "@tailwindcss/postcss": "^4",
     "tailwindcss": "^4",
     "eslint": "^9",
@@ -54,6 +55,7 @@
   },
   "dependencies": {
     "ai": "^4.2.0",
+    "@babel/standalone": "^7.27.0",
     "@llamaindex/env": "workspace:*",
     "llamaindex": "workspace:*",
     "next": "^15.2.4",
diff --git a/packages/server/src/handlers/chat.ts b/packages/server/src/handlers/chat.ts
index d6bc3ac94b..cdb63112a8 100644
--- a/packages/server/src/handlers/chat.ts
+++ b/packages/server/src/handlers/chat.ts
@@ -10,9 +10,9 @@ import {
 import { runWorkflow } from "../utils/workflow";
 
 export const handleChat = async (
-  workflowFactory: WorkflowFactory,
   req: IncomingMessage,
   res: ServerResponse,
+  workflowFactory: WorkflowFactory,
 ) => {
   try {
     const body = await parseRequestBody(req);
diff --git a/packages/server/src/handlers/components.ts b/packages/server/src/handlers/components.ts
new file mode 100644
index 0000000000..1c3b9e8351
--- /dev/null
+++ b/packages/server/src/handlers/components.ts
@@ -0,0 +1,69 @@
+import fs from "fs";
+import type { IncomingMessage, ServerResponse } from "http";
+import path from "path";
+import { promisify } from "util";
+import { sendJSONResponse } from "../utils/request";
+
+export const getComponents = async (
+  req: IncomingMessage,
+  res: ServerResponse,
+  componentsDir: string,
+) => {
+  try {
+    const exists = await promisify(fs.exists)(componentsDir);
+    if (!exists) {
+      return sendJSONResponse(res, 404, {
+        error: "Components directory not found",
+      });
+    }
+
+    const files = await promisify(fs.readdir)(componentsDir);
+
+    // filter files with valid extensions
+    const validExtensions = [".tsx", ".jsx"];
+    const filteredFiles = files.filter((file) =>
+      validExtensions.includes(path.extname(file)),
+    );
+
+    // filter duplicate components
+    const uniqueFiles = filterDuplicateComponents(filteredFiles);
+
+    const components = await Promise.all(
+      uniqueFiles.map(async (file) => {
+        const filePath = path.join(componentsDir, file);
+        const content = await promisify(fs.readFile)(filePath, "utf-8");
+        return {
+          type: path.basename(file, path.extname(file)),
+          code: content,
+          filename: file,
+        };
+      }),
+    );
+
+    sendJSONResponse(res, 200, components);
+  } catch (error) {
+    console.error("Error reading components:", error);
+    sendJSONResponse(res, 500, { error: "Failed to read components" });
+  }
+};
+
+function filterDuplicateComponents(files: string[]) {
+  const compMap = new Map<string, string>();
+
+  for (const file of files) {
+    const type = path.basename(file, path.extname(file));
+
+    if (compMap.has(type)) {
+      const existingComp = compMap.get(type)!;
+      if (file.endsWith(".tsx") && !existingComp.endsWith(".tsx")) {
+        // prefer .tsx files over others
+        console.warn(`Preferring ${file} over ${existingComp}`);
+        compMap.set(type, file);
+      }
+    } else {
+      compMap.set(type, file);
+    }
+  }
+
+  return Array.from(compMap.values());
+}
diff --git a/packages/server/src/server.ts b/packages/server/src/server.ts
index 61af6327df..f58fa378f0 100644
--- a/packages/server/src/server.ts
+++ b/packages/server/src/server.ts
@@ -4,8 +4,10 @@ import { createServer } from "http";
 import next from "next";
 import path from "path";
 import { parse } from "url";
+import { promisify } from "util";
 import { handleChat } from "./handlers/chat";
 import { getLlamaCloudConfig } from "./handlers/cloud";
+import { getComponents } from "./handlers/components";
 import { handleServeFiles } from "./handlers/files";
 import type { LlamaIndexServerOptions, ServerWorkflow } from "./types";
 
@@ -17,12 +19,18 @@ export class LlamaIndexServer {
   port: number;
   app: ReturnType<typeof next>;
   workflowFactory: () => Promise<ServerWorkflow> | ServerWorkflow;
+  componentsDir?: string | undefined;
 
   constructor(options: LlamaIndexServerOptions) {
     const { workflow, ...nextAppOptions } = options;
     this.app = next({ dev, dir: nextDir, ...nextAppOptions });
     this.port = nextAppOptions.port ?? parseInt(process.env.PORT || "3000", 10);
     this.workflowFactory = workflow;
+    this.componentsDir = options.componentsDir;
+
+    if (this.componentsDir) {
+      this.createComponentsDir(this.componentsDir);
+    }
 
     this.modifyConfig(options);
   }
@@ -33,6 +41,7 @@ export class LlamaIndexServer {
     const llamaCloudApi = getEnv("LLAMA_CLOUD_API_KEY")
       ? "/api/chat/config/llamacloud"
       : undefined;
+    const componentsApi = this.componentsDir ? "/api/components" : undefined;
 
     // content in javascript format
     const content = `
@@ -40,12 +49,20 @@ export class LlamaIndexServer {
         CHAT_API: '/api/chat',
         APP_TITLE: ${JSON.stringify(appTitle)},
         LLAMA_CLOUD_API: ${JSON.stringify(llamaCloudApi)},
-        STARTER_QUESTIONS: ${JSON.stringify(starterQuestions)}
+        STARTER_QUESTIONS: ${JSON.stringify(starterQuestions)},
+        COMPONENTS_API: ${JSON.stringify(componentsApi)}
       }
     `;
     fs.writeFileSync(configFile, content);
   }
 
+  private async createComponentsDir(componentsDir: string) {
+    const exists = await promisify(fs.exists)(componentsDir);
+    if (!exists) {
+      await promisify(fs.mkdir)(componentsDir);
+    }
+  }
+
   async start() {
     await this.app.prepare();
 
@@ -54,13 +71,21 @@ export class LlamaIndexServer {
       const pathname = parsedUrl.pathname;
 
       if (pathname === "/api/chat" && req.method === "POST") {
-        return handleChat(this.workflowFactory, req, res);
+        return handleChat(req, res, this.workflowFactory);
       }
 
       if (pathname?.startsWith("/api/files") && req.method === "GET") {
         return handleServeFiles(req, res, pathname);
       }
 
+      if (
+        this.componentsDir &&
+        pathname === "/api/components" &&
+        req.method === "GET"
+      ) {
+        return getComponents(req, res, this.componentsDir);
+      }
+
       if (
         getEnv("LLAMA_CLOUD_API_KEY") &&
         pathname === "/api/chat/config/llamacloud" &&
diff --git a/packages/server/src/types.ts b/packages/server/src/types.ts
index 8edf1a7aa7..fe797fc533 100644
--- a/packages/server/src/types.ts
+++ b/packages/server/src/types.ts
@@ -28,4 +28,5 @@ export type LlamaIndexServerOptions = NextAppOptions & {
   workflow: WorkflowFactory;
   starterQuestions?: string[];
   appTitle?: string;
+  componentsDir?: string;
 };
diff --git a/packages/server/src/utils/request.ts b/packages/server/src/utils/request.ts
index fc5992b1ba..7e08c20f47 100644
--- a/packages/server/src/utils/request.ts
+++ b/packages/server/src/utils/request.ts
@@ -20,7 +20,7 @@ export async function parseRequestBody(request: IncomingMessage) {
 export function sendJSONResponse(
   response: ServerResponse,
   statusCode: number,
-  body: Record<string, unknown> | string,
+  body: Record<string, unknown> | string | Array<unknown>,
 ) {
   response.statusCode = statusCode;
   response.setHeader("Content-Type", "application/json");
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e33ddb5d81..d5406f3d4b 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1793,6 +1793,9 @@ importers:
 
   packages/server:
     dependencies:
+      '@babel/standalone':
+        specifier: ^7.27.0
+        version: 7.27.0
       '@llamaindex/chat-ui':
         specifier: 0.3.2
         version: 0.3.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@@ -1848,6 +1851,9 @@ importers:
       '@tailwindcss/postcss':
         specifier: ^4
         version: 4.0.9
+      '@types/babel__standalone':
+        specifier: ^7.1.9
+        version: 7.1.9
       '@types/node':
         specifier: ^22.9.0
         version: 22.9.0
@@ -2503,6 +2509,10 @@ packages:
     resolution: {integrity: sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==}
     engines: {node: '>=6.9.0'}
 
+  '@babel/standalone@7.27.0':
+    resolution: {integrity: sha512-UxFDpi+BuSz6Q1X73P3ZSM1CB7Nbbqys+7COi/tdouRuaqRsJ6GAzUyxTswbqItHSItVY3frQdd+paBHHGEk9g==}
+    engines: {node: '>=6.9.0'}
+
   '@babel/template@7.26.8':
     resolution: {integrity: sha512-iNKaX3ZebKIsCvJ+0jd6embf+Aulaa3vNBqZ41kM7iTWjx5qzWKXGHiJUW3+nTpQ18SG11hdF8OAzKrpXkb96Q==}
     engines: {node: '>=6.9.0'}
@@ -5706,6 +5716,9 @@ packages:
   '@types/babel__generator@7.6.8':
     resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==}
 
+  '@types/babel__standalone@7.1.9':
+    resolution: {integrity: sha512-IcCNPLqpevUD7UpV8QB0uwQPOyoOKACFf0YtYWRHcmxcakaje4Q7dbG2+jMqxw/I8Zk0NHvEps66WwS7z/UaaA==}
+
   '@types/babel__template@7.4.4':
     resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==}
 
@@ -14091,6 +14104,8 @@ snapshots:
     dependencies:
       regenerator-runtime: 0.14.1
 
+  '@babel/standalone@7.27.0': {}
+
   '@babel/template@7.26.8':
     dependencies:
       '@babel/code-frame': 7.26.2
@@ -17545,6 +17560,15 @@ snapshots:
     dependencies:
       '@babel/types': 7.26.8
 
+  '@types/babel__standalone@7.1.9':
+    dependencies:
+      '@babel/parser': 7.26.8
+      '@babel/types': 7.26.8
+      '@types/babel__core': 7.20.5
+      '@types/babel__generator': 7.6.8
+      '@types/babel__template': 7.4.4
+      '@types/babel__traverse': 7.20.6
+
   '@types/babel__template@7.4.4':
     dependencies:
       '@babel/parser': 7.26.8


Reviews:
user: login: leehuwuj
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-08T06:40:03Z
user: login: thucpn
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-08T07:14:20Z
user: login: thucpn
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-08T08:14:18Z
user: login: marcusschiesser
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T09:12:08Z
user: login: thucpn
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-08T09:16:00Z
user: login: thucpn
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-08T11:10:32Z
user: login: thucpn
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-08T11:37:21Z
user: login: marcusschiesser
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T11:53:42Z
user: login: marcusschiesser
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T11:54:56Z
user: login: marcusschiesser
body: .
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T11:55:48Z
user: login: thucpn
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-08T12:03:40Z
user: login: marcusschiesser
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T12:37:25Z
user: login: leehuwuj
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-09T04:23:55Z
user: login: thucpn
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-09T04:28:33Z
user: login: marcusschiesser
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T07:49:21Z
user: login: marcusschiesser
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T07:56:13Z
user: login: thucpn
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-09T09:26:52Z
user: login: thucpn
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-09T09:30:03Z
user: login: leehuwuj
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-09T10:15:12Z
user: login: marcusschiesser
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T10:21:28Z

Review comments:
diff_hunk: @@ -0,0 +1,75 @@
+"use client";
+
+import {
+  getChatUIAnnotation,
+  JSONValue,
+  useChatMessage,
+} from "@llamaindex/chat-ui";
+import React from "react";
+
+type ComponentDef = {
+  type: string;
+  code: string;
+};
+
+type EventComponent = ComponentDef & {
+  events: JSONValue[];
+};
+
+export const DynamicEvents = () => {
+  const { message } = useChatMessage();
+  const [componentDefs, setComponentDefs] = React.useState<ComponentDef[]>([]);
+
+  React.useEffect(() => {
+    fetchComponentDefinitions().then(setComponentDefs);
+  }, []);
+
+  const components: EventComponent[] = componentDefs.map((comp) => {
+    return {
+      ...comp,
+      events: getChatUIAnnotation(
+        message.annotations,
+        comp.type,
+      ) as JSONValue[],
+    };
+  });
+
+  if (components.length === 0) return null;
+
+  return (
+    <div className="components-container">
+      {components.map((component, index) => {
+        return (
+          <React.Fragment key={`${component.type}-${index}`}>
+            {renderEventComponent(component)}
+          </React.Fragment>
+        );
+      })}
+    </div>
+  );
+};
+
+async function fetchComponentDefinitions() {
+  try {
+    const response = await fetch("/api/components");
+    const componentsJson = await response.json();
+    return componentsJson.data as ComponentDef[];
path: packages/server/next/app/components/ui/chat/dynamic-events.tsx
user: login: leehuwuj
body: It's `/api/components` so why not return as list `ComponentDef[]` by default?
created_at: 2025-04-08T06:40:03Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,75 @@
+"use client";
+
+import {
+  getChatUIAnnotation,
+  JSONValue,
+  useChatMessage,
+} from "@llamaindex/chat-ui";
+import React from "react";
+
+type ComponentDef = {
+  type: string;
+  code: string;
+};
+
+type EventComponent = ComponentDef & {
+  events: JSONValue[];
+};
+
+export const DynamicEvents = () => {
+  const { message } = useChatMessage();
+  const [componentDefs, setComponentDefs] = React.useState<ComponentDef[]>([]);
+
+  React.useEffect(() => {
+    fetchComponentDefinitions().then(setComponentDefs);
+  }, []);
+
+  const components: EventComponent[] = componentDefs.map((comp) => {
+    return {
+      ...comp,
+      events: getChatUIAnnotation(
+        message.annotations,
+        comp.type,
+      ) as JSONValue[],
+    };
+  });
+
+  if (components.length === 0) return null;
+
+  return (
+    <div className="components-container">
+      {components.map((component, index) => {
+        return (
+          <React.Fragment key={`${component.type}-${index}`}>
+            {renderEventComponent(component)}
+          </React.Fragment>
+        );
+      })}
+    </div>
+  );
+};
+
+async function fetchComponentDefinitions() {
+  try {
+    const response = await fetch("/api/components");
+    const componentsJson = await response.json();
+    return componentsJson.data as ComponentDef[];
path: packages/server/next/app/components/ui/chat/dynamic-events.tsx
user: login: thucpn
body: I updated to return a list
created_at: 2025-04-08T07:14:20Z
author_association: COLLABORATOR
diff_hunk: @@ -4,12 +4,23 @@ import { ChatSection as ChatSectionUI } from "@llamaindex/chat-ui";
 import "@llamaindex/chat-ui/styles/markdown.css";
 import "@llamaindex/chat-ui/styles/pdf.css";
 import { useChat } from "ai/react";
+import { useEffect, useState } from "react";
 import Header from "./header";
 import CustomChatInput from "./ui/chat/chat-input";
 import CustomChatMessages from "./ui/chat/chat-messages";
+import {
+  ComponentDef,
+  fetchComponentDefinitions,
+} from "./ui/chat/dynamic-events";
 import { getConfig } from "./ui/lib/utils";
 
 export default function ChatSection() {
+  const [componentDefs, setComponentDefs] = useState<ComponentDef[]>([]);
+
+  useEffect(() => {
+    fetchComponentDefinitions().then(setComponentDefs);
+  }, []);
path: packages/server/next/app/components/chat-section.tsx
user: login: thucpn
body: Fetch component definitions and use babel to transform from jsx to js.
This is triggered once the UI is init
created_at: 2025-04-08T08:14:18Z
author_association: COLLABORATOR
diff_hunk: @@ -1,17 +1,21 @@
 "use client";
 
 import { ChatMessage } from "@llamaindex/chat-ui";
-import { DeepResearchCard } from "./custom/deep-research-card";
+import { ComponentDef, DynamicEvents } from "./dynamic-events";
path: packages/server/next/app/components/ui/chat/chat-message-content.tsx
user: login: marcusschiesser
body: need to remove custom/deep-research-card code and later ensure that create-llama generates its `ComponentDef` for deep research use case
created_at: 2025-04-08T08:58:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,101 @@
+"use client";
+
+import * as Babel from "@babel/standalone"; // Import Babel standalone for runtime transpilation
+import {
+  getChatUIAnnotation,
+  JSONValue,
+  useChatMessage,
+} from "@llamaindex/chat-ui";
+import React from "react";
+
+export type ComponentDef = {
+  type: string;
+  code: string;
+};
+
+type EventComponent = ComponentDef & {
+  events: JSONValue[];
+};
+
+export const DynamicEvents = ({
+  componentDefs,
+}: {
+  componentDefs: ComponentDef[];
+}) => {
+  const {
+    message: { annotations },
+  } = useChatMessage();
+
+  const components: EventComponent[] = componentDefs
+    .map((comp) => {
+      const events = getChatUIAnnotation(annotations, comp.type) as JSONValue[]; // get all event data by type
+      if (!events?.length) return null;
+      return { ...comp, events };
+    })
+    .filter((comp) => comp !== null);
+
+  if (components.length === 0) return null;
+
+  return (
+    <div className="components-container">
+      {components.map((component, index) => {
+        return (
+          <React.Fragment key={`${component.type}-${index}`}>
+            {renderEventComponent(component)}
+          </React.Fragment>
+        );
+      })}
+    </div>
+  );
+};
+
+export async function fetchComponentDefinitions(): Promise<ComponentDef[]> {
+  try {
+    const response = await fetch("/api/components");
path: packages/server/next/app/components/ui/chat/dynamic-events.tsx
user: login: marcusschiesser
body: baseurl?
created_at: 2025-04-08T08:59:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -17,12 +18,14 @@ export class LlamaIndexServer {
   port: number;
   app: ReturnType<typeof next>;
   workflowFactory: () => Promise<ServerWorkflow> | ServerWorkflow;
+  componentsDir: string;
 
   constructor(options: LlamaIndexServerOptions) {
     const { workflow, ...nextAppOptions } = options;
     this.app = next({ dev, dir: nextDir, ...nextAppOptions });
     this.port = nextAppOptions.port ?? parseInt(process.env.PORT || "3000", 10);
     this.workflowFactory = workflow;
+    this.componentsDir = options.componentsDir ?? "output/components";
path: packages/server/src/server.ts
user: login: marcusschiesser
body: please ensure that it also works if `output/components` folder doesn't exist
created_at: 2025-04-08T09:09:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -158,6 +162,8 @@ function appendEventDataToAnnotations(
     downloadLlamaCloudFilesFromNodes(sourceNodes); // download files in background
   }
 
+  // TODO: trigger generate component code and save to componentsDir
path: packages/server/src/utils/workflow.ts
user: login: marcusschiesser
body: this will add a lot of latency, we will trigger generating the component code in a own function in the generate.ts script
created_at: 2025-04-08T09:11:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -158,6 +162,8 @@ function appendEventDataToAnnotations(
     downloadLlamaCloudFilesFromNodes(sourceNodes); // download files in background
   }
 
+  // TODO: trigger generate component code and save to componentsDir
path: packages/server/src/utils/workflow.ts
user: login: marcusschiesser
body: the server should just log a warning if the component doesn't exist and inform the user to generate a component in the componentDir
created_at: 2025-04-08T09:12:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,101 @@
+"use client";
+
+import * as Babel from "@babel/standalone"; // Import Babel standalone for runtime transpilation
+import {
+  getChatUIAnnotation,
+  JSONValue,
+  useChatMessage,
+} from "@llamaindex/chat-ui";
+import React from "react";
+
+export type ComponentDef = {
+  type: string;
+  code: string;
+};
+
+type EventComponent = ComponentDef & {
+  events: JSONValue[];
+};
+
+export const DynamicEvents = ({
+  componentDefs,
+}: {
+  componentDefs: ComponentDef[];
+}) => {
+  const {
+    message: { annotations },
+  } = useChatMessage();
+
+  const components: EventComponent[] = componentDefs
+    .map((comp) => {
+      const events = getChatUIAnnotation(annotations, comp.type) as JSONValue[]; // get all event data by type
+      if (!events?.length) return null;
+      return { ...comp, events };
+    })
+    .filter((comp) => comp !== null);
+
+  if (components.length === 0) return null;
+
+  return (
+    <div className="components-container">
+      {components.map((component, index) => {
+        return (
+          <React.Fragment key={`${component.type}-${index}`}>
+            {renderEventComponent(component)}
+          </React.Fragment>
+        );
+      })}
+    </div>
+  );
+};
+
+export async function fetchComponentDefinitions(): Promise<ComponentDef[]> {
+  try {
+    const response = await fetch("/api/components");
path: packages/server/next/app/components/ui/chat/dynamic-events.tsx
user: login: thucpn
body: Currently nextjs frontend doesn't use baseUrl. It's always use / for all current apis
created_at: 2025-04-08T09:16:00Z
author_association: COLLABORATOR
diff_hunk: @@ -211,3 +211,7 @@
     animation: slideIn 0.5s ease-out;
   }
 }
+
+.components-container * {
+  font-family: inherit !important;
+}
path: packages/server/next/app/globals.css
user: login: thucpn
body: make generated components using font family from root
created_at: 2025-04-08T11:10:31Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,38 @@
+import fs from "fs";
+import type { IncomingMessage, ServerResponse } from "http";
+import path from "path";
+import { promisify } from "util";
+import { sendJSONResponse } from "../utils/request";
+
+export const getComponents = async (
+  req: IncomingMessage,
+  res: ServerResponse,
+  componentsDir: string,
+) => {
+  try {
+    const exists = await promisify(fs.exists)(componentsDir);
+    if (!exists) {
+      return sendJSONResponse(res, 404, {
+        error: "Components directory not found",
+      });
+    }
+
+    const files = await promisify(fs.readdir)(componentsDir);
+    const components = await Promise.all(
+      files.map(async (file) => {
+        const filePath = path.join(componentsDir, file);
+        const content = await promisify(fs.readFile)(filePath, "utf-8");
+        return {
+          type: path.basename(file, path.extname(file)), // use file name as component type
+          code: content,
+          filename: file,
path: packages/server/src/handlers/components.ts
user: login: thucpn
body: need response `filename` for Babel and tsx prefer @leehuwuj 
created_at: 2025-04-08T11:37:21Z
author_association: COLLABORATOR
diff_hunk: @@ -17,12 +18,14 @@ export class LlamaIndexServer {
   port: number;
   app: ReturnType<typeof next>;
   workflowFactory: () => Promise<ServerWorkflow> | ServerWorkflow;
+  componentsDir: string;
 
   constructor(options: LlamaIndexServerOptions) {
     const { workflow, ...nextAppOptions } = options;
     this.app = next({ dev, dir: nextDir, ...nextAppOptions });
     this.port = nextAppOptions.port ?? parseInt(process.env.PORT || "3000", 10);
     this.workflowFactory = workflow;
+    this.componentsDir = options.componentsDir ?? "output/components";
path: packages/server/src/server.ts
user: login: marcusschiesser
body: i think we shouldn't create the componentsdir if it doesn't exist
created_at: 2025-04-08T11:53:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -17,12 +18,14 @@ export class LlamaIndexServer {
   port: number;
   app: ReturnType<typeof next>;
   workflowFactory: () => Promise<ServerWorkflow> | ServerWorkflow;
+  componentsDir: string;
 
   constructor(options: LlamaIndexServerOptions) {
     const { workflow, ...nextAppOptions } = options;
     this.app = next({ dev, dir: nextDir, ...nextAppOptions });
     this.port = nextAppOptions.port ?? parseInt(process.env.PORT || "3000", 10);
     this.workflowFactory = workflow;
+    this.componentsDir = options.componentsDir ?? "output/components";
path: packages/server/src/server.ts
user: login: marcusschiesser
body: it should be like this:
1. componentsDir not specified - don't use the feature, don't create a folder
2. componentsDir specified - use the feature and create folder if it doesn't exist
created_at: 2025-04-08T11:54:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -17,12 +18,14 @@ export class LlamaIndexServer {
   port: number;
   app: ReturnType<typeof next>;
   workflowFactory: () => Promise<ServerWorkflow> | ServerWorkflow;
+  componentsDir: string;
 
   constructor(options: LlamaIndexServerOptions) {
     const { workflow, ...nextAppOptions } = options;
     this.app = next({ dev, dir: nextDir, ...nextAppOptions });
     this.port = nextAppOptions.port ?? parseInt(process.env.PORT || "3000", 10);
     this.workflowFactory = workflow;
+    this.componentsDir = options.componentsDir ?? "output/components";
path: packages/server/src/server.ts
user: login: thucpn
body: > 1. componentsDir not specified - don't use the feature, don't create a folder

Should we need to show a warning in this case? If user uses DeepResearchWorkflow but he doesn't specify componentsDir , DeepResearchEvent won't be shown.

created_at: 2025-04-08T12:03:40Z
author_association: COLLABORATOR
diff_hunk: @@ -17,12 +18,14 @@ export class LlamaIndexServer {
   port: number;
   app: ReturnType<typeof next>;
   workflowFactory: () => Promise<ServerWorkflow> | ServerWorkflow;
+  componentsDir: string;
 
   constructor(options: LlamaIndexServerOptions) {
     const { workflow, ...nextAppOptions } = options;
     this.app = next({ dev, dir: nextDir, ...nextAppOptions });
     this.port = nextAppOptions.port ?? parseInt(process.env.PORT || "3000", 10);
     this.workflowFactory = workflow;
+    this.componentsDir = options.componentsDir ?? "output/components";
path: packages/server/src/server.ts
user: login: marcusschiesser
body: Yes if we can distinguish frontend events and we don't have a component then a warning makes sense also without componentDir specified. 
created_at: 2025-04-08T12:37:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,38 @@
+import fs from "fs";
+import type { IncomingMessage, ServerResponse } from "http";
+import path from "path";
+import { promisify } from "util";
+import { sendJSONResponse } from "../utils/request";
+
+export const getComponents = async (
+  req: IncomingMessage,
+  res: ServerResponse,
+  componentsDir: string,
+) => {
+  try {
+    const exists = await promisify(fs.exists)(componentsDir);
+    if (!exists) {
+      return sendJSONResponse(res, 404, {
+        error: "Components directory not found",
+      });
+    }
+
+    const files = await promisify(fs.readdir)(componentsDir);
+    const components = await Promise.all(
path: packages/server/src/handlers/components.ts
user: login: leehuwuj
body: Do we have a file extension filter here? For example, only load the .jsx and .tsx files.
created_at: 2025-04-09T04:23:54Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,38 @@
+import fs from "fs";
+import type { IncomingMessage, ServerResponse } from "http";
+import path from "path";
+import { promisify } from "util";
+import { sendJSONResponse } from "../utils/request";
+
+export const getComponents = async (
+  req: IncomingMessage,
+  res: ServerResponse,
+  componentsDir: string,
+) => {
+  try {
+    const exists = await promisify(fs.exists)(componentsDir);
+    if (!exists) {
+      return sendJSONResponse(res, 404, {
+        error: "Components directory not found",
+      });
+    }
+
+    const files = await promisify(fs.readdir)(componentsDir);
+    const components = await Promise.all(
path: packages/server/src/handlers/components.ts
user: login: thucpn
body: great catch! I just updated
created_at: 2025-04-09T04:28:32Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,138 @@
+"use client";
+
+import * as Babel from "@babel/standalone"; // Import Babel standalone for runtime transpilation
+import {
+  getChatUIAnnotation,
+  JSONValue,
+  useChatMessage,
+} from "@llamaindex/chat-ui";
+import React from "react";
+import { getConfig } from "../lib/utils";
+
+export type ComponentDef = {
+  type: string; // eg. deep_research_event
+  code: string; // eg. export const DeepResearchEvent = () => {...}
+  filename: string; // eg. deep_research_event.tsx
+};
+
+type EventComponent = ComponentDef & {
+  events: JSONValue[];
+};
+
+export const DynamicEvents = ({
+  componentDefs,
+}: {
+  componentDefs: ComponentDef[];
+}) => {
+  const {
+    message: { annotations },
+  } = useChatMessage();
+
+  const components: EventComponent[] = componentDefs
+    .map((comp) => {
+      const events = getChatUIAnnotation(annotations, comp.type) as JSONValue[]; // get all event data by type
+      if (!events?.length) return null;
+      return { ...comp, events };
+    })
+    .filter((comp) => comp !== null);
+
+  if (components.length === 0) return null;
+
+  return (
+    <div className="components-container">
+      {components.map((component, index) => {
+        return (
+          <React.Fragment key={`${component.type}-${index}`}>
+            {renderEventComponent(component)}
+          </React.Fragment>
+        );
+      })}
+    </div>
+  );
+};
+
+export async function fetchComponentDefinitions(): Promise<ComponentDef[]> {
+  const endpoint = getConfig("COMPONENTS_API");
+  if (!endpoint) return [];
+
+  try {
+    const response = await fetch(endpoint);
+    const componentsJson = await response.json();
+    const rawComponents = componentsJson as ComponentDef[];
+
+    // Check for duplicate component types
+    const componentTypeMap = new Map<string, ComponentDef>();
+
+    rawComponents.forEach((comp) => {
path: packages/server/next/app/components/ui/chat/dynamic-events.tsx
user: login: marcusschiesser
body: why not filtering duplicates in the backend?
created_at: 2025-04-09T07:40:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -158,9 +164,51 @@ function appendEventDataToAnnotations(
     downloadLlamaCloudFilesFromNodes(sourceNodes); // download files in background
   }
 
+  checkComponentAvailability(transformedEvent, componentsDir);
+
   dataStream.appendMessageAnnotation(transformedEvent.data as JSONValue);
 }
 
+function checkComponentAvailability(
path: packages/server/src/utils/workflow.ts
user: login: marcusschiesser
body: better move this concern to packages/server/src/handlers/components.ts?
created_at: 2025-04-09T07:49:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -158,9 +164,51 @@ function appendEventDataToAnnotations(
     downloadLlamaCloudFilesFromNodes(sourceNodes); // download files in background
   }
 
+  checkComponentAvailability(transformedEvent, componentsDir);
+
   dataStream.appendMessageAnnotation(transformedEvent.data as JSONValue);
 }
 
+function checkComponentAvailability(
path: packages/server/src/utils/workflow.ts
user: login: marcusschiesser
body: we could also move these warning logs to the FE as the frontend knows that `"sources", "agent"` are events with special renderers
created_at: 2025-04-09T07:56:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,147 @@
+"use client";
+
+import * as Babel from "@babel/standalone"; // Import Babel standalone for runtime transpilation
+import {
+  getChatUIAnnotation,
+  JSONValue,
+  MessageAnnotation,
+  MessageAnnotationType,
+  useChatMessage,
+} from "@llamaindex/chat-ui";
+import React, { useEffect, useRef } from "react";
+import { getConfig } from "../lib/utils";
+
+export type ComponentDef = {
+  type: string; // eg. deep_research_event
+  code: string; // eg. export const DeepResearchEvent = () => {...}
+  filename: string; // eg. deep_research_event.tsx
+};
+
+type EventComponent = ComponentDef & {
+  events: JSONValue[];
+};
+
+// image, document_file, sources, events, suggested_questions, agent
+const BUILT_IN_CHATUI_COMPONENTS = Object.values(MessageAnnotationType);
+
+export const DynamicEvents = ({
+  componentDefs,
+}: {
+  componentDefs: ComponentDef[];
+}) => {
+  const {
+    message: { annotations },
+  } = useChatMessage();
+
+  const shownWarningsRef = useRef<Set<string>>(new Set()); // track warnings
path: packages/server/next/app/components/ui/chat/dynamic-events.tsx
user: login: thucpn
body: the warning for each type should be shown once for each message
created_at: 2025-04-09T09:26:51Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,147 @@
+"use client";
+
+import * as Babel from "@babel/standalone"; // Import Babel standalone for runtime transpilation
+import {
+  getChatUIAnnotation,
+  JSONValue,
+  MessageAnnotation,
+  MessageAnnotationType,
+  useChatMessage,
+} from "@llamaindex/chat-ui";
+import React, { useEffect, useRef } from "react";
+import { getConfig } from "../lib/utils";
+
+export type ComponentDef = {
+  type: string; // eg. deep_research_event
+  code: string; // eg. export const DeepResearchEvent = () => {...}
+  filename: string; // eg. deep_research_event.tsx
+};
+
+type EventComponent = ComponentDef & {
+  events: JSONValue[];
+};
+
+// image, document_file, sources, events, suggested_questions, agent
+const BUILT_IN_CHATUI_COMPONENTS = Object.values(MessageAnnotationType);
path: packages/server/next/app/components/ui/chat/dynamic-events.tsx
user: login: thucpn
body: I forgot we have this enum in chat-ui, very convenient to check build-in components

```
enum MessageAnnotationType {
    IMAGE = "image",
    DOCUMENT_FILE = "document_file",
    SOURCES = "sources",
    EVENTS = "events",
    SUGGESTED_QUESTIONS = "suggested_questions",
    AGENT_EVENTS = "agent"
}
```
created_at: 2025-04-09T09:30:03Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,159 @@
+---
+title: Using LlamaIndex Server
+description: Running LlamaIndex workflows with both API endpoints and a user interface for interaction
+---
+
+import { Tab, Tabs } from "fumadocs-ui/components/tabs";
+
+# LlamaIndex Server
+
+LlamaIndexServer is a Next.js-based application that allows you to quickly launch your [LlamaIndex Workflows](https://ts.llamaindex.ai/docs/llamaindex/modules/agents/workflows) and [Agent Workflows](https://ts.llamaindex.ai/docs/llamaindex/modules/agents/agent_workflow) as an API server with an optional chat UI. It provides a complete environment for running LlamaIndex workflows with both API endpoints and a user interface for interaction.
+
+## Features
+
+- Serving a workflow as a chatbot
+- Built on Next.js for high performance and easy API development
+- Optional built-in chat UI with extendable UI components
+- Prebuilt development code
+
+## Installation
+
+<Tabs groupId="install" items={["npm", "yarn", "pnpm"]} persist>
+	```shell tab="npm"
+	npm install @llamaindex/server
+	```
+
+	```shell tab="yarn"
+	yarn add @llamaindex/server
+	```
+
+	```shell tab="pnpm"
+	pnpm add @llamaindex/server
+	```
+</Tabs>
+
+## Quick Start
+
+Create index.ts file and add the following code:
+
+```ts
+import { LlamaIndexServer } from "@llamaindex/server";
+import { wiki } from "@llamaindex/tools"; // or any other tool
+
+const createWorkflow = () => agent({ tools: [wiki()] })
+
+new LlamaIndexServer({
+  workflow: createWorkflow,
+  appTitle: "LlamaIndex App",
+  starterQuestions: ["Who is the first president of the United States?"],
+}).start();
+```
+
+## Running the Server
+
+In the same directory as `index.ts`, run the following command to start the server:
+
+  ```bash
+  tsx index.ts
+  ```
+The server will start at `http://localhost:3000`
+
+You can also make a request to the server:
+
+  ```bash
+  curl -X POST "http://localhost:3000/api/chat" -H "Content-Type: application/json" -d '{"message": "Who is the first president of the United States?"}'
+  ```
+
+## Configuration Options
+
+The LlamaIndexServer accepts the following configuration
+
+- `workflow`: A callable function that creates a workflow instance for each request
+- `starterQuestions`: List of starter questions for the chat UI
+- `appTitle`: The title of the application
+- `componentsDir`: The directory for custom UI components rendering events emitted by the workflow. The default is undefined, which does not render custom UI components.
+
+LlamaIndexServer accepts all the configuration options from Nextjs Custom Server such as `port`, `hostname`, `dev`, etc.
+See all Nextjs Custom Server options [here](https://nextjs.org/docs/app/building-your-application/configuring/custom-server).
+
+## Default Endpoints and Features
+
+### Chat Endpoint
+
+The server includes a default chat endpoint at `/api/chat` for handling chat interactions.
+
+### Chat UI
+
+The server always provides a chat interface at the root path (`/`) with:
+
+- Configurable starter questions
+- Real-time chat interface
+- API endpoint integration
+
+### Static File Serving
+
+- The server automatically mounts the `data` and `output` folders at `{server_url}{api_prefix}/files/data` (default: `/api/files/data`) and `{server_url}{api_prefix}/files/output` (default: `/api/files/output`) respectively.
+- Your workflows can use both folders to store and access files. As a convention, the `data` folder is used for documents that are ingested and the `output` folder is used for documents that are generated by the workflow.
+
+
+## Custom UI Components
+
+The LlamaIndex server provides support for rendering workflow events using custom UI components, allowing you to extend and customize the chat interface.
+
+### Overview
+
+Custom UI components are a powerful feature that enables you to:
+
+- Add custom interface elements to the chat UI using React JSX or TSX files
+- Extend the default chat interface functionality
+- Create specialized visualizations or interactions
+
+### Configuration
+
+Your workflow must emit events that fit this structure, allowing the LlamaIndex server to display the right UI components based on the event type.
+
+```json
+{
+    "type": "<event_name>",
+    "data": <data model>
+}
+```
+
+### Server Setup
+
+1. Initialize the LlamaIndex server with a component directory:
+
+```ts
+new LlamaIndexServer({
+  workflow: workflowFactory,
+  appTitle: "LlamaIndex App",
+  componentsDir: "output/components",
path: apps/next/src/content/docs/llamaindex/modules/chat/llamaindex-server.mdx
user: login: leehuwuj
body: "component"
created_at: 2025-04-09T10:15:12Z
author_association: COLLABORATOR

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-04-08T04:42:13Z
body: ###  🦋  Changeset detected

Latest commit: ea304b4b169bbcc4f0f73f4c5e65bc6686968b16

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 1 package</summary>

  | Name               | Type  |
| ------------------ | ----- |
| @llamaindex/server | Patch |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/run-llama/LlamaIndexTS/new/tp/component-dir-in-llamaindex-server?filename=.changeset/grumpy-otters-fold.md&value=---%0A%22%40llamaindex%2Fdoc%22%3A%20patch%0A%22%40llamaindex%2Fserver%22%3A%20patch%0A---%0A%0Afeat%3A%20component%20dir%20in%20llamaindex%20server%0A)


author_association: NONE
user: login: vercel[bot]
created_at: 2025-04-08T04:42:14Z
body: [vc]: #Datsodqu0lvCznKV12zqlTjjgPlHVxCcHa+4z5MnHlI=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJsbGFtYWluZGV4LXRzLWRvYyIsImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9sbGFtYS1pbmRleC9sbGFtYWluZGV4LXRzLWRvYy80Mm9SN1VzYTZ3dTZqY1oyaFRZeEJUVGhuZ1Z3IiwicHJldmlld1VybCI6ImxsYW1haW5kZXgtdHMtZG9jLWdpdC10cC1jb21wb25lbnQtZGlyLWluLWxsYW1haW5kZXgtc2VydmVyLnByLnN0YWdpbmcubGxhbWFpbmRleC5haSIsIm5leHRDb21taXRTdGF0dXMiOiJERVBMT1lFRCIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJsbGFtYWluZGV4LXRzLWRvYy1naXQtdHAtY29tcG9uZW50LWRpci1pbi1sbGFtYWluZGV4LXNlcnZlci5wci5zdGFnaW5nLmxsYW1haW5kZXguYWkifSwicm9vdERpcmVjdG9yeSI6ImFwcHMvbmV4dCJ9XX0=
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **llamaindex-ts-doc** | ✅ Ready ([Inspect](https://vercel.com/llama-index/llamaindex-ts-doc/42oR7Usa6wu6jcZ2hTYxBTThngVw)) | [Visit Preview](https://llamaindex-ts-doc-git-tp-component-dir-in-llamaindex-server.pr.staging.llamaindex.ai) | 💬 [**Add feedback**](https://vercel.live/open-feedback/llamaindex-ts-doc-git-tp-component-dir-in-llamaindex-server.pr.staging.llamaindex.ai?via=pr-comment-feedback-link) | Apr 9, 2025 10:54am |


author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-08T04:44:24Z
body: ## size-limit report 📦 
| Path                         | Size           | Loading time (3g) | Running time (snapdragon) | Total time |
| ---------------------------- | -------------- | ----------------- | ------------------------- | ---------- |
| dist/LlamaIndexImportTest.js | 517.11 KB (0%) | 10.4 s (0%)       | 12.9 s (+1.66% 🔺)        | 23.2 s     |
author_association: CONTRIBUTOR
user: login: pkg-pr-new[bot]
created_at: 2025-04-08T04:47:32Z
body: 
[Open in StackBlitz](https://pkg.pr.new/template/b6040560-f642-4cc1-9e3f-014a26cddb69)


<details><summary><b>@llamaindex/autotool</b></summary><p>

```
npm i https://pkg.pr.new/run-llama/LlamaIndexTS/@llamaindex/autotool@1828
```

</p></details>
      
    

<details><summary><b>@llamaindex/cloud</b></summary><p>

```
npm i https://pkg.pr.new/run-llama/LlamaIndexTS/@llamaindex/cloud@1828
```

</p></details>
      
    

<details><summary><b>@llamaindex/community</b></summary><p>

```
npm i https://pkg.pr.new/run-llama/LlamaIndexTS/@llamaindex/community@1828
```

</p></details>
      
    

<details><summary><b>@llamaindex/core</b></summary><p>

```
npm i https://pkg.pr.new/run-llama/LlamaIndexTS/@llamaindex/core@1828
```

</p></details>
      
    

<details><summary><b>@llamaindex/env</b></summary><p>

```
npm i https://pkg.pr.new/run-llama/LlamaIndexTS/@llamaindex/env@1828
```

</p></details>
      
    

<details><summary><b>@llamaindex/experimental</b></summary><p>

```
npm i https://pkg.pr.new/run-llama/LlamaIndexTS/@llamaindex/experimental@1828
```

</p></details>
      
    

<details><summary><b>llamaindex</b></summary><p>

```
npm i https://pkg.pr.new/run-llama/LlamaIndexTS/llamaindex@1828
```

</p></details>
      
    

<details><summary><b>@llamaindex/node-parser</b></summary><p>

```
npm i https://pkg.pr.new/run-llama/LlamaIndexTS/@llamaindex/node-parser@1828
```

</p></details>
      
    

<details><summary><b>@llamaindex/readers</b></summary><p>

```
npm i https://pkg.pr.new/run-llama/LlamaIndexTS/@llamaindex/readers@1828
```

</p></details>
      
    

<details><summary><b>@llamaindex/server</b></summary><p>

```
npm i https://pkg.pr.new/run-llama/LlamaIndexTS/@llamaindex/server@1828
```

</p></details>
      
    

<details><summary><b>@llamaindex/tools</b></summary><p>

```
npm i https://pkg.pr.new/run-llama/LlamaIndexTS/@llamaindex/tools@1828
```

</p></details>
      
    

<details><summary><b>@llamaindex/wasm-tools</b></summary><p>

```
npm i https://pkg.pr.new/run-llama/LlamaIndexTS/@llamaindex/wasm-tools@1828
```

</p></details>
      
    

<details><summary><b>@llamaindex/workflow</b></summary><p>

```
npm i https://pkg.pr.new/run-llama/LlamaIndexTS/@llamaindex/workflow@1828
```

</p></details>
      
    

<details><summary><b>@llamaindex/anthropic</b></summary><p>

```
npm i https://pkg.pr.new/run-llama/LlamaIndexTS/@llamaindex/anthropic@1828
```

</p></details>
      
    

<details><summary><b>@llamaindex/clip</b></summary><p>

```
npm i https://pkg.pr.new/run-llama/LlamaIndexTS/@llamaindex/clip@1828
```

</p></details>
      
    

<details><summary><b>@llamaindex/cohere</b></summary><p>

```
npm i https://pkg.pr.new/run-llama/LlamaIndexTS/@llamaindex/cohere@1828
```

</p></details>
      
    

<details><summary><b>@llamaindex/deepinfra</b></summary><p>

```
npm i https://pkg.pr.new/run-llama/LlamaIndexTS/@llamaindex/deepinfra@1828
```

</p></details>
      
    

<details><summary><b>@llamaindex/deepseek</b></summary><p>

```
npm i https://pkg.pr.new/run-llama/LlamaIndexTS/@llamaindex/deepseek@1828
```

</p></details>
      
    

<details><summary><b>@llamaindex/fireworks</b></summary><p>

```
npm i https://pkg.pr.new/run-llama/LlamaIndexTS/@llamaindex/fireworks@1828
```

</p></details>
      
    

<details><summary><b>@llamaindex/google</b></summary><p>

```
npm i https://pkg.pr.new/run-llama/LlamaIndexTS/@llamaindex/google@1828
```

</p></details>
      
    

<details><summary><b>@llamaindex/groq</b></summary><p>

```
npm i https://pkg.pr.new/run-llama/LlamaIndexTS/@llamaindex/groq@1828
```

</p></details>
      
    

<details><summary><b>@llamaindex/huggingface</b></summary><p>

```
npm i https://pkg.pr.new/run-llama/LlamaIndexTS/@llamaindex/huggingface@1828
```

</p></details>
      
    

<details><summary><b>@llamaindex/jinaai</b></summary><p>

```
npm i https://pkg.pr.new/run-llama/LlamaIndexTS/@llamaindex/jinaai@1828
```

</p></details>
      
    

<details><summary><b>@llamaindex/mistral</b></summary><p>

```
npm i https://pkg.pr.new/run-llama/LlamaIndexTS/@llamaindex/mistral@1828
```

</p></details>
      
    

<details><summary><b>@llamaindex/mixedbread</b></summary><p>

```
npm i https://pkg.pr.new/run-llama/LlamaIndexTS/@llamaindex/mixedbread@1828
```

</p></details>
      
    

<details><summary><b>@llamaindex/ollama</b></summary><p>

```
npm i https://pkg.pr.new/run-llama/LlamaIndexTS/@llamaindex/ollama@1828
```

</p></details>
      
    

<details><summary><b>@llamaindex/openai</b></summary><p>

```
npm i https://pkg.pr.new/run-llama/LlamaIndexTS/@llamaindex/openai@1828
```

</p></details>
      
    

<details><summary><b>@llamaindex/perplexity</b></summary><p>

```
npm i https://pkg.pr.new/run-llama/LlamaIndexTS/@llamaindex/perplexity@1828
```

</p></details>
      
    

<details><summary><b>@llamaindex/portkey-ai</b></summary><p>

```
npm i https://pkg.pr.new/run-llama/LlamaIndexTS/@llamaindex/portkey-ai@1828
```

</p></details>
      
    

<details><summary><b>@llamaindex/replicate</b></summary><p>

```
npm i https://pkg.pr.new/run-llama/LlamaIndexTS/@llamaindex/replicate@1828
```

</p></details>
      
    

<details><summary><b>@llamaindex/together</b></summary><p>

```
npm i https://pkg.pr.new/run-llama/LlamaIndexTS/@llamaindex/together@1828
```

</p></details>
      
    

<details><summary><b>@llamaindex/vercel</b></summary><p>

```
npm i https://pkg.pr.new/run-llama/LlamaIndexTS/@llamaindex/vercel@1828
```

</p></details>
      
    

<details><summary><b>@llamaindex/vllm</b></summary><p>

```
npm i https://pkg.pr.new/run-llama/LlamaIndexTS/@llamaindex/vllm@1828
```

</p></details>
      
    

<details><summary><b>@llamaindex/voyage-ai</b></summary><p>

```
npm i https://pkg.pr.new/run-llama/LlamaIndexTS/@llamaindex/voyage-ai@1828
```

</p></details>
      
    

<details><summary><b>@llamaindex/astra</b></summary><p>

```
npm i https://pkg.pr.new/run-llama/LlamaIndexTS/@llamaindex/astra@1828
```

</p></details>
      
    

<details><summary><b>@llamaindex/azure</b></summary><p>

```
npm i https://pkg.pr.new/run-llama/LlamaIndexTS/@llamaindex/azure@1828
```

</p></details>
      
    

<details><summary><b>@llamaindex/chroma</b></summary><p>

```
npm i https://pkg.pr.new/run-llama/LlamaIndexTS/@llamaindex/chroma@1828
```

</p></details>
      
    

<details><summary><b>@llamaindex/elastic-search</b></summary><p>

```
npm i https://pkg.pr.new/run-llama/LlamaIndexTS/@llamaindex/elastic-search@1828
```

</p></details>
      
    

<details><summary><b>@llamaindex/firestore</b></summary><p>

```
npm i https://pkg.pr.new/run-llama/LlamaIndexTS/@llamaindex/firestore@1828
```

</p></details>
      
    

<details><summary><b>@llamaindex/milvus</b></summary><p>

```
npm i https://pkg.pr.new/run-llama/LlamaIndexTS/@llamaindex/milvus@1828
```

</p></details>
      
    

<details><summary><b>@llamaindex/mongodb</b></summary><p>

```
npm i https://pkg.pr.new/run-llama/LlamaIndexTS/@llamaindex/mongodb@1828
```

</p></details>
      
    

<details><summary><b>@llamaindex/pinecone</b></summary><p>

```
npm i https://pkg.pr.new/run-llama/LlamaIndexTS/@llamaindex/pinecone@1828
```

</p></details>
      
    

<details><summary><b>@llamaindex/postgres</b></summary><p>

```
npm i https://pkg.pr.new/run-llama/LlamaIndexTS/@llamaindex/postgres@1828
```

</p></details>
      
    

<details><summary><b>@llamaindex/qdrant</b></summary><p>

```
npm i https://pkg.pr.new/run-llama/LlamaIndexTS/@llamaindex/qdrant@1828
```

</p></details>
      
    

<details><summary><b>@llamaindex/supabase</b></summary><p>

```
npm i https://pkg.pr.new/run-llama/LlamaIndexTS/@llamaindex/supabase@1828
```

</p></details>
      
    

<details><summary><b>@llamaindex/upstash</b></summary><p>

```
npm i https://pkg.pr.new/run-llama/LlamaIndexTS/@llamaindex/upstash@1828
```

</p></details>
      
    

<details><summary><b>@llamaindex/weaviate</b></summary><p>

```
npm i https://pkg.pr.new/run-llama/LlamaIndexTS/@llamaindex/weaviate@1828
```

</p></details>
      
    

_commit: <a href="https://github.com/run-llama/LlamaIndexTS/runs/40241737601"><code>ea304b4</code></a>_

author_association: NONE