mirror of
https://github.com/run-llama/create-llama.git
synced 2026-07-01 21:04:08 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 97a7d9bc25 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"create-llama": patch
|
||||
---
|
||||
|
||||
chore: bump @llamaindex/server 0.3.0 in templates
|
||||
@@ -123,21 +123,6 @@ jobs:
|
||||
run: pnpm run pack-install
|
||||
working-directory: packages/create-llama
|
||||
|
||||
- name: Build server
|
||||
run: pnpm run build
|
||||
working-directory: packages/server
|
||||
|
||||
- name: Pack @llamaindex/server package
|
||||
run: |
|
||||
pnpm pack --pack-destination "${{ runner.temp }}"
|
||||
if [ "${{ runner.os }}" == "Windows" ]; then
|
||||
file=$(find "${{ runner.temp }}" -name "llamaindex-server-*.tgz" | head -n 1)
|
||||
mv "$file" "${{ runner.temp }}/llamaindex-server.tgz"
|
||||
else
|
||||
mv ${{ runner.temp }}/llamaindex-server-*.tgz ${{ runner.temp }}/llamaindex-server.tgz
|
||||
fi
|
||||
working-directory: packages/server
|
||||
|
||||
- name: Run Playwright tests for TypeScript
|
||||
run: |
|
||||
pnpm run e2e:ts
|
||||
@@ -146,7 +131,6 @@ jobs:
|
||||
LLAMA_CLOUD_API_KEY: ${{ secrets.LLAMA_CLOUD_API_KEY }}
|
||||
FRAMEWORK: ${{ matrix.frameworks }}
|
||||
VECTORDB: ${{ matrix.vectordbs }}
|
||||
SERVER_PACKAGE_PATH: ${{ runner.temp }}/llamaindex-server.tgz
|
||||
working-directory: packages/create-llama
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -44,10 +44,6 @@ jobs:
|
||||
- name: Run build
|
||||
run: pnpm run build
|
||||
|
||||
- name: Run Typecheck for examples
|
||||
run: pnpm run typecheck
|
||||
working-directory: packages/server/examples
|
||||
|
||||
- name: Run Python format check
|
||||
uses: chartboost/ruff-action@v1
|
||||
with:
|
||||
|
||||
@@ -6,8 +6,6 @@ cache/
|
||||
build/
|
||||
.next/
|
||||
out/
|
||||
packages/server/server/
|
||||
packages/server/project/
|
||||
**/playwright-report/
|
||||
**/test-results/
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ Create-llama is a monorepo containing CLI tools and server frameworks for buildi
|
||||
### Monorepo Structure
|
||||
|
||||
- **`packages/create-llama/`**: Main CLI tool for scaffolding LlamaIndex applications
|
||||
- **`packages/server/`**: TypeScript/Next.js server framework (`@llamaindex/server`)
|
||||
- **`python/llama-index-server/`**: Python/FastAPI server framework
|
||||
- **Root**: Workspace configuration and shared development tools
|
||||
|
||||
@@ -44,15 +43,6 @@ npm run e2e # Playwright tests for generated projects
|
||||
npm run clean # Clean build artifacts and template caches
|
||||
```
|
||||
|
||||
### TypeScript Server Package
|
||||
|
||||
```bash
|
||||
cd packages/server
|
||||
pnpm dev # Watch mode with bunchee
|
||||
pnpm build # Multi-step build: ESM/CJS + Next.js + static assets
|
||||
pnpm clean # Clean all build outputs
|
||||
```
|
||||
|
||||
### Python Server Package
|
||||
|
||||
```bash
|
||||
@@ -84,13 +74,6 @@ The CLI uses a sophisticated template system in `packages/create-llama/templates
|
||||
|
||||
## Server Framework Architecture
|
||||
|
||||
### TypeScript Server (`@llamaindex/server`)
|
||||
|
||||
- **Core**: `LlamaIndexServer` class wrapping Next.js with workflow support
|
||||
- **Frontend**: React-based chat UI with shadcn/ui components
|
||||
- **API**: `/api/chat` endpoint with streaming responses
|
||||
- **Build Process**: Complex multi-step build including static assets for Python integration
|
||||
|
||||
### Python Server (`llama-index-server`)
|
||||
|
||||
- **Core**: `LlamaIndexServer` class extending FastAPI
|
||||
|
||||
@@ -111,7 +111,7 @@ non-interactively. For a list of the latest options, call `create-llama --help`.
|
||||
|
||||
The generated code is using the LlamaIndex Server, which serves LlamaIndex Workflows and Agent Workflows via an API server. See the following docs for more information:
|
||||
|
||||
- [LlamaIndex Server For TypeScript](./packages/server/README.md)
|
||||
- [LlamaIndex Server For TypeScript](https://github.com/run-llama/chat-ui/tree/main/packages/server)
|
||||
- [LlamaIndex Server For Python](./python/llama-index-server/README.md)
|
||||
|
||||
Inspired by and adapted from [create-next-app](https://github.com/vercel/next.js/tree/canary/packages/create-next-app)
|
||||
|
||||
@@ -31,19 +31,6 @@ export default tseslint.config(
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["packages/server/**"],
|
||||
rules: {
|
||||
"no-irregular-whitespace": "off",
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"@typescript-eslint/no-explicit-any": [
|
||||
"error",
|
||||
{
|
||||
ignoreRestArgs: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
ignores: [
|
||||
"python/**",
|
||||
@@ -57,9 +44,6 @@ export default tseslint.config(
|
||||
"**/out/**",
|
||||
"**/node_modules/**",
|
||||
"**/build/**",
|
||||
"packages/server/server/**",
|
||||
"packages/server/project/**",
|
||||
"packages/server/bin/**",
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
@@ -258,16 +258,6 @@ async function updatePackageJson({
|
||||
};
|
||||
}
|
||||
|
||||
// if having custom server package tgz file, use it for testing @llamaindex/server
|
||||
const serverPackagePath = process.env.SERVER_PACKAGE_PATH;
|
||||
if (serverPackagePath) {
|
||||
const relativePath = path.relative(process.cwd(), serverPackagePath);
|
||||
packageJson.dependencies = {
|
||||
...packageJson.dependencies,
|
||||
"@llamaindex/server": `file:${relativePath}`,
|
||||
};
|
||||
}
|
||||
|
||||
await fs.writeFile(
|
||||
packageJsonFile,
|
||||
JSON.stringify(packageJson, null, 2) + os.EOL,
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"dev": "nodemon --exec tsx index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@llamaindex/server": "0.2.10",
|
||||
"@llamaindex/server": "^0.3.0",
|
||||
"dotenv": "^16.4.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
+2
-2
@@ -42,8 +42,8 @@ The human-in-the-loop approach used here is based on a simple idea: the workflow
|
||||
|
||||
To do this, you will need to implement two custom events:
|
||||
|
||||
- [HumanInputEvent](https://github.com/run-llama/create-llama/blob/main/packages/server/src/utils/hitl/events.ts): This event is used to request input from the user.
|
||||
- [HumanResponseEvent](https://github.com/run-llama/create-llama/blob/main/packages/server/src/utils/hitl/events.ts): This event is sent to the workflow to resume execution with input from the user.
|
||||
- [HumanInputEvent](https://github.com/run-llama/chat-ui/tree/main/packages/server/src/utils/hitl/events.ts): This event is used to request input from the user.
|
||||
- [HumanResponseEvent](https://github.com/run-llama/chat-ui/tree/main/packages/server/src/utils/hitl/events.ts): This event is sent to the workflow to resume execution with input from the user.
|
||||
|
||||
In this example, we have implemented these two custom events in [`events.ts`](src/app/events.ts):
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@llamaindex/openai": "~0.4.0",
|
||||
"@llamaindex/server": "~0.2.1",
|
||||
"@llamaindex/server": "^0.3.0",
|
||||
"@llamaindex/workflow": "~1.1.8",
|
||||
"@llamaindex/tools": "~0.1.2",
|
||||
"llamaindex": "~0.11.0",
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
# server contains Nextjs frontend code (not compiled)
|
||||
server/
|
||||
|
||||
# the ejected nextjs project
|
||||
project/
|
||||
|
||||
# temp is the copy of next folder but without API folder, used to build frontend static files
|
||||
temp/
|
||||
@@ -1,206 +0,0 @@
|
||||
# @llamaindex/server
|
||||
|
||||
## 0.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- fec752e: refactor: llamacloud configs
|
||||
|
||||
## 0.2.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 91ce4e1: feat: support file server for python llamadeploy
|
||||
|
||||
## 0.2.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 52cc37f: feat: flag to enable useChatWorkflow
|
||||
- 952b5b4: fix: peer deps and sourcemap issues made ts server start fail
|
||||
|
||||
## 0.2.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- e2486eb: feat: support human in the loop for TS
|
||||
|
||||
## 0.2.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- af9ad3c: feat: show document artifact after generating report
|
||||
- a543a27: feat: bump chat-ui with inline artifact
|
||||
- 1ff6eaf: Add support for chat upload file
|
||||
|
||||
## 0.2.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 3ff0a18: fix: default header padding
|
||||
- df10474: fix: missing cursor pointer for button
|
||||
- 087c961: Support zod and chat-ui hooks for custom components
|
||||
|
||||
## 0.2.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 058b376: Fix generate script for ejected project
|
||||
|
||||
## 0.2.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 5fe9e17: support eject to fully customize next folder
|
||||
- b8a1ff6: Bump version: chat-ui@0.4.6
|
||||
|
||||
## 0.2.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- eee3230: feat: support custom layout
|
||||
- 0bc5a0d: Add suggestNextQuestions config
|
||||
- 3acec88: chore: bump chat-ui
|
||||
|
||||
## 0.2.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 25fba43: refactor: migrate to Nextjs Route Handler
|
||||
- 6f75d4a: fix: unsupported language in code gen workflow
|
||||
|
||||
## 0.2.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- f072308: feat: add dev mode UI
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 0384268: Use the new workflow engine and deprecate the old one.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d9f9e3c: chore: bump chat-ui to support code editor & document editor
|
||||
|
||||
## 0.1.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 8fe5fc2: chore: add llamaindex server package
|
||||
|
||||
## 0.1.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 82d4b46: feat: re-add supports for artifacts
|
||||
|
||||
## 0.1.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7ca9ddf: Add generate ui workflow to @llamaindex/server
|
||||
- 3310eaa: chore: bump chat-ui
|
||||
- llamaindex@0.10.2
|
||||
|
||||
## 0.1.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- llamaindex@0.10.1
|
||||
|
||||
## 0.1.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- edb8b87: fix: shadcn components cannot be used in next server
|
||||
- Updated dependencies [6cf928f]
|
||||
- llamaindex@0.10.0
|
||||
|
||||
## 0.1.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- bb34ade: feat: support cn utils for server UI
|
||||
- llamaindex@0.9.19
|
||||
|
||||
## 0.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 400b3b5: feat: use full-source code with import statements for custom comps
|
||||
- llamaindex@0.9.18
|
||||
|
||||
## 0.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 3ffee26: feat: enhance config params for LlamaIndexServer
|
||||
|
||||
## 0.0.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 0b75bd6: feat: component dir in llamaindex server
|
||||
|
||||
## 0.0.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [3534c37]
|
||||
- llamaindex@0.9.17
|
||||
|
||||
## 0.0.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 4999df1: bump nextjs
|
||||
- Updated dependencies [f5e4d09]
|
||||
- llamaindex@0.9.16
|
||||
|
||||
## 0.0.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 8c02684: fix: handle stream error
|
||||
- c515a32: feat: return raw output for agent toolcall result
|
||||
- llamaindex@0.9.15
|
||||
|
||||
## 0.0.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 9d951b2: feat: support llamacloud in @llamaindex/server
|
||||
- Updated dependencies [9d951b2]
|
||||
- llamaindex@0.9.14
|
||||
|
||||
## 0.0.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 164cf7a: fix: custom next server start fail
|
||||
|
||||
## 0.0.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 299008b: feat: copy create-llama to @llamaindex/servers
|
||||
- 75d6e29: feat: response source nodes in query tool output
|
||||
- Updated dependencies [75d6e29]
|
||||
- llamaindex@0.9.13
|
||||
|
||||
## 0.0.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- f8a86e4: feat: @llamaindex/server
|
||||
- Updated dependencies [21bebfc]
|
||||
- Updated dependencies [93bc0ff]
|
||||
- Updated dependencies [91a18e7]
|
||||
- Updated dependencies [f8a86e4]
|
||||
- Updated dependencies [5189b44]
|
||||
- Updated dependencies [58a9446]
|
||||
- @llamaindex/core@0.6.0
|
||||
- @llamaindex/workflow@1.0.0
|
||||
@@ -1,162 +0,0 @@
|
||||
# @llamaindex/server Package
|
||||
|
||||
This package provides a Next.js-based server framework for running LlamaIndex workflows with both API endpoints and a chat UI interface.
|
||||
|
||||
## Overview
|
||||
|
||||
The `@llamaindex/server` package (`src/`) allows you to quickly launch LlamaIndex Workflows and Agent Workflows as an API server with an optional sophisticated chat UI. It combines a backend API server with a frontend React interface built on Next.js.
|
||||
|
||||
## Key Components
|
||||
|
||||
### Core Server (src/server.ts)
|
||||
|
||||
- **LlamaIndexServer class**: Main server implementation that wraps Next.js
|
||||
- Handles workflow factory initialization and UI configuration
|
||||
- Manages custom components and layout directories
|
||||
- Creates HTTP server with custom routing for chat API
|
||||
- Automatically configures client-side config in `public/config.js`
|
||||
|
||||
### Chat Handler (src/handlers/chat.ts)
|
||||
|
||||
- **handleChat function**: Processes POST requests to `/api/chat`
|
||||
- Converts AI SDK messages to LlamaIndex format
|
||||
- Manages workflow execution with abort signals
|
||||
- Streams responses back to client with optional question suggestions
|
||||
- Handles errors and validation
|
||||
|
||||
### Workflow Management (src/utils/workflow.ts)
|
||||
|
||||
- **runWorkflow function**: Executes workflows with proper event handling
|
||||
- Transforms workflow events (tool calls, source nodes) into UI-friendly formats
|
||||
- Downloads LlamaCloud files automatically in background
|
||||
- Processes agent events and source annotations
|
||||
|
||||
### Event System (src/events.ts)
|
||||
|
||||
- **Source Events**: For displaying document/file sources with metadata
|
||||
- **Agent Events**: For showing agent tool usage and progress
|
||||
- **Artifact Events**: For structured data like code/documents sent to Canvas UI
|
||||
- Helper functions for converting LlamaIndex data to UI events
|
||||
|
||||
### UI Generation (src/utils/gen-ui.ts)
|
||||
|
||||
- **generateEventComponent function**: Uses LLM to auto-generate React components
|
||||
- Creates workflow for UI planning, aggregation, and code generation
|
||||
- Validates generated components against supported dependencies
|
||||
- Supports shadcn/ui, lucide-react, tailwind CSS, and LlamaIndex chat-ui
|
||||
|
||||
### Types (src/types.ts)
|
||||
|
||||
- **WorkflowFactory**: Function signature for creating workflow instances
|
||||
- **UIConfig**: Configuration options for chat interface
|
||||
- **LlamaIndexServerOptions**: Main server configuration interface
|
||||
|
||||
## Next.js Frontend
|
||||
|
||||
The `next/` directory contains the React frontend:
|
||||
|
||||
### API Routes
|
||||
|
||||
- `/api/chat/route.ts`: Main chat endpoint (delegates to handleChat)
|
||||
- `/api/components/route.ts`: Serves custom UI components
|
||||
- `/api/layout/route.ts`: Serves custom layout components
|
||||
- `/api/files/[...slug]/route.ts`: File serving for data/output folders
|
||||
|
||||
### UI Components
|
||||
|
||||
- Chat interface with message history, streaming responses, and canvas panel
|
||||
- Extensible component system for custom workflow events
|
||||
- Custom layout support for headers/footers
|
||||
- Built with shadcn/ui components and Tailwind CSS
|
||||
|
||||
## Build Process
|
||||
|
||||
### Development
|
||||
|
||||
```bash
|
||||
pnpm dev # Watch mode with bunchee
|
||||
```
|
||||
|
||||
### Production Build
|
||||
|
||||
```bash
|
||||
pnpm build # Multi-step build process
|
||||
```
|
||||
|
||||
The build process:
|
||||
|
||||
1. **prebuild**: Cleans dist, server, and temp directories
|
||||
2. **build**: Compiles source with bunchee to ESM/CJS
|
||||
3. **postbuild**: Prepares TypeScript server and Python static assets
|
||||
4. **prepare:ts-server**: Copies Next.js app, builds CSS, compiles API routes
|
||||
5. **prepare:py-static**: Creates static build for Python integration
|
||||
|
||||
## Key Features
|
||||
|
||||
### Workflow Integration
|
||||
|
||||
- Factory pattern for creating workflow instances per request
|
||||
- Supports Agent Workflows with startAgentEvent/stopAgentEvent contract
|
||||
- Automatic event transformation and streaming
|
||||
- Built-in tool call and source node handling
|
||||
|
||||
### UI Extensibility
|
||||
|
||||
- AI-generated components based on Zod schemas
|
||||
- Custom layout sections (header/footer)
|
||||
- Canvas panel for artifacts (documents, code)
|
||||
- Event aggregation and real-time updates
|
||||
|
||||
### File Handling
|
||||
|
||||
- Automatic mounting of `data/` and `output/` folders
|
||||
- LlamaCloud file downloads in background
|
||||
- Static asset serving through Next.js
|
||||
|
||||
### Development Features
|
||||
|
||||
- Hot reload support for workflow code (beta)
|
||||
- Dev mode panel for live code editing
|
||||
- TypeScript support throughout
|
||||
- Comprehensive error handling
|
||||
|
||||
## Configuration
|
||||
|
||||
Server configuration through `LlamaIndexServerOptions`:
|
||||
|
||||
- `workflow`: Factory function for creating workflow instances
|
||||
- `uiConfig.starterQuestions`: Predefined questions for chat interface
|
||||
- `uiConfig.componentsDir`: Directory for custom event components
|
||||
- `uiConfig.layoutDir`: Directory for custom layout components
|
||||
- `uiConfig.devMode`: Enable live code editing
|
||||
- `suggestNextQuestions`: Auto-suggest follow-up questions
|
||||
- `llamaCloud`: An object to configure the LlamaCloud integration containing the following properties:
|
||||
- `outputDir`: The directory for LlamaCloud output
|
||||
- `indexSelector`: Whether to show the LlamaCloud index selector in the chat UI
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Runtime Dependencies
|
||||
|
||||
- Next.js 15+ for server framework
|
||||
- React 19+ for UI components
|
||||
- LlamaIndex workflow engine
|
||||
- Radix UI components (shadcn/ui)
|
||||
- AI SDK for streaming responses
|
||||
|
||||
### Development Dependencies
|
||||
|
||||
- Bunchee for bundling
|
||||
- TypeScript for type safety
|
||||
- Tailwind CSS for styling
|
||||
- PostCSS for CSS processing
|
||||
|
||||
## Usage Patterns
|
||||
|
||||
1. **Basic Setup**: Create workflow factory, configure UI, start server
|
||||
2. **Custom Events**: Define Zod schemas, generate UI components with LLM
|
||||
3. **File Integration**: Use data/output folders for document processing
|
||||
4. **Development**: Use dev mode for iterative workflow development
|
||||
5. **Production**: Build static assets for deployment with Python backend
|
||||
|
||||
The package serves as a complete solution for deploying LlamaIndex workflows with professional chat interfaces and extensible UI components.
|
||||
@@ -1,335 +0,0 @@
|
||||
# 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
|
||||
|
||||
- Add a sophisticated chatbot UI to your LlamaIndex workflow
|
||||
- Edit code and document artifacts in an OpenAI Canvas-style UI
|
||||
- Extendable UI components for events and headers
|
||||
- Built on Next.js for high performance and easy API development
|
||||
- Human-in-the-loop (HITL) support, check out the [Human-in-the-loop](https://github.com/run-llama/create-llama/blob/main/packages/server/examples/hitl/README.md) documentation for more details.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm i @llamaindex/server
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
Create an `index.ts` file and add the following code:
|
||||
|
||||
```ts
|
||||
import { LlamaIndexServer } from "@llamaindex/server";
|
||||
import { openai } from "@llamaindex/openai";
|
||||
import { agent } from "@llamaindex/workflow";
|
||||
import { wiki } from "@llamaindex/tools"; // or any other tool
|
||||
|
||||
const createWorkflow = () => agent({ tools: [wiki()], llm: openai("gpt-4o") });
|
||||
|
||||
new LlamaIndexServer({
|
||||
workflow: createWorkflow,
|
||||
uiConfig: {
|
||||
starterQuestions: ["Who is the first president of the United States?"],
|
||||
},
|
||||
}).start();
|
||||
```
|
||||
|
||||
The `createWorkflow` function is a factory function that creates an [Agent Workflow](https://ts.llamaindex.ai/docs/llamaindex/modules/agents/agent_workflow) with a tool that retrieves information from Wikipedia in this case. For more details, read about the [Workflow factory contract](#workflow-factory-contract).
|
||||
|
||||
## 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 options:
|
||||
|
||||
- `workflow`: A callable function that creates a workflow instance for each request. See [Workflow factory contract](#workflow-factory-contract) for more details.
|
||||
- `uiConfig`: An object to configure the chat UI containing the following properties:
|
||||
- `starterQuestions`: List of starter questions for the chat UI (default: `[]`)
|
||||
- `enableFileUpload`: Whether to enable file upload in the chat UI (default: `false`). See [Upload file example](./examples/private-file/README.md) for more details.
|
||||
- `componentsDir`: The directory for custom UI components rendering events emitted by the workflow. The default is undefined, which does not render custom UI components.
|
||||
- `layoutDir`: The directory for custom layout sections. The default value is `layout`. See [Custom Layout](#custom-layout) for more details.
|
||||
- `dev_mode`: When enabled, you can update workflow code in the UI and see the changes immediately. It's currently in beta and only supports updating workflow code at `app/src/workflow.ts`. Please start server in dev mode (`npm run dev`) to use see this reload feature enabled.
|
||||
- `suggestNextQuestions`: Whether to suggest next questions after the assistant's response (default: `true`). You can change the prompt for the next questions by setting the `NEXT_QUESTION_PROMPT` environment variable.
|
||||
- `llamaCloud`: An object to configure the LlamaCloud integration containing the following properties:
|
||||
- `outputDir`: The directory for LlamaCloud output
|
||||
- `indexSelector`: Whether to show the LlamaCloud index selector in the chat UI
|
||||
|
||||
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).
|
||||
|
||||
## Workflow factory contract
|
||||
|
||||
The `workflow` provided will be called for each chat request to initialize a new workflow instance. For advanced use cases, you can define workflowFactory with a chatBody which include list of UI messages in the request body.
|
||||
|
||||
```typescript
|
||||
import { type Message } from "ai";
|
||||
import { agent } from "@llamaindex/workflow";
|
||||
|
||||
const workflowFactory = (chatBody: { messages: Message[] }) => {
|
||||
...
|
||||
};
|
||||
```
|
||||
|
||||
The contract of the generated workflow must be the same as for the [Agent Workflow](https://ts.llamaindex.ai/docs/llamaindex/modules/agents/agent_workflow). This means that the workflow must handle a `startAgentEvent` event, which is the entry point of the workflow and contains the following information in it's `data` property:
|
||||
|
||||
```typescript
|
||||
{
|
||||
userInput: MessageContent;
|
||||
chatHistory?: ChatMessage[] | undefined;
|
||||
};
|
||||
```
|
||||
|
||||
The `userInput` is the latest user message and the `chatHistory` is the list of messages exchanged between the user and the workflow so far.
|
||||
|
||||
Furthermore, the workflow must stop with a `stopAgentEvent` event to mark the end of the workflow. In between, the workflow can emit [UI events](##AI-generated-UI-Components) to render custom UI components and [Artifact events](##Sending-Artifacts-to-the-UI) to send structured data like generated documents or code snippets to the UI.
|
||||
|
||||
```ts
|
||||
import {
|
||||
createStatefulMiddleware,
|
||||
createWorkflow,
|
||||
startAgentEvent,
|
||||
} from "@llamaindex/workflow";
|
||||
import { ChatMemoryBuffer, type ChatMessage, Settings } from "llamaindex";
|
||||
import { openai } from "@llamaindex/openai";
|
||||
import { wiki } from "@llamaindex/tools";
|
||||
|
||||
Settings.llm = openai("gpt-4o");
|
||||
|
||||
export const workflowFactory = async () => {
|
||||
const workflow = createWorkflow();
|
||||
|
||||
workflow.handle([startAgentEvent], async ({ data }) => {
|
||||
const { state, sendEvent } = getContext();
|
||||
const messages = data.chatHistory;
|
||||
|
||||
const toolCallResponse = await chatWithTools(
|
||||
Settings.llm,
|
||||
[wiki()],
|
||||
messages,
|
||||
);
|
||||
|
||||
// using result from tool call and use `sendEvent` to emit the next event...
|
||||
});
|
||||
|
||||
// define more workflow handling logic here...
|
||||
|
||||
// Finally stop with a `stopAgentEvent` event to mark the end of the workflow.
|
||||
// return stopAgentEvent.with({
|
||||
// result: "This is the end!",
|
||||
// });
|
||||
|
||||
return workflow;
|
||||
};
|
||||
```
|
||||
|
||||
To generate sophisticated examples of workflows, you best use the [create-llama](https://github.com/run-llama/create-llama) project.
|
||||
|
||||
## AI-generated UI Components
|
||||
|
||||
The LlamaIndex server provides support for rendering workflow events using custom UI components, allowing you to extend and customize the chat interface.
|
||||
These components can be auto-generated using an LLM by providing a JSON schema of the workflow event.
|
||||
|
||||
### UI Event Schema
|
||||
|
||||
To display custom UI components, your workflow needs to emit UI events that have an event type for identification and a data object:
|
||||
|
||||
```typescript
|
||||
class UIEvent extends WorkflowEvent<{
|
||||
type: "ui_event";
|
||||
data: UIEventData;
|
||||
}> {}
|
||||
```
|
||||
|
||||
The `data` object can be any JSON object. To enable AI generation of the UI component, you need to provide a schema for that data (here we're using Zod):
|
||||
|
||||
```typescript
|
||||
const MyEventDataSchema = z
|
||||
.object({
|
||||
stage: z
|
||||
.enum(["retrieve", "analyze", "answer"])
|
||||
.describe("The current stage the workflow process is in."),
|
||||
progress: z
|
||||
.number()
|
||||
.min(0)
|
||||
.max(1)
|
||||
.describe("The progress in percent of the current stage"),
|
||||
})
|
||||
.describe("WorkflowStageProgress");
|
||||
|
||||
type UIEventData = z.infer<typeof MyEventDataSchema>;
|
||||
```
|
||||
|
||||
### Generate UI Components
|
||||
|
||||
The `generateEventComponent` function uses an LLM to generate a custom UI component based on the JSON schema of a workflow event. The schema should contain accurate descriptions of each field so that the LLM can generate matching components for your use case. We've done this for you in the example above using the `describe` function from Zod:
|
||||
|
||||
```typescript
|
||||
import { OpenAI } from "llamaindex";
|
||||
import { generateEventComponent } from "@llamaindex/server";
|
||||
import { MyEventDataSchema } from "./your-workflow";
|
||||
|
||||
// Also works well with Claude 3.5 Sonnet and Google Gemini 2.5 Pro
|
||||
const llm = new OpenAI({ model: "gpt-4.1" });
|
||||
const code = generateEventComponent(MyEventDataSchema, llm);
|
||||
```
|
||||
|
||||
After generating the code, we need to save it to a file. The file name must match the event type from your workflow (e.g., `ui_event.jsx` for handling events with `ui_event` type):
|
||||
|
||||
```ts
|
||||
fs.writeFileSync("components/ui_event.jsx", code);
|
||||
```
|
||||
|
||||
Feel free to modify the generated code to match your needs. If you're not satisfied with the generated code, we suggest improving the provided JSON schema first or trying another LLM.
|
||||
|
||||
> Note that `generateEventComponent` is generating JSX code, but you can also provide a TSX file.
|
||||
|
||||
## Custom Layout
|
||||
|
||||
LlamaIndex Server supports custom layout for header and footer. To use custom layout, you need to initialize the LlamaIndex server with the `layoutDir` that contains your custom layout files.
|
||||
|
||||
```ts
|
||||
new LlamaIndexServer({
|
||||
workflow: createWorkflow,
|
||||
uiConfig: {
|
||||
layoutDir: "layout",
|
||||
},
|
||||
}).start();
|
||||
```
|
||||
|
||||
```
|
||||
layout/
|
||||
header.tsx
|
||||
footer.tsx
|
||||
```
|
||||
|
||||
We currently support custom header and footer for the chat interface. The syntax for these files is the same as events components in components directory.
|
||||
Note that by default, we are still rendering the default LlamaIndex Header. It's also the fallback when having errors rendering the custom header. Example layout files will be generated in the `layout` directory of your project when creating a new project with `create-llama`.
|
||||
|
||||
### Server Setup
|
||||
|
||||
To use the generated UI components, you need to initialize the LlamaIndex server with the `componentsDir` that contains your custom UI components:
|
||||
|
||||
```ts
|
||||
new LlamaIndexServer({
|
||||
workflow: createWorkflow,
|
||||
uiConfig: {
|
||||
componentsDir: "components",
|
||||
},
|
||||
}).start();
|
||||
```
|
||||
|
||||
## Sending Artifacts to the UI
|
||||
|
||||
In addition to UI events for custom components, LlamaIndex Server supports a special `ArtifactEvent` to send structured data like generated documents or code snippets to the UI. These artifacts are displayed in a dedicated "Canvas" panel in the chat interface.
|
||||
|
||||
### Artifact Event Structure
|
||||
|
||||
To send an artifact, your workflow needs to emit an event with `type: "artifact"`. The `data` payload of this event should include:
|
||||
|
||||
- `type`: A string indicating the type of artifact (e.g., `"document"`, `"code"`).
|
||||
- `created_at`: A timestamp (e.g., `Date.now()`) indicating when the artifact was created.
|
||||
- `data`: An object containing the specific details of the artifact. The structure of this object depends on the artifact `type`.
|
||||
|
||||
### Defining and Sending an ArtifactEvent
|
||||
|
||||
First, define your artifact event using `workflowEvent` from `@llamaindex/workflow`:
|
||||
|
||||
```typescript
|
||||
import { workflowEvent } from "@llamaindex/workflow";
|
||||
|
||||
// Example for a document artifact
|
||||
const artifactEvent = workflowEvent<{
|
||||
type: "artifact"; // Must be "artifact"
|
||||
data: {
|
||||
type: "document"; // Custom type for your artifact (e.g., "document", "code")
|
||||
created_at: number;
|
||||
data: {
|
||||
// Specific data for the document artifact type
|
||||
title: string;
|
||||
content: string;
|
||||
type: "markdown" | "html"; // document format
|
||||
};
|
||||
};
|
||||
}>();
|
||||
```
|
||||
|
||||
Then, within your workflow logic, use `sendEvent` (obtained from `getContext()`) to emit the event:
|
||||
|
||||
```typescript
|
||||
// Assuming 'sendEvent' is available in your workflow handler
|
||||
// and 'documentDetails' contains the content for the artifact.
|
||||
|
||||
sendEvent(
|
||||
artifactEvent.with({
|
||||
type: "artifact", // This top-level type must be "artifact"
|
||||
data: {
|
||||
type: "document", // This is your specific artifact type
|
||||
created_at: Date.now(),
|
||||
data: {
|
||||
title: "My Generated Document",
|
||||
content: "# Hello World
|
||||
This is a markdown document.",
|
||||
type: "markdown",
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
```
|
||||
|
||||
This will send the artifact to the LlamaIndex Server UI, where it will be rendered in the [ChatCanvasPanel](/packages/server/next/app/components/ui/chat/canvas/panel.tsx) by a renderer depending on the artifact type. For type `document` this is using the [DocumentArtifactViewer](https://github.com/run-llama/chat-ui/blob/bacb75fc6edceacf742fba18632404a2483b5a81/packages/chat-ui/src/chat/canvas/artifacts/document.tsx#L17).
|
||||
|
||||
## 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. By convention, the `data` folder is used for documents that are ingested, and the `output` folder is used for documents generated by the workflow.
|
||||
|
||||
### Eject Mode
|
||||
|
||||
If you want to fully customize the server UI and routes, you can use `npm eject`. It will create a normal Next.js project with the same functionality as @llamaindex/server.
|
||||
By default, the ejected project will be in the `next` directory in the current working directory. You can change the output directory by providing custom path after `eject` command:
|
||||
|
||||
```bash
|
||||
npm eject <path-to-output-directory>
|
||||
```
|
||||
|
||||
How eject works:
|
||||
|
||||
1. Init nextjs project with eslint, prettier, postcss, tailwindcss, shadcn components, etc.
|
||||
2. Copy your workflow definition and setting files in src/app/\* to the ejected project in app/api/chat
|
||||
3. Copy your components, data, output, storage folders to the ejected project
|
||||
4. Copy your current .env file to the ejected project
|
||||
5. Clean up files that are no longer needed and update imports
|
||||
|
||||
## API Reference
|
||||
|
||||
- [LlamaIndexServer](https://ts.llamaindex.ai/docs/api/classes/LlamaIndexServer)
|
||||
@@ -1,172 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require("fs").promises;
|
||||
const path = require("path");
|
||||
|
||||
// Resolve the project directory in node_modules/@llamaindex/server/project
|
||||
// This is the template that used to construct the nextjs project
|
||||
const projectDir = path.resolve(__dirname, "../project");
|
||||
|
||||
// Resolve the src directory that contains workflow & setting files
|
||||
const srcDir = path.join(process.cwd(), "src");
|
||||
const srcAppDir = path.join(srcDir, "app");
|
||||
const generateFile = path.join(srcDir, "generate.ts");
|
||||
const envFile = path.join(process.cwd(), ".env");
|
||||
|
||||
// The environment variables that are used as LlamaIndexServer configs
|
||||
const SERVER_CONFIG_VARS = [
|
||||
{
|
||||
key: "OPENAI_API_KEY",
|
||||
defaultValue: "<your-openai-api-key>",
|
||||
description: "OpenAI API key",
|
||||
},
|
||||
{
|
||||
key: "SUGGEST_NEXT_QUESTIONS",
|
||||
defaultValue: "true",
|
||||
description: "Whether to suggest next questions (`suggestNextQuestions`)",
|
||||
},
|
||||
{
|
||||
key: "COMPONENTS_DIR",
|
||||
defaultValue: "components",
|
||||
description: "Directory for custom components (`componentsDir`)",
|
||||
},
|
||||
{
|
||||
key: "WORKFLOW_FILE_PATH",
|
||||
defaultValue: "app/api/chat/app/workflow.ts",
|
||||
description: "The path to the workflow file (will be updated in dev mode)",
|
||||
},
|
||||
{
|
||||
key: "NEXT_PUBLIC_USE_COMPONENTS_DIR",
|
||||
defaultValue: "true",
|
||||
description: "Whether to enable components directory feature on frontend",
|
||||
},
|
||||
{
|
||||
key: "NEXT_PUBLIC_DEV_MODE",
|
||||
defaultValue: "true",
|
||||
description: "Whether to enable dev mode (`devMode`)",
|
||||
},
|
||||
{
|
||||
key: "NEXT_PUBLIC_STARTER_QUESTIONS",
|
||||
defaultValue: '["Summarize the document", "What are the key points?"]',
|
||||
description:
|
||||
"Initial questions to display in the chat (`starterQuestions`)",
|
||||
},
|
||||
{
|
||||
key: "NEXT_PUBLIC_SHOW_LLAMACLOUD_SELECTOR",
|
||||
defaultValue: "false",
|
||||
description:
|
||||
"Whether to show LlamaCloud selector for frontend (`llamaCloudIndexSelector`)",
|
||||
},
|
||||
];
|
||||
|
||||
async function eject() {
|
||||
try {
|
||||
// validate required directories (nextjs project template, src directory, src/app directory)
|
||||
const requiredDirs = [projectDir, srcDir, srcAppDir];
|
||||
for (const dir of requiredDirs) {
|
||||
const exists = await fs
|
||||
.access(dir)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
if (!exists) {
|
||||
console.error("Error: directory does not exist at", dir);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Get destination directory from command line arguments (pnpm eject <path>)
|
||||
const args = process.argv;
|
||||
const outputIndex = args.indexOf("eject");
|
||||
const destDir =
|
||||
outputIndex !== -1 && args[outputIndex + 1]
|
||||
? path.resolve(args[outputIndex + 1]) // Use provided path after eject
|
||||
: path.join(process.cwd(), "next"); // Default to "next" folder in the current working directory
|
||||
|
||||
// remove destination directory if it exists
|
||||
await fs.rm(destDir, { recursive: true, force: true });
|
||||
|
||||
// create destination directory
|
||||
await fs.mkdir(destDir, { recursive: true });
|
||||
|
||||
// Copy the nextjs project template to the destination directory
|
||||
await fs.cp(projectDir, destDir, { recursive: true });
|
||||
|
||||
// copy src/app/* to destDir/app/api/chat
|
||||
const chatRouteDir = path.join(destDir, "app", "api", "chat");
|
||||
await fs.cp(srcAppDir, path.join(chatRouteDir, "app"), { recursive: true });
|
||||
|
||||
// nextjs project doesn't depend on @llamaindex/server anymore, we need to update the imports in workflow file
|
||||
const workflowFile = path.join(chatRouteDir, "app", "workflow.ts");
|
||||
let workflowContent = await fs.readFile(workflowFile, "utf-8");
|
||||
workflowContent = workflowContent.replace("@llamaindex/server", "../utils");
|
||||
await fs.writeFile(workflowFile, workflowContent);
|
||||
|
||||
// copy generate.ts if it exists
|
||||
const genFilePath = path.join(chatRouteDir, "generate.ts");
|
||||
const genFileExists = await copy(generateFile, genFilePath);
|
||||
if (genFileExists) {
|
||||
// update the import @llamaindex/server in generate.ts
|
||||
let genContent = await fs.readFile(genFilePath, "utf-8");
|
||||
genContent = genContent.replace("@llamaindex/server", "./utils");
|
||||
await fs.writeFile(genFilePath, genContent);
|
||||
}
|
||||
|
||||
// copy folders in root directory if exists
|
||||
const rootFolders = ["components", "data", "output", "storage"];
|
||||
for (const folder of rootFolders) {
|
||||
await copy(path.join(process.cwd(), folder), path.join(destDir, folder));
|
||||
}
|
||||
|
||||
// copy .env if it exists or create a new one
|
||||
const envFileExists = await copy(envFile, path.join(destDir, ".env"));
|
||||
if (!envFileExists) {
|
||||
await fs.writeFile(path.join(destDir, ".env"), "");
|
||||
}
|
||||
|
||||
// update .env file with more server configs
|
||||
let envFileContent = await fs.readFile(path.join(destDir, ".env"), "utf-8");
|
||||
for (const envVar of SERVER_CONFIG_VARS) {
|
||||
const { key, defaultValue, description } = envVar;
|
||||
if (!envFileContent.includes(key)) {
|
||||
// if the key is not exists in the env file, add it
|
||||
envFileContent += `\n# ${description}\n${key}=${defaultValue}\n`;
|
||||
}
|
||||
}
|
||||
await fs.writeFile(path.join(destDir, ".env"), envFileContent);
|
||||
|
||||
// rename gitignore -> .gitignore
|
||||
await fs.rename(
|
||||
path.join(destDir, "gitignore"),
|
||||
path.join(destDir, ".gitignore"),
|
||||
);
|
||||
|
||||
// user can customize layout directory in nextjs project, remove layout api
|
||||
await fs.rm(path.join(destDir, "app", "api", "layout"), {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
|
||||
// remove no-needed files
|
||||
await fs.unlink(path.join(destDir, "public", "config.js"));
|
||||
await fs.unlink(path.join(destDir, "next-build.config.ts"));
|
||||
|
||||
console.log("Successfully ejected @llamaindex/server to", destDir);
|
||||
} catch (error) {
|
||||
console.error("Error during eject:", error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// copy src to dest if src exists, return true if src exists
|
||||
async function copy(src, dest) {
|
||||
const srcExists = await fs
|
||||
.access(src)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
if (srcExists) {
|
||||
await fs.cp(src, dest, { recursive: true });
|
||||
}
|
||||
return srcExists;
|
||||
}
|
||||
|
||||
eject();
|
||||
@@ -1,186 +0,0 @@
|
||||
# LlamaIndex Server Examples
|
||||
|
||||
This package contains practical examples demonstrating how to use the `@llamaindex/server` package to build chat applications with LlamaIndex workflows.
|
||||
|
||||
## Package Overview
|
||||
|
||||
The examples package is a collection of standalone TypeScript applications that showcase different features and capabilities of the LlamaIndex Server framework. Each example can be run independently to demonstrate specific functionality.
|
||||
|
||||
## Key Features Demonstrated
|
||||
|
||||
### 1. Simple Workflow (`simple-workflow/calculator.ts`)
|
||||
|
||||
- **Purpose**: Basic agent workflow with tool integration
|
||||
- **Features**: Calculator agent with add tool, starter questions
|
||||
- **Key Concepts**: Tool definition with Zod schemas, basic server setup
|
||||
|
||||
### 2. Agentic RAG (`agentic-rag/index.ts`)
|
||||
|
||||
- **Purpose**: Retrieval-Augmented Generation with document querying
|
||||
- **Features**: Vector store index, document ingestion, query engine tool, automatic question suggestions
|
||||
- **Key Concepts**: RAG implementation, source node inclusion, embedding models
|
||||
|
||||
### 3. Custom Layout (`custom-layout/index.ts` + `layout/header.tsx`)
|
||||
|
||||
- **Purpose**: Custom UI components and layout customization
|
||||
- **Features**: Weather agent with custom header layout, branded interface
|
||||
- **Key Concepts**: Layout directory configuration, React component integration
|
||||
|
||||
### 4. Development Mode (`devmode/index.ts` + `src/app/workflow.ts`)
|
||||
|
||||
- **Purpose**: Live development and hot reloading capabilities
|
||||
- **Features**: Dev mode panel, workflow file hot reloading, separate workflow file structure
|
||||
- **Key Concepts**: Development workflow, file watching, modular architecture
|
||||
|
||||
## Development Scripts
|
||||
|
||||
```bash
|
||||
# Type checking
|
||||
pnpm typecheck
|
||||
|
||||
# Run development server (defaults to simple-workflow/calculator.ts)
|
||||
pnpm dev
|
||||
|
||||
# Run specific examples
|
||||
npx nodemon --exec tsx agentic-rag/index.ts
|
||||
npx nodemon --exec tsx custom-layout/index.ts
|
||||
npx nodemon --exec tsx devmode/index.ts --ignore src/app/workflow_*.ts # Dev mode with file watching
|
||||
```
|
||||
|
||||
## Environment Setup
|
||||
|
||||
All examples require OpenAI API access:
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY=your_openai_api_key
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Core Dependencies
|
||||
|
||||
- `@llamaindex/server`: Main server framework (workspace dependency)
|
||||
- `@llamaindex/workflow`: Workflow engine for agent creation
|
||||
- `@llamaindex/openai`: OpenAI LLM and embedding integrations
|
||||
- `@llamaindex/tools`: Tool utilities
|
||||
- `@llamaindex/readers`: Document readers
|
||||
- `llamaindex`: Core LlamaIndex library
|
||||
- `zod`: Schema validation for tools
|
||||
|
||||
### Development Dependencies
|
||||
|
||||
- `tsx`: TypeScript execution for development
|
||||
- `nodemon`: File watching and auto-restart
|
||||
- `typescript`: TypeScript compiler
|
||||
|
||||
## Architecture Patterns
|
||||
|
||||
### Workflow Factory Pattern
|
||||
|
||||
All examples use the workflow factory pattern:
|
||||
|
||||
```typescript
|
||||
const workflowFactory = () => agent({ tools: [...] });
|
||||
// or
|
||||
const workflowFactory = async () => { /* setup logic */ return agent({ tools: [...] }); };
|
||||
```
|
||||
|
||||
### Server Configuration
|
||||
|
||||
Standard server setup pattern:
|
||||
|
||||
```typescript
|
||||
new LlamaIndexServer({
|
||||
workflow: workflowFactory,
|
||||
uiConfig: {
|
||||
/* UI configuration */
|
||||
},
|
||||
port: 3000,
|
||||
}).start();
|
||||
```
|
||||
|
||||
### Tool Definition Pattern
|
||||
|
||||
Consistent tool creation with Zod schemas:
|
||||
|
||||
```typescript
|
||||
tool({
|
||||
name: "tool_name",
|
||||
description: "Tool description",
|
||||
parameters: z.object({
|
||||
/* parameters */
|
||||
}),
|
||||
execute: (params) => {
|
||||
/* implementation */
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Example-Specific Features
|
||||
|
||||
### Simple Workflow
|
||||
|
||||
- Basic arithmetic operations
|
||||
- Minimal setup for learning
|
||||
- Demonstrates core workflow concepts
|
||||
|
||||
### Agentic RAG
|
||||
|
||||
- Document indexing with embeddings
|
||||
- Vector similarity search
|
||||
- Source node tracking for citations
|
||||
- Auto-generated follow-up questions
|
||||
|
||||
### Custom Layout
|
||||
|
||||
- Custom React components in `layout/` directory
|
||||
- Branded header with navigation
|
||||
- Layout directory configuration (`layoutDir: "layout"`)
|
||||
|
||||
### Dev Mode
|
||||
|
||||
- Live code editing in browser
|
||||
- Hot reloading of workflow files
|
||||
- Separate workflow file organization
|
||||
- Development panel UI
|
||||
|
||||
## TypeScript Configuration
|
||||
|
||||
- Target: ES2022 with bundler module resolution
|
||||
- Strict type checking enabled
|
||||
- Excludes: `node_modules`, `dist`, `custom-layout/layout` (runtime components)
|
||||
- Output: `dist/` directory
|
||||
|
||||
## Development Workflow
|
||||
|
||||
1. **Choose Example**: Select appropriate example for your use case
|
||||
2. **Environment Setup**: Configure OpenAI API key
|
||||
3. **Run Development Server**: Use `pnpm dev` or specific nodemon commands
|
||||
4. **Access UI**: Open browser at `http://localhost:3000`
|
||||
5. **Iterate**: Modify code and see changes in real-time
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Agent Creation
|
||||
|
||||
All examples use the `agent()` function from `@llamaindex/workflow` with tool arrays.
|
||||
|
||||
### UI Configuration
|
||||
|
||||
- `starterQuestions`: Predefined questions for user guidance
|
||||
- `layoutDir`: Custom layout components directory
|
||||
- `devMode`: Enable development features
|
||||
- `suggestNextQuestions`: Auto-generate follow-up questions
|
||||
|
||||
### Error Handling
|
||||
|
||||
Examples demonstrate proper async/await patterns and error handling for LLM operations.
|
||||
|
||||
## Integration Points
|
||||
|
||||
- **LlamaIndex Core**: Document processing, indexing, querying
|
||||
- **OpenAI**: LLM and embedding model integration
|
||||
- **React/Next.js**: Frontend UI components and server-side rendering
|
||||
- **TypeScript**: Type safety throughout the application stack
|
||||
|
||||
This examples package serves as a comprehensive reference for building production-ready chat applications with LlamaIndex workflows.
|
||||
@@ -1,38 +0,0 @@
|
||||
# LlamaIndex Server Examples
|
||||
|
||||
This directory provides example projects demonstrating how to use the LlamaIndex Server.
|
||||
|
||||
## How to Run the Examples
|
||||
|
||||
1. **Install dependencies**
|
||||
|
||||
In the root of this directory, run:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
2. **Set your OpenAI API key**
|
||||
|
||||
Export your OpenAI API key as an environment variable:
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY=your_openai_api_key
|
||||
```
|
||||
|
||||
3. **Start an example**
|
||||
|
||||
Replace `<example>` with the name of the example you want to run (e.g., `private-file`):
|
||||
|
||||
```bash
|
||||
pnpm nodemon --exec tsx <example>/index.ts
|
||||
```
|
||||
|
||||
4. **Open the application in your browser**
|
||||
|
||||
Visit [http://localhost:3000](http://localhost:3000) to interact with the running example.
|
||||
|
||||
## Notes
|
||||
|
||||
- Make sure you have [pnpm](https://pnpm.io/) installed.
|
||||
- Each example may have its own specific instructions or requirements; check the individual example's index.ts for details.
|
||||
@@ -1,38 +0,0 @@
|
||||
import { OpenAI, OpenAIEmbedding } from "@llamaindex/openai";
|
||||
import { LlamaIndexServer } from "@llamaindex/server";
|
||||
import { agent } from "@llamaindex/workflow";
|
||||
import { Document, Settings, VectorStoreIndex } from "llamaindex";
|
||||
|
||||
Settings.llm = new OpenAI({
|
||||
model: "gpt-4o-mini",
|
||||
});
|
||||
|
||||
Settings.embedModel = new OpenAIEmbedding({
|
||||
model: "text-embedding-3-small",
|
||||
});
|
||||
|
||||
export const workflowFactory = async () => {
|
||||
const index = await VectorStoreIndex.fromDocuments([
|
||||
new Document({ text: "The dog is brown" }),
|
||||
new Document({ text: "The dog is yellow" }),
|
||||
]);
|
||||
|
||||
const queryEngineTool = index.queryTool({
|
||||
metadata: {
|
||||
name: "query_document",
|
||||
description: `This tool can retrieve information in documents`,
|
||||
},
|
||||
includeSourceNodes: true,
|
||||
});
|
||||
|
||||
return agent({ tools: [queryEngineTool] });
|
||||
};
|
||||
|
||||
new LlamaIndexServer({
|
||||
workflow: workflowFactory,
|
||||
suggestNextQuestions: true,
|
||||
uiConfig: {
|
||||
starterQuestions: ["What is the color of the dog?"],
|
||||
},
|
||||
port: 3000,
|
||||
}).start();
|
||||
@@ -1,22 +0,0 @@
|
||||
This example demonstrates how to use the code generation workflow.
|
||||
|
||||
```ts
|
||||
new LlamaIndexServer({
|
||||
workflow: workflowFactory,
|
||||
uiConfig: {
|
||||
starterQuestions: [
|
||||
"Generate a calculator app",
|
||||
"Create a simple todo list app",
|
||||
],
|
||||
componentsDir: "components",
|
||||
},
|
||||
port: 3000,
|
||||
}).start();
|
||||
```
|
||||
|
||||
Export OpenAI API key and start the server in dev mode.
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY=<your-openai-api-key>
|
||||
npx nodemon --exec tsx index.ts
|
||||
```
|
||||
@@ -1,132 +0,0 @@
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Markdown } from "@llamaindex/chat-ui/widgets";
|
||||
import { ListChecks, Loader2, Wand2 } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const STAGE_META = {
|
||||
plan: {
|
||||
icon: ListChecks,
|
||||
badgeText: "Step 1/2: Planning",
|
||||
gradient: "from-blue-100 via-blue-50 to-white",
|
||||
progress: 33,
|
||||
iconBg: "bg-blue-100 text-blue-600",
|
||||
badge: "bg-blue-100 text-blue-700",
|
||||
},
|
||||
generate: {
|
||||
icon: Wand2,
|
||||
badgeText: "Step 2/2: Generating",
|
||||
gradient: "from-violet-100 via-violet-50 to-white",
|
||||
progress: 66,
|
||||
iconBg: "bg-violet-100 text-violet-600",
|
||||
badge: "bg-violet-100 text-violet-700",
|
||||
},
|
||||
};
|
||||
|
||||
function ArtifactWorkflowCard({ event }) {
|
||||
const [visible, setVisible] = useState(event?.state !== "completed");
|
||||
const [fade, setFade] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (event?.state === "completed") {
|
||||
setVisible(false);
|
||||
} else {
|
||||
setVisible(true);
|
||||
setFade(false);
|
||||
}
|
||||
}, [event?.state]);
|
||||
|
||||
if (!event || !visible) return null;
|
||||
|
||||
const { state, requirement } = event;
|
||||
const meta = STAGE_META[state];
|
||||
|
||||
if (!meta) return null;
|
||||
|
||||
return (
|
||||
<div className="flex min-h-[180px] w-full items-center justify-center py-2">
|
||||
<Card
|
||||
className={cn(
|
||||
"w-full rounded-xl shadow-md transition-all duration-500",
|
||||
"border-0",
|
||||
fade && "pointer-events-none opacity-0",
|
||||
`bg-gradient-to-br ${meta.gradient}`,
|
||||
)}
|
||||
style={{
|
||||
boxShadow:
|
||||
"0 2px 12px 0 rgba(80, 80, 120, 0.08), 0 1px 3px 0 rgba(80, 80, 120, 0.04)",
|
||||
}}
|
||||
>
|
||||
<CardHeader className="flex flex-row items-center gap-2 px-3 pb-1 pt-2">
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-center rounded-full p-1",
|
||||
meta.iconBg,
|
||||
)}
|
||||
>
|
||||
<meta.icon className="h-5 w-5" />
|
||||
</div>
|
||||
<CardTitle className="flex items-center gap-2 text-base font-semibold">
|
||||
<Badge className={cn("ml-1", meta.badge, "px-2 py-0.5 text-xs")}>
|
||||
{meta.badgeText}
|
||||
</Badge>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="px-3 py-1">
|
||||
{state === "plan" && (
|
||||
<div className="flex flex-col items-center gap-2 py-2">
|
||||
<Loader2 className="mb-1 h-6 w-6 animate-spin text-blue-400" />
|
||||
<div className="text-center text-sm font-medium text-blue-900">
|
||||
Analyzing your request...
|
||||
</div>
|
||||
<Skeleton className="mt-1 h-3 w-1/2 rounded-full" />
|
||||
</div>
|
||||
)}
|
||||
{state === "generate" && (
|
||||
<div className="flex flex-col gap-2 py-2">
|
||||
<div className="flex items-center gap-1">
|
||||
<Loader2 className="h-4 w-4 animate-spin text-violet-400" />
|
||||
<span className="text-sm font-medium text-violet-900">
|
||||
Working on the requirement:
|
||||
</span>
|
||||
</div>
|
||||
<div className="max-h-24 overflow-auto rounded-lg border border-violet-200 bg-violet-50 px-2 py-1 text-xs">
|
||||
{requirement ? (
|
||||
<Markdown content={requirement} />
|
||||
) : (
|
||||
<span className="italic text-violet-400">
|
||||
No requirements available yet.
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
<div className="px-3 pb-2 pt-1">
|
||||
<Progress
|
||||
value={meta.progress}
|
||||
className={cn(
|
||||
"h-1 rounded-full bg-gray-200",
|
||||
state === "plan" && "bg-blue-200",
|
||||
state === "generate" && "bg-violet-200",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Component({ events }) {
|
||||
const aggregateEvents = () => {
|
||||
if (!events || events.length === 0) return null;
|
||||
return events[events.length - 1];
|
||||
};
|
||||
|
||||
const event = aggregateEvents();
|
||||
|
||||
return <ArtifactWorkflowCard event={event} />;
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { OpenAI } from "@llamaindex/openai";
|
||||
import { LlamaIndexServer } from "@llamaindex/server";
|
||||
import { Settings } from "llamaindex";
|
||||
import { workflowFactory } from "./src/app/workflow";
|
||||
|
||||
Settings.llm = new OpenAI({
|
||||
model: "gpt-4o-mini",
|
||||
});
|
||||
|
||||
new LlamaIndexServer({
|
||||
workflow: workflowFactory,
|
||||
uiConfig: {
|
||||
starterQuestions: [
|
||||
"Generate a calculator app",
|
||||
"Create a simple todo list app",
|
||||
],
|
||||
componentsDir: "components",
|
||||
},
|
||||
port: 3000,
|
||||
}).start();
|
||||
@@ -1,337 +0,0 @@
|
||||
import { artifactEvent, extractLastArtifact } from "@llamaindex/server";
|
||||
import { ChatMemoryBuffer, MessageContent, Settings } from "llamaindex";
|
||||
|
||||
import {
|
||||
agentStreamEvent,
|
||||
createStatefulMiddleware,
|
||||
createWorkflow,
|
||||
startAgentEvent,
|
||||
stopAgentEvent,
|
||||
workflowEvent,
|
||||
} from "@llamaindex/workflow";
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
export const RequirementSchema = z.object({
|
||||
next_step: z.enum(["answering", "coding"]),
|
||||
language: z.string().nullable().optional(),
|
||||
file_name: z.string().nullable().optional(),
|
||||
requirement: z.string(),
|
||||
});
|
||||
|
||||
export type Requirement = z.infer<typeof RequirementSchema>;
|
||||
|
||||
export const UIEventSchema = z.object({
|
||||
type: z.literal("ui_event"),
|
||||
data: z.object({
|
||||
state: z
|
||||
.enum(["plan", "generate", "completed"])
|
||||
.describe(
|
||||
"The current state of the workflow: 'plan', 'generate', or 'completed'.",
|
||||
),
|
||||
requirement: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"An optional requirement creating or updating a code, if applicable.",
|
||||
),
|
||||
}),
|
||||
});
|
||||
|
||||
export type UIEvent = z.infer<typeof UIEventSchema>;
|
||||
const planEvent = workflowEvent<{
|
||||
userInput: MessageContent;
|
||||
context?: string | undefined;
|
||||
}>();
|
||||
|
||||
const generateArtifactEvent = workflowEvent<{
|
||||
requirement: Requirement;
|
||||
}>();
|
||||
|
||||
const synthesizeAnswerEvent = workflowEvent<object>();
|
||||
|
||||
const uiEvent = workflowEvent<UIEvent>();
|
||||
|
||||
export function workflowFactory(reqBody: unknown) {
|
||||
const llm = Settings.llm;
|
||||
|
||||
const { withState, getContext } = createStatefulMiddleware(() => {
|
||||
return {
|
||||
memory: new ChatMemoryBuffer({ llm }),
|
||||
lastArtifact: extractLastArtifact(reqBody),
|
||||
};
|
||||
});
|
||||
const workflow = withState(createWorkflow());
|
||||
|
||||
workflow.handle([startAgentEvent], async ({ data }) => {
|
||||
const { userInput, chatHistory = [] } = data;
|
||||
// Prepare chat history
|
||||
const { state } = getContext();
|
||||
// Put user input to the memory
|
||||
if (!userInput) {
|
||||
throw new Error("Missing user input to start the workflow");
|
||||
}
|
||||
state.memory.set(chatHistory);
|
||||
state.memory.put({ role: "user", content: userInput });
|
||||
|
||||
return planEvent.with({
|
||||
userInput: userInput,
|
||||
context: state.lastArtifact
|
||||
? JSON.stringify(state.lastArtifact)
|
||||
: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
workflow.handle([planEvent], async ({ data: planData }) => {
|
||||
const { sendEvent } = getContext();
|
||||
const { state } = getContext();
|
||||
sendEvent(
|
||||
uiEvent.with({
|
||||
type: "ui_event",
|
||||
data: {
|
||||
state: "plan",
|
||||
},
|
||||
}),
|
||||
);
|
||||
const user_msg = planData.userInput;
|
||||
const context = planData.context
|
||||
? `## The context is: \n${planData.context}\n`
|
||||
: "";
|
||||
const prompt = `
|
||||
You are a product analyst responsible for analyzing the user's request and providing the next step for code or document generation.
|
||||
You are helping user with their code artifact. To update the code, you need to plan a coding step.
|
||||
|
||||
Follow these instructions:
|
||||
1. Carefully analyze the conversation history and the user's request to determine what has been done and what the next step should be.
|
||||
2. The next step must be one of the following two options:
|
||||
- "coding": To make the changes to the current code.
|
||||
- "answering": If you don't need to update the current code or need clarification from the user.
|
||||
Important: Avoid telling the user to update the code themselves, you are the one who will update the code (by planning a coding step).
|
||||
3. If the next step is "coding", you may specify the language ("typescript" or "python") and file_name if known, otherwise set them to null.
|
||||
4. The requirement must be provided clearly what is the user request and what need to be done for the next step in details
|
||||
as precise and specific as possible, don't be stingy with in the requirement.
|
||||
5. If the next step is "answering", set language and file_name to null, and the requirement should describe what to answer or explain to the user.
|
||||
6. Be concise; only return the requirements for the next step.
|
||||
7. The requirements must be in the following format:
|
||||
\`\`\`json
|
||||
{
|
||||
"next_step": "answering" | "coding",
|
||||
"language": "typescript" | "python" | null,
|
||||
"file_name": string | null,
|
||||
"requirement": string
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
## Example 1:
|
||||
User request: Create a calculator app.
|
||||
You should return:
|
||||
\`\`\`json
|
||||
{
|
||||
"next_step": "coding",
|
||||
"language": "typescript",
|
||||
"file_name": "calculator.tsx",
|
||||
"requirement": "Generate code for a calculator app that has a simple UI with a display and button layout. The display should show the current input and the result. The buttons should include basic operators, numbers, clear, and equals. The calculation should work correctly."
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
## Example 2:
|
||||
User request: Explain how the game loop works.
|
||||
Context: You have already generated the code for a snake game.
|
||||
You should return:
|
||||
\`\`\`json
|
||||
{
|
||||
"next_step": "answering",
|
||||
"language": null,
|
||||
"file_name": null,
|
||||
"requirement": "The user is asking about the game loop. Explain how the game loop works."
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
${context}
|
||||
|
||||
Now, plan the user's next step for this request:
|
||||
${user_msg}
|
||||
`;
|
||||
|
||||
const response = await llm.complete({
|
||||
prompt,
|
||||
});
|
||||
// parse the response to Requirement
|
||||
// 1. use regex to find the json block
|
||||
const jsonBlock = response.text.match(/```json\s*([\s\S]*?)\s*```/);
|
||||
if (!jsonBlock) {
|
||||
throw new Error("No JSON block found in the response.");
|
||||
}
|
||||
const requirement = RequirementSchema.parse(JSON.parse(jsonBlock[1]));
|
||||
state.memory.put({
|
||||
role: "assistant",
|
||||
content: `The plan for next step: \n${response.text}`,
|
||||
});
|
||||
|
||||
if (requirement.next_step === "coding") {
|
||||
return generateArtifactEvent.with({
|
||||
requirement,
|
||||
});
|
||||
} else {
|
||||
return synthesizeAnswerEvent.with({});
|
||||
}
|
||||
});
|
||||
|
||||
workflow.handle([generateArtifactEvent], async ({ data: planData }) => {
|
||||
const { sendEvent } = getContext();
|
||||
const { state } = getContext();
|
||||
|
||||
sendEvent(
|
||||
uiEvent.with({
|
||||
type: "ui_event",
|
||||
data: {
|
||||
state: "generate",
|
||||
requirement: planData.requirement.requirement,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const previousArtifact = state.lastArtifact
|
||||
? JSON.stringify(state.lastArtifact)
|
||||
: "There is no previous artifact";
|
||||
const requirementText = planData.requirement.requirement;
|
||||
|
||||
const prompt = `
|
||||
You are a skilled developer who can help user with coding.
|
||||
You are given a task to generate or update a code for a given requirement.
|
||||
|
||||
## Follow these instructions:
|
||||
**1. Carefully read the user's requirements.**
|
||||
If any details are ambiguous or missing, make reasonable assumptions and clearly reflect those in your output.
|
||||
If the previous code is provided:
|
||||
+ Carefully analyze the code with the request to make the right changes.
|
||||
+ Avoid making a lot of changes from the previous code if the request is not to write the code from scratch again.
|
||||
**2. For code requests:**
|
||||
- If the user does not specify a framework or language, default to a React component using the Next.js framework.
|
||||
- For Next.js, use Shadcn UI components, Typescript, @types/node, @types/react, @types/react-dom, PostCSS, and TailwindCSS.
|
||||
The import pattern should be:
|
||||
\`\`\`typescript
|
||||
import { ComponentName } from "@/components/ui/component-name"
|
||||
import { Markdown } from "@llamaindex/chat-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
\`\`\`
|
||||
- Ensure the code is idiomatic, production-ready, and includes necessary imports.
|
||||
- Only generate code relevant to the user's request—do not add extra boilerplate.
|
||||
**3. Don't be verbose on response**
|
||||
- No other text or comments only return the code which wrapped by \`\`\`language\`\`\` block.
|
||||
- If the user's request is to update the code, only return the updated code.
|
||||
**4. Only the following languages are allowed: "typescript", "python".**
|
||||
**5. If there is no code to update, return the reason without any code block.**
|
||||
|
||||
## Example:
|
||||
\`\`\`typescript
|
||||
import React from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export default function MyComponent() {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-screen">
|
||||
<Button>Click me</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
The previous code is:
|
||||
{previousArtifact}
|
||||
|
||||
Now, i have to generate the code for the following requirement:
|
||||
{requirement}
|
||||
`
|
||||
.replace("{previousArtifact}", previousArtifact)
|
||||
.replace("{requirement}", requirementText);
|
||||
|
||||
const response = await llm.complete({
|
||||
prompt,
|
||||
});
|
||||
|
||||
// Extract the code from the response
|
||||
const codeMatch = response.text.match(/```(\w+)([\s\S]*)```/);
|
||||
if (!codeMatch) {
|
||||
return synthesizeAnswerEvent.with({});
|
||||
}
|
||||
|
||||
const code = codeMatch[2].trim();
|
||||
|
||||
// Put the generated code to the memory
|
||||
state.memory.put({
|
||||
role: "assistant",
|
||||
content: `Updated the code: \n${response.text}`,
|
||||
});
|
||||
|
||||
// To show the Canvas panel for the artifact
|
||||
sendEvent(
|
||||
artifactEvent.with({
|
||||
type: "artifact",
|
||||
data: {
|
||||
type: "code",
|
||||
created_at: Date.now(),
|
||||
data: {
|
||||
language: planData.requirement.language || "",
|
||||
file_name: planData.requirement.file_name || "",
|
||||
code,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
return synthesizeAnswerEvent.with({});
|
||||
});
|
||||
|
||||
workflow.handle([synthesizeAnswerEvent], async () => {
|
||||
const { sendEvent } = getContext();
|
||||
const { state } = getContext();
|
||||
|
||||
const chatHistory = await state.memory.getMessages();
|
||||
const messages = [
|
||||
...chatHistory,
|
||||
{
|
||||
role: "system" as const,
|
||||
content: `
|
||||
You are a helpful assistant who is responsible for explaining the work to the user.
|
||||
Based on the conversation history, provide an answer to the user's question.
|
||||
The user has access to the code so avoid mentioning the whole code again in your response.
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
const responseStream = await llm.chat({
|
||||
messages,
|
||||
stream: true,
|
||||
});
|
||||
|
||||
sendEvent(
|
||||
uiEvent.with({
|
||||
type: "ui_event",
|
||||
data: {
|
||||
state: "completed",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
let response = "";
|
||||
for await (const chunk of responseStream) {
|
||||
response += chunk.delta;
|
||||
sendEvent(
|
||||
agentStreamEvent.with({
|
||||
delta: chunk.delta,
|
||||
response: "",
|
||||
currentAgentName: "assistant",
|
||||
raw: chunk,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return stopAgentEvent.with({
|
||||
result: response,
|
||||
});
|
||||
});
|
||||
|
||||
return workflow;
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { OpenAI } from "@llamaindex/openai";
|
||||
import { LlamaIndexServer } from "@llamaindex/server";
|
||||
import { agent } from "@llamaindex/workflow";
|
||||
import { Settings, tool } from "llamaindex";
|
||||
import { z } from "zod";
|
||||
|
||||
Settings.llm = new OpenAI({
|
||||
model: "gpt-4o-mini",
|
||||
});
|
||||
|
||||
const weatherAgent = agent({
|
||||
tools: [
|
||||
tool({
|
||||
name: "weather",
|
||||
description: "Get the weather in a given city",
|
||||
parameters: z.object({ city: z.string() }),
|
||||
execute: ({ city }) => `The weather in ${city} is sunny`,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
new LlamaIndexServer({
|
||||
workflow: () => weatherAgent,
|
||||
uiConfig: {
|
||||
starterQuestions: [
|
||||
"What is the weather in Tokyo?",
|
||||
"What is the weather in Ho Chi Minh City?",
|
||||
],
|
||||
layoutDir: "layout",
|
||||
},
|
||||
port: 3000,
|
||||
}).start();
|
||||
@@ -1,40 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Sparkles, Star } from "lucide-react";
|
||||
|
||||
export default function Header() {
|
||||
return (
|
||||
<div className="flex items-center justify-between p-2 px-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Sparkles className="size-4" />
|
||||
<h1 className="font-semibold">LlamaIndex App</h1>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<a
|
||||
href="https://www.llamaindex.ai/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
>
|
||||
Built by LlamaIndex
|
||||
</a>
|
||||
<img
|
||||
className="h-[24px] w-[24px] rounded-sm"
|
||||
src="/llama.png"
|
||||
alt="Llama Logo"
|
||||
/>
|
||||
</div>
|
||||
<a
|
||||
href="https://github.com/run-llama/LlamaIndexTS"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:bg-accent flex items-center gap-2 rounded-md border border-gray-300 px-2 py-1 text-sm"
|
||||
>
|
||||
<Star className="size-4" />
|
||||
Star on GitHub
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
This example shows how to use the dev mode of the server.
|
||||
|
||||
First, we need to set `devMode` to `true` in the `uiConfig` of the server.
|
||||
|
||||
```ts
|
||||
new LlamaIndexServer({
|
||||
workflow: workflowFactory,
|
||||
uiConfig: {
|
||||
devMode: true,
|
||||
},
|
||||
port: 3000,
|
||||
}).start();
|
||||
```
|
||||
|
||||
Export OpenAI API key and start the server in dev mode.
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY=<your-openai-api-key>
|
||||
npx nodemon --exec tsx index.ts --ignore src/app/workflow_*.ts
|
||||
```
|
||||
@@ -1,20 +0,0 @@
|
||||
import { OpenAI } from "@llamaindex/openai";
|
||||
import { LlamaIndexServer } from "@llamaindex/server";
|
||||
import { Settings } from "llamaindex";
|
||||
import { workflowFactory } from "./src/app/workflow";
|
||||
|
||||
Settings.llm = new OpenAI({
|
||||
model: "gpt-4o-mini",
|
||||
});
|
||||
|
||||
new LlamaIndexServer({
|
||||
workflow: workflowFactory,
|
||||
uiConfig: {
|
||||
devMode: true,
|
||||
starterQuestions: [
|
||||
"What is the weather in Tokyo?",
|
||||
"What is the weather in New York?",
|
||||
],
|
||||
},
|
||||
port: 3000,
|
||||
}).start();
|
||||
@@ -1,16 +0,0 @@
|
||||
import { agent } from "@llamaindex/workflow";
|
||||
import { tool } from "llamaindex";
|
||||
import { z } from "zod";
|
||||
|
||||
export const workflowFactory = async () => {
|
||||
return agent({
|
||||
tools: [
|
||||
tool({
|
||||
name: "weather",
|
||||
description: "Get the weather in a specific city",
|
||||
parameters: z.object({ city: z.string() }),
|
||||
execute: ({ city }) => `The weather in ${city} is sunny`,
|
||||
}),
|
||||
],
|
||||
});
|
||||
};
|
||||
@@ -1,172 +0,0 @@
|
||||
# Human in the Loop
|
||||
|
||||
This example shows how to use the LlamaIndexServer with a human in the loop. It allows you to start CLI commands that are reviewed by a human before execution.
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Environment Setup
|
||||
|
||||
Export your OpenAI API key:
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY=<your-openai-api-key>
|
||||
```
|
||||
|
||||
### Starting the Server
|
||||
|
||||
Run the server in development mode:
|
||||
|
||||
```bash
|
||||
npx nodemon --exec tsx index.ts --ignore output/*
|
||||
```
|
||||
|
||||
### Access the Application
|
||||
|
||||
Open your browser and go to:
|
||||
|
||||
```
|
||||
http://localhost:3000
|
||||
```
|
||||
|
||||
You will see the LlamaIndexServer UI, where you can interact with the HITL agent. Try "List all files in the current directory" and see how the agent pauses and waits for a human response before executing the command.
|
||||
|
||||
## How does HITL work?
|
||||
|
||||
### Events
|
||||
|
||||
The human-in-the-loop approach used here is based on a simple idea: the workflow pauses and waits for a human response before proceeding to the next step.
|
||||
|
||||
To do this, you will need to implement two custom events:
|
||||
|
||||
- [HumanInputEvent](https://github.com/run-llama/create-llama/blob/main/packages/server/src/utils/hitl/events.ts): This event is used to request input from the user.
|
||||
- [HumanResponseEvent](https://github.com/run-llama/create-llama/blob/main/packages/server/src/utils/hitl/events.ts): This event is sent to the workflow to resume execution with input from the user.
|
||||
|
||||
In this example, we have implemented these two custom events in [`events.ts`](src/app/events.ts):
|
||||
|
||||
- `cliHumanInputEvent` – to request input from the user for CLI command execution.
|
||||
- `cliHumanResponseEvent` – to resume the workflow with the response from the user.
|
||||
|
||||
```typescript
|
||||
export const cliHumanInputEvent = humanInputEvent<{
|
||||
type: "cli_human_input";
|
||||
data: { command: string };
|
||||
response: typeof cliHumanResponseEvent;
|
||||
}>();
|
||||
|
||||
export const cliHumanResponseEvent = humanResponseEvent<{
|
||||
type: "human_response";
|
||||
data: { execute: boolean; command: string };
|
||||
}>();
|
||||
```
|
||||
|
||||
### UI Component
|
||||
|
||||
HITL also needs a custom UI component, that is shown when the LlamaIndexServer receives the `cliHumanInputEvent`. The name of the component is defined in the `type` field of the `cliHumanInputEvent` - in our case, it is `cli_human_input`, which corresponds to the [cli_human_input.tsx](./components/cli_human_input.tsx) component.
|
||||
|
||||
The custom component must use `append` to send a message with a `human_response` annotation. The data of the annotation must be in the format of the response event `cliHumanResponseEvent`, in our case, for sending to execute the command `ls -l`, we would send:
|
||||
|
||||
```tsx
|
||||
append({
|
||||
content: "Yes",
|
||||
role: "user",
|
||||
annotations: [
|
||||
{
|
||||
type: "human_response",
|
||||
data: {
|
||||
execute: true,
|
||||
command: "ls -l", // The command to execute
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
This component displays the command to execute and the user can choose to execute or cancel the command execution.
|
||||
|
||||
### Workflow Implementation
|
||||
|
||||
The workflow is implemented in [`workflow.ts`](src/app/workflow.ts) using LlamaIndex workflows. The workflow handles three main steps:
|
||||
|
||||
1. **Initial Request Handling**: When a user input is received, the workflow uses `chatWithTools` to determine if a CLI command should be executed. If so, it emits a `cliHumanInputEvent` to request user permission.
|
||||
|
||||
```typescript
|
||||
workflow.handle([startAgentEvent], async ({ data }) => {
|
||||
const { userInput, chatHistory = [] } = data;
|
||||
|
||||
const toolCallResponse = await chatWithTools(
|
||||
llm,
|
||||
[cliExecutor],
|
||||
chatHistory.concat({ role: "user", content: userInput }),
|
||||
);
|
||||
|
||||
const cliExecutorToolCall = toolCallResponse.toolCalls.find(
|
||||
(toolCall) => toolCall.name === cliExecutor.metadata.name,
|
||||
);
|
||||
|
||||
const command = cliExecutorToolCall?.input?.command as string;
|
||||
if (command) {
|
||||
return cliHumanInputEvent.with({
|
||||
type: "cli_human_input",
|
||||
data: { command },
|
||||
response: cliHumanResponseEvent,
|
||||
});
|
||||
}
|
||||
|
||||
return summaryEvent.with("");
|
||||
});
|
||||
```
|
||||
|
||||
2. **Human Response Handling**: After receiving human input, the workflow either executes the command or cancels based on the user's choice.
|
||||
|
||||
```typescript
|
||||
workflow.handle([cliHumanResponseEvent], async ({ data }) => {
|
||||
const { command, execute } = data.data;
|
||||
|
||||
if (!execute) {
|
||||
return summaryEvent.with(`User reject to execute the command ${command}`);
|
||||
}
|
||||
|
||||
const result = (await cliExecutor.call({ command })) as string;
|
||||
|
||||
return summaryEvent.with(
|
||||
`Executed the command ${command} and got the result: ${result}`,
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
3. **Final Response**: The workflow generates a final response based on the execution result and streams it back to the user.
|
||||
|
||||
### Tools
|
||||
|
||||
The CLI executor tool is defined in [`tools.ts`](src/app/tools.ts):
|
||||
|
||||
```typescript
|
||||
export const cliExecutor = tool({
|
||||
name: "cli_executor",
|
||||
description: "This tool executes a command and returns the output.",
|
||||
parameters: z.object({ command: z.string() }),
|
||||
execute: async ({ command }) => {
|
||||
try {
|
||||
const output = execSync(command, {
|
||||
encoding: "utf-8",
|
||||
});
|
||||
return output;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return "Command failed";
|
||||
}
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
The HITL implementation consists of:
|
||||
|
||||
1. **Workflow Factory** (`workflow.ts`): Creates and configures the workflow with event handlers
|
||||
2. **Events** (`events.ts`): Defines typed events for human input and response
|
||||
3. **Tools** (`tools.ts`): Implements the CLI executor tool
|
||||
4. **UI Component** (`components/cli_human_input.tsx`): Provides the user interface for human approval
|
||||
5. **Server Entry** (`index.ts`): Configures and starts the LlamaIndexServer
|
||||
|
||||
This architecture ensures that dangerous operations like CLI command execution require explicit human approval before proceeding.
|
||||
@@ -1,95 +0,0 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardFooter } from "@/components/ui/card";
|
||||
import { JSONValue, useChatUI } from "@llamaindex/chat-ui";
|
||||
import React, { FC, useState } from "react";
|
||||
import { z } from "zod";
|
||||
|
||||
// This schema is equivalent to the CLICommand model defined in events.py
|
||||
const CLIInputEventSchema = z.object({
|
||||
command: z.string(),
|
||||
});
|
||||
type CLIInputEvent = z.infer<typeof CLIInputEventSchema>;
|
||||
|
||||
const CLIHumanInput: FC<{
|
||||
events: JSONValue[];
|
||||
}> = ({ events }) => {
|
||||
const inputEvent = (events || [])
|
||||
.map((ev) => {
|
||||
const parseResult = CLIInputEventSchema.safeParse(ev);
|
||||
return parseResult.success ? parseResult.data : null;
|
||||
})
|
||||
.filter((ev): ev is CLIInputEvent => ev !== null)
|
||||
.at(-1);
|
||||
|
||||
const { append } = useChatUI();
|
||||
const [confirmedValue, setConfirmedValue] = useState<boolean | null>(null);
|
||||
const [editableCommand, setEditableCommand] = useState<string | undefined>(
|
||||
inputEvent?.command,
|
||||
);
|
||||
|
||||
// Update editableCommand if inputEvent changes (e.g. new event comes in)
|
||||
React.useEffect(() => {
|
||||
setEditableCommand(inputEvent?.command);
|
||||
}, [inputEvent?.command]);
|
||||
|
||||
const handleConfirm = () => {
|
||||
append({
|
||||
content: "Yes",
|
||||
role: "user",
|
||||
annotations: [
|
||||
{
|
||||
type: "human_response",
|
||||
data: {
|
||||
execute: true,
|
||||
command: editableCommand, // Use editable command
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
setConfirmedValue(true);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
append({
|
||||
content: "No",
|
||||
role: "user",
|
||||
annotations: [
|
||||
{
|
||||
type: "human_response",
|
||||
data: {
|
||||
execute: false,
|
||||
command: inputEvent?.command,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
setConfirmedValue(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="my-4">
|
||||
<CardContent className="pt-6">
|
||||
<p className="text-sm text-gray-700">
|
||||
Do you want to execute the following command?
|
||||
</p>
|
||||
<input
|
||||
disabled
|
||||
type="text"
|
||||
value={editableCommand || ""}
|
||||
onChange={(e) => setEditableCommand(e.target.value)}
|
||||
className="my-2 w-full overflow-x-auto rounded border border-gray-300 bg-gray-100 p-3 font-mono text-xs text-gray-800"
|
||||
/>
|
||||
</CardContent>
|
||||
{confirmedValue === null ? (
|
||||
<CardFooter className="flex justify-end gap-2">
|
||||
<>
|
||||
<Button onClick={handleConfirm}>Yes</Button>
|
||||
<Button onClick={handleCancel}>No</Button>
|
||||
</>
|
||||
</CardFooter>
|
||||
) : null}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default CLIHumanInput;
|
||||
@@ -1,20 +0,0 @@
|
||||
import { OpenAI } from "@llamaindex/openai";
|
||||
import { LlamaIndexServer } from "@llamaindex/server";
|
||||
import { Settings } from "llamaindex";
|
||||
import { workflowFactory } from "./src/app/workflow";
|
||||
|
||||
Settings.llm = new OpenAI({
|
||||
model: "gpt-4o-mini",
|
||||
});
|
||||
|
||||
new LlamaIndexServer({
|
||||
workflow: workflowFactory,
|
||||
uiConfig: {
|
||||
starterQuestions: [
|
||||
"Check status of git in the current directory",
|
||||
"List all files in the current directory",
|
||||
],
|
||||
componentsDir: "components",
|
||||
},
|
||||
port: 3000,
|
||||
}).start();
|
||||
@@ -1,12 +0,0 @@
|
||||
import { humanInputEvent, humanResponseEvent } from "@llamaindex/server";
|
||||
|
||||
export const cliHumanInputEvent = humanInputEvent<{
|
||||
type: "cli_human_input";
|
||||
data: { command: string };
|
||||
response: typeof cliHumanResponseEvent;
|
||||
}>();
|
||||
|
||||
export const cliHumanResponseEvent = humanResponseEvent<{
|
||||
type: "human_response";
|
||||
data: { execute: boolean; command: string };
|
||||
}>();
|
||||
@@ -1,20 +0,0 @@
|
||||
import { execSync } from "child_process";
|
||||
import { tool } from "llamaindex";
|
||||
import { z } from "zod";
|
||||
|
||||
export const cliExecutor = tool({
|
||||
name: "cli_executor",
|
||||
description: "This tool executes a command and returns the output.",
|
||||
parameters: z.object({ command: z.string() }),
|
||||
execute: async ({ command }) => {
|
||||
try {
|
||||
const output = execSync(command, {
|
||||
encoding: "utf-8",
|
||||
});
|
||||
return output;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return "Command failed";
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -1,106 +0,0 @@
|
||||
import { OpenAI } from "@llamaindex/openai";
|
||||
import { toAgentRunEvent, writeResponseToStream } from "@llamaindex/server";
|
||||
import { chatWithTools } from "@llamaindex/tools";
|
||||
import {
|
||||
createWorkflow,
|
||||
getContext,
|
||||
startAgentEvent,
|
||||
stopAgentEvent,
|
||||
withSnapshot,
|
||||
workflowEvent,
|
||||
} from "@llamaindex/workflow";
|
||||
import { ChatMessage, Settings, ToolCallLLM } from "llamaindex";
|
||||
import { cliHumanInputEvent, cliHumanResponseEvent } from "./events";
|
||||
import { cliExecutor } from "./tools";
|
||||
|
||||
Settings.llm = new OpenAI({
|
||||
model: "gpt-4o-mini",
|
||||
});
|
||||
|
||||
const summaryEvent = workflowEvent<string>(); // simple event to summarize the result
|
||||
|
||||
export const workflowFactory = (body: unknown) => {
|
||||
const llm = Settings.llm as ToolCallLLM;
|
||||
|
||||
if (!llm.supportToolCall) {
|
||||
throw new Error("LLM is not a ToolCallLLM");
|
||||
}
|
||||
|
||||
const { messages } = body as { messages: ChatMessage[] };
|
||||
|
||||
const workflow = withSnapshot(createWorkflow());
|
||||
|
||||
workflow.handle([startAgentEvent], async ({ data }) => {
|
||||
const { userInput, chatHistory = [] } = data;
|
||||
if (!userInput) {
|
||||
throw new Error("User input is required");
|
||||
}
|
||||
|
||||
// in this example, we use chatWithTools to decide should perform a tool call or not
|
||||
// if cli executor is called, emit HumanInputEvent to ask user for permission
|
||||
const toolCallResponse = await chatWithTools(
|
||||
llm,
|
||||
[cliExecutor],
|
||||
chatHistory.concat({ role: "user", content: userInput }),
|
||||
);
|
||||
const cliExecutorToolCall = toolCallResponse.toolCalls.find(
|
||||
(toolCall) => toolCall.name === cliExecutor.metadata.name,
|
||||
);
|
||||
const command = cliExecutorToolCall?.input?.command as string;
|
||||
if (command) {
|
||||
return cliHumanInputEvent.with({
|
||||
type: "cli_human_input",
|
||||
data: { command },
|
||||
response: cliHumanResponseEvent,
|
||||
});
|
||||
}
|
||||
|
||||
// if no tool call, just response as normal
|
||||
return summaryEvent.with("");
|
||||
});
|
||||
|
||||
// do actions after getting response from human
|
||||
workflow.handle([cliHumanResponseEvent], async ({ data }) => {
|
||||
const { sendEvent } = getContext();
|
||||
const { command, execute } = data.data;
|
||||
|
||||
if (!execute) {
|
||||
// stop the workflow if user reject to execute the command
|
||||
return summaryEvent.with(`User reject to execute the command ${command}`);
|
||||
}
|
||||
|
||||
sendEvent(
|
||||
toAgentRunEvent({
|
||||
agent: "CLI Executor",
|
||||
text: `Execute the command "${command}" and return the result`,
|
||||
type: "text",
|
||||
}),
|
||||
);
|
||||
|
||||
const result = (await cliExecutor.call({ command })) as string;
|
||||
|
||||
return summaryEvent.with(
|
||||
`Executed the command ${command} and got the result: ${result}`,
|
||||
);
|
||||
});
|
||||
|
||||
workflow.handle([summaryEvent], async ({ data: summaryResult }) => {
|
||||
const { sendEvent } = getContext();
|
||||
|
||||
const chatHistory = messages;
|
||||
if (summaryResult) {
|
||||
chatHistory.push({ role: "user", content: summaryResult });
|
||||
}
|
||||
|
||||
const stream = await llm.chat({
|
||||
messages: chatHistory,
|
||||
stream: true,
|
||||
});
|
||||
|
||||
const result = await writeResponseToStream(stream, sendEvent);
|
||||
|
||||
return stopAgentEvent.with({ result });
|
||||
});
|
||||
|
||||
return workflow;
|
||||
};
|
||||
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"name": "llamaindex-server-examples",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit",
|
||||
"dev": "nodemon --exec tsx simple-workflow/calculator.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@llamaindex/openai": "~0.4.0",
|
||||
"@llamaindex/readers": "~3.1.4",
|
||||
"@llamaindex/server": "workspace:*",
|
||||
"@llamaindex/tools": "~0.1.2",
|
||||
"dotenv": "^16.4.7",
|
||||
"llamaindex": "~0.11.0",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.3",
|
||||
"nodemon": "^3.1.10",
|
||||
"tsx": "4.7.2",
|
||||
"typescript": "^5.3.2"
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
# Upload File Example
|
||||
|
||||
This example shows how to use the uploaded file (private file) from the user in the workflow.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Please follow the setup instructions in the [examples README](../README.md).
|
||||
|
||||
You will also need:
|
||||
|
||||
- An OpenAI API key
|
||||
- The `enableFileUpload` option in the `uiConfig` is set to `true`.
|
||||
|
||||
```typescript
|
||||
new LlamaIndexServer({
|
||||
// ... other options
|
||||
uiConfig: { enableFileUpload: true },
|
||||
}).start();
|
||||
```
|
||||
|
||||
## How to get the uploaded files in your workflow:
|
||||
|
||||
In LlamaIndexServer, the uploaded file is included in chat message annotations. You can easily get the uploaded files from chat messages using the [extractFileAttachments](https://github.com/llamaindex/llamaindex/blob/main/packages/server/src/utils/events.ts) function.
|
||||
|
||||
```typescript
|
||||
import { type Message } from "ai";
|
||||
import { extractFileAttachments } from "@llamaindex/server";
|
||||
|
||||
async function workflowFactory(reqBody: { messages: Message[] }) {
|
||||
const attachments = extractFileAttachments(reqBody.messages);
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### AgentWorkflow
|
||||
|
||||
If you are using AgentWorkflow, to provide file access to the agent, you can create a tool to read the file content. We recommend to use the `fileId` as the parameter of the tool instead of the `filePath` to avoid showing internal file path to the user. You can use the `getStoredFilePath` helper function to get the file path from the file id.
|
||||
|
||||
```typescript
|
||||
import { getStoredFilePath, extractFileAttachments } from "@llamaindex/server";
|
||||
|
||||
const readFileTool = tool(
|
||||
({ fileId }) => {
|
||||
// Get the file path from the file id
|
||||
const filePath = getStoredFilePath({ id: fileId });
|
||||
return fsPromises.readFile(filePath, "utf8");
|
||||
},
|
||||
{
|
||||
name: "read_file",
|
||||
description: `Use this tool with the file id to read the file content. The available file are: [${attachments.map((file) => file.id).join(", ")}]`,
|
||||
parameters: z.object({
|
||||
fileId: z.string(),
|
||||
}),
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
**Tip:** You can either put the attachments file information to the tool description or agent's system prompt.
|
||||
|
||||
Check: [agent-workflow.ts](./agent-workflow.ts) for the full example.
|
||||
|
||||
### Custom Workflow
|
||||
|
||||
In custom workflow, instead of defining a tool, you can use the helper functions (`extractFileAttachments` and `getStoredFilePath`) to work with file attachments in your workflow.
|
||||
|
||||
Check: [custom-workflow.ts](./custom-workflow.ts) for the full example.
|
||||
|
||||
> To run custom workflow example, update the `index.ts` file to use the `workflowFactory` from `custom-workflow.ts` instead of `agent-workflow.ts`.
|
||||
@@ -1,39 +0,0 @@
|
||||
import { extractFileAttachments, getStoredFilePath } from "@llamaindex/server";
|
||||
import { agent } from "@llamaindex/workflow";
|
||||
import { type Message } from "ai";
|
||||
import { tool } from "llamaindex";
|
||||
import { promises as fsPromises } from "node:fs";
|
||||
import { z } from "zod";
|
||||
|
||||
export const workflowFactory = async (reqBody: { messages: Message[] }) => {
|
||||
const { messages } = reqBody;
|
||||
// Extract the files from the messages
|
||||
const files = extractFileAttachments(messages);
|
||||
const fileIds = files.map((file) => file.id);
|
||||
|
||||
// Define a tool to read the file content using the id
|
||||
const readFileTool = tool(
|
||||
({ fileId }) => {
|
||||
if (!fileIds.includes(fileId)) {
|
||||
throw new Error(`File with id ${fileId} not found`);
|
||||
}
|
||||
|
||||
const filePath = getStoredFilePath({ id: fileId });
|
||||
return fsPromises.readFile(filePath, "utf8");
|
||||
},
|
||||
{
|
||||
name: "read_file",
|
||||
description: `Use this tool with the id of the file to read the file content. Here are the available file ids: [${fileIds.join(", ")}]`,
|
||||
parameters: z.object({
|
||||
fileId: z.string(),
|
||||
}),
|
||||
},
|
||||
);
|
||||
return agent({
|
||||
tools: [readFileTool],
|
||||
systemPrompt: `
|
||||
You are a helpful assistant that can help the user with their file.
|
||||
You can use the read_file tool to read the file content.
|
||||
`,
|
||||
});
|
||||
};
|
||||
@@ -1,98 +0,0 @@
|
||||
import { extractFileAttachments } from "@llamaindex/server";
|
||||
import { ChatMemoryBuffer, MessageContent, Settings } from "llamaindex";
|
||||
|
||||
import {
|
||||
agentStreamEvent,
|
||||
createStatefulMiddleware,
|
||||
createWorkflow,
|
||||
startAgentEvent,
|
||||
stopAgentEvent,
|
||||
workflowEvent,
|
||||
} from "@llamaindex/workflow";
|
||||
import { Message } from "ai";
|
||||
import { promises as fsPromises } from "node:fs";
|
||||
|
||||
const fileHelperEvent = workflowEvent<{
|
||||
userInput: MessageContent;
|
||||
fileContent: string;
|
||||
}>();
|
||||
|
||||
/**
|
||||
* This is an simple workflow to demonstrate how to use uploaded files in the workflow.
|
||||
*/
|
||||
export function workflowFactory(reqBody: { messages: Message[] }) {
|
||||
const llm = Settings.llm;
|
||||
|
||||
// First, extract the uploaded file from the messages
|
||||
const attachments = extractFileAttachments(reqBody.messages);
|
||||
|
||||
if (attachments.length === 0) {
|
||||
throw new Error("Please upload a file to start");
|
||||
}
|
||||
|
||||
// Then, add the uploaded file info to the workflow state
|
||||
const { withState, getContext } = createStatefulMiddleware(() => {
|
||||
return {
|
||||
memory: new ChatMemoryBuffer({ llm }),
|
||||
uploadedFile: attachments[attachments.length - 1],
|
||||
};
|
||||
});
|
||||
const workflow = withState(createWorkflow());
|
||||
|
||||
// Handle the start of the workflow: read the file content
|
||||
workflow.handle([startAgentEvent], async ({ data }) => {
|
||||
const { userInput } = data;
|
||||
// Prepare chat history
|
||||
const { state } = getContext();
|
||||
if (!userInput) {
|
||||
throw new Error("Missing user input to start the workflow");
|
||||
}
|
||||
state.memory.put({ role: "user", content: userInput });
|
||||
|
||||
// Read file content
|
||||
const fileContent = await fsPromises.readFile(
|
||||
state.uploadedFile.path,
|
||||
"utf8",
|
||||
);
|
||||
|
||||
return fileHelperEvent.with({
|
||||
userInput,
|
||||
fileContent,
|
||||
});
|
||||
});
|
||||
|
||||
// Use LLM to help the user with the file content
|
||||
workflow.handle([fileHelperEvent], async ({ data }) => {
|
||||
const { sendEvent } = getContext();
|
||||
|
||||
const prompt = `
|
||||
You are a helpful assistant that can help the user with their file.
|
||||
|
||||
Here is the provided file content:
|
||||
${data.fileContent}
|
||||
|
||||
Now, let help the user with this request:
|
||||
${data.userInput}
|
||||
`;
|
||||
|
||||
const response = await llm.complete({
|
||||
prompt,
|
||||
stream: true,
|
||||
});
|
||||
|
||||
// Stream the response
|
||||
for await (const chunk of response) {
|
||||
sendEvent(
|
||||
agentStreamEvent.with({
|
||||
delta: chunk.text,
|
||||
response: chunk.text,
|
||||
currentAgentName: "agent",
|
||||
raw: chunk.raw,
|
||||
}),
|
||||
);
|
||||
}
|
||||
sendEvent(stopAgentEvent.with({ result: "" }));
|
||||
});
|
||||
|
||||
return workflow;
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { OpenAI, OpenAIEmbedding } from "@llamaindex/openai";
|
||||
import { LlamaIndexServer } from "@llamaindex/server";
|
||||
import { Settings } from "llamaindex";
|
||||
import { workflowFactory } from "./agent-workflow";
|
||||
// Uncomment this to use a custom workflow
|
||||
// import { workflowFactory } from "./custom-workflow";
|
||||
|
||||
Settings.llm = new OpenAI({
|
||||
model: "gpt-4o-mini",
|
||||
});
|
||||
|
||||
Settings.embedModel = new OpenAIEmbedding({
|
||||
model: "text-embedding-3-small",
|
||||
});
|
||||
|
||||
new LlamaIndexServer({
|
||||
workflow: workflowFactory,
|
||||
suggestNextQuestions: false,
|
||||
uiConfig: {
|
||||
enableFileUpload: true,
|
||||
},
|
||||
port: 3000,
|
||||
}).start();
|
||||
@@ -1,28 +0,0 @@
|
||||
import { OpenAI } from "@llamaindex/openai";
|
||||
import { LlamaIndexServer } from "@llamaindex/server";
|
||||
import { agent } from "@llamaindex/workflow";
|
||||
import { Settings, tool } from "llamaindex";
|
||||
import { z } from "zod";
|
||||
|
||||
Settings.llm = new OpenAI({
|
||||
model: "gpt-4o-mini",
|
||||
});
|
||||
|
||||
const calculatorAgent = agent({
|
||||
tools: [
|
||||
tool({
|
||||
name: "add",
|
||||
description: "Adds two numbers",
|
||||
parameters: z.object({ x: z.number(), y: z.number() }),
|
||||
execute: ({ x, y }) => x + y,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
new LlamaIndexServer({
|
||||
workflow: () => calculatorAgent,
|
||||
uiConfig: {
|
||||
starterQuestions: ["1 + 1", "2 + 2"],
|
||||
},
|
||||
port: 3000,
|
||||
}).start();
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "bundler",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["**/*"],
|
||||
"exclude": ["node_modules", "dist", "custom-layout/layout", "hitl/components"]
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
This is a [LlamaIndex](https://www.llamaindex.ai/) project using [Next.js](https://nextjs.org/) that is ejected from [`llamaindex-server`](https://github.com/run-llama/create-llama/tree/main/packages/server) via `npm eject` command.
|
||||
|
||||
## Quick Start
|
||||
|
||||
As this is a Next.js project, you can use the following commands to start the development server:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
## Useful Commands
|
||||
|
||||
- Generate Datasource (in case you're having a `./data` folder): `npm run generate`
|
||||
- Typecheck: `npm run typecheck`
|
||||
- Lint: `npm run lint`
|
||||
- Format: `npm run format`
|
||||
- Build & Start: `npm run build && npm run start`
|
||||
|
||||
## Deployment
|
||||
|
||||
The project can be deployed to any platform that supports Next.js like Vercel.
|
||||
|
||||
## Configuration
|
||||
|
||||
Your original [`llamaindex-server`](https://github.com/run-llama/create-llama/tree/main/packages/server#configuration-options) configurations have been migrated to a [`.env`](.env) file.
|
||||
|
||||
Changing the `.env` file will change the behavior of the application, e.g. for changing the initial questions to display in the chat, you can do:
|
||||
|
||||
```
|
||||
NEXT_PUBLIC_STARTER_QUESTIONS=['What is the capital of France?']
|
||||
```
|
||||
|
||||
Alternatively, you can also change the file referencing `process.env.NEXT_PUBLIC_STARTER_QUESTIONS` directly in the source code.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about LlamaIndex, take a look at the following resources:
|
||||
|
||||
- [LlamaIndex Documentation](https://docs.llamaindex.ai) - learn about LlamaIndex (Python features).
|
||||
- [LlamaIndexTS Documentation](https://ts.llamaindex.ai) - learn about LlamaIndex (Typescript features).
|
||||
|
||||
You can check out [the LlamaIndexTS GitHub repository](https://github.com/run-llama/LlamaIndexTS) - your feedback and contributions are welcome!
|
||||
@@ -1,32 +0,0 @@
|
||||
import { getEnv } from "@llamaindex/env";
|
||||
import { LLamaCloudFileService } from "llamaindex";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
export async function GET(request: NextRequest): Promise<NextResponse> {
|
||||
if (!getEnv("LLAMA_CLOUD_API_KEY")) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "env variable LLAMA_CLOUD_API_KEY is required to use LlamaCloud",
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const config = {
|
||||
projects: await LLamaCloudFileService.getAllProjectsWithPipelines(),
|
||||
pipeline: {
|
||||
pipeline: getEnv("LLAMA_CLOUD_INDEX_NAME"),
|
||||
project: getEnv("LLAMA_CLOUD_PROJECT_NAME"),
|
||||
},
|
||||
};
|
||||
return NextResponse.json(config, { status: 200 });
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "Failed to fetch LlamaCloud configuration",
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
import { type Message } from "ai";
|
||||
import { type MessageType } from "llamaindex";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
// import chat utils
|
||||
import {
|
||||
getHumanResponsesFromMessage,
|
||||
pauseForHumanInput,
|
||||
processWorkflowStream,
|
||||
runWorkflow,
|
||||
sendSuggestedQuestionsEvent,
|
||||
toDataStream,
|
||||
} from "./utils";
|
||||
|
||||
// import workflow factory and settings from local file
|
||||
import { stopAgentEvent } from "@llamaindex/workflow";
|
||||
import { initSettings } from "./app/settings";
|
||||
import { workflowFactory } from "./app/workflow";
|
||||
|
||||
initSettings();
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const reqBody = await req.json();
|
||||
const suggestNextQuestions = process.env.SUGGEST_NEXT_QUESTIONS === "true";
|
||||
|
||||
const { messages, id: requestId } = reqBody as {
|
||||
messages: Message[];
|
||||
id?: string;
|
||||
};
|
||||
const chatHistory = messages.map((message) => ({
|
||||
role: message.role as MessageType,
|
||||
content: message.content,
|
||||
}));
|
||||
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
if (lastMessage?.role !== "user") {
|
||||
return NextResponse.json(
|
||||
{
|
||||
detail: "Messages cannot be empty and last message must be from user",
|
||||
},
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const abortController = new AbortController();
|
||||
req.signal.addEventListener("abort", () =>
|
||||
abortController.abort("Connection closed"),
|
||||
);
|
||||
|
||||
const context = await runWorkflow({
|
||||
workflow: await workflowFactory(reqBody),
|
||||
input: { userInput: lastMessage.content, chatHistory },
|
||||
human: {
|
||||
snapshotId: requestId, // use requestId to restore snapshot
|
||||
responses: getHumanResponsesFromMessage(lastMessage),
|
||||
},
|
||||
});
|
||||
|
||||
const stream = processWorkflowStream(context.stream).until(
|
||||
(event) =>
|
||||
abortController.signal.aborted || stopAgentEvent.include(event),
|
||||
);
|
||||
|
||||
const dataStream = toDataStream(stream, {
|
||||
callbacks: {
|
||||
onPauseForHumanInput: async (responseEvent) => {
|
||||
await pauseForHumanInput(context, responseEvent, requestId); // use requestId to save snapshot
|
||||
},
|
||||
onFinal: async (completion, dataStreamWriter) => {
|
||||
chatHistory.push({
|
||||
role: "assistant" as MessageType,
|
||||
content: completion,
|
||||
});
|
||||
if (suggestNextQuestions) {
|
||||
await sendSuggestedQuestionsEvent(dataStreamWriter, chatHistory);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
return new Response(dataStream, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "text/plain; charset=utf-8",
|
||||
"X-Vercel-AI-Data-Stream": "v1",
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Chat handler error:", error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
detail: (error as Error).message || "Internal server error",
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { NextRequest } from "next/server";
|
||||
import { handleComponentRoute } from "../shared/component-handler";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const params = request.nextUrl.searchParams;
|
||||
const directory =
|
||||
params.get("componentsDir") || process.env.COMPONENTS_DIR || "components";
|
||||
return handleComponentRoute(directory);
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
import { exec } from "child_process";
|
||||
import fs from "fs";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import path from "path";
|
||||
import { promisify } from "util";
|
||||
|
||||
const DEFAULT_WORKFLOW_FILE_PATH =
|
||||
process.env.WORKFLOW_FILE_PATH || "src/app/workflow.ts";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const filePath = DEFAULT_WORKFLOW_FILE_PATH;
|
||||
|
||||
const fileExists = await promisify(fs.exists)(DEFAULT_WORKFLOW_FILE_PATH);
|
||||
if (!fileExists) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
detail: `Dev mode is currently in beta. It only supports updating workflow file at ${filePath}`,
|
||||
},
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
const content = await promisify(fs.readFile)(filePath, "utf-8");
|
||||
const last_modified = fs.statSync(filePath).mtime.getTime();
|
||||
|
||||
return NextResponse.json(
|
||||
{ content, file_path: filePath, last_modified },
|
||||
{ status: 200 },
|
||||
);
|
||||
}
|
||||
|
||||
export async function PUT(request: NextRequest) {
|
||||
const filePath = DEFAULT_WORKFLOW_FILE_PATH;
|
||||
const { content } = await request.json();
|
||||
|
||||
const fileExists = await promisify(fs.exists)(filePath);
|
||||
if (!fileExists) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
detail: `Dev mode is currently in beta. It only supports updating workflow file at ${DEFAULT_WORKFLOW_FILE_PATH}`,
|
||||
},
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const resolvedFilePath = path.resolve(DEFAULT_WORKFLOW_FILE_PATH);
|
||||
const result = await validateTypeScriptFile(resolvedFilePath, content);
|
||||
|
||||
if (!result.isValid) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
detail: result.errors.join("\n"),
|
||||
},
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
await promisify(fs.writeFile)(filePath, content);
|
||||
return NextResponse.json({ content }, { status: 200 });
|
||||
} catch (error) {
|
||||
console.error("Error updating workflow file:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to update workflow file" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// use typescript package to validate the file syntax and imports
|
||||
async function validateTypeScriptFile(filePath: string, content: string) {
|
||||
// Update workflow file directly will cause the server restart immediately.
|
||||
// So we create a temporary file with the same content in the same directory as the workflow file
|
||||
// This file will be used to validate the file syntax and imports. It will be deleted after validation.
|
||||
const tempFilePath = path.join(
|
||||
path.dirname(filePath),
|
||||
`workflow_${Date.now()}.ts`,
|
||||
);
|
||||
fs.writeFileSync(tempFilePath, content);
|
||||
|
||||
const errors = [];
|
||||
try {
|
||||
const tscCommand = `npx tsc ${tempFilePath} --noEmit --skipLibCheck true`;
|
||||
await promisify(exec)(tscCommand);
|
||||
} catch (error) {
|
||||
const errorMessage = (error as { stdout: string })?.stdout;
|
||||
errors.push(errorMessage);
|
||||
} finally {
|
||||
// Clean up temporary file
|
||||
if (fs.existsSync(tempFilePath)) fs.unlinkSync(tempFilePath);
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors: errors,
|
||||
};
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
import fs from "fs";
|
||||
import { LLamaCloudFileService } from "llamaindex";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { promisify } from "util";
|
||||
import { downloadFile } from "../helpers";
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ slug: string[] }> },
|
||||
) {
|
||||
const { searchParams } = request.nextUrl;
|
||||
const isUsingLlamaCloud = searchParams.get("useLlamaCloud") === "true";
|
||||
const filePath = (await params).slug.join("/");
|
||||
|
||||
if (!filePath.startsWith("output") && !filePath.startsWith("data")) {
|
||||
return NextResponse.json({ error: "No permission" }, { status: 400 });
|
||||
}
|
||||
|
||||
const decodedFilePath = decodeURIComponent(filePath);
|
||||
|
||||
// if using llama cloud and file not exists, download it
|
||||
if (isUsingLlamaCloud) {
|
||||
const fileExists = await promisify(fs.exists)(decodedFilePath);
|
||||
if (!fileExists) {
|
||||
const { pipeline_id, file_name } =
|
||||
getLlamaCloudPipelineIdAndFileName(decodedFilePath);
|
||||
|
||||
if (pipeline_id && file_name) {
|
||||
// get the file url from llama cloud
|
||||
const downloadUrl = await LLamaCloudFileService.getFileUrl(
|
||||
pipeline_id,
|
||||
file_name,
|
||||
);
|
||||
if (!downloadUrl) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: `Cannot create LlamaCloud download url for pipeline_id=${pipeline_id}, file_name=${file_name}`,
|
||||
},
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
// download the LlamaCloud file to local
|
||||
await downloadFile(downloadUrl, decodedFilePath);
|
||||
console.log("File downloaded successfully to: ", decodedFilePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fileExists = await promisify(fs.exists)(decodedFilePath);
|
||||
if (fileExists) {
|
||||
const fileBuffer = await promisify(fs.readFile)(decodedFilePath);
|
||||
return new NextResponse(fileBuffer);
|
||||
} else {
|
||||
return NextResponse.json({ error: "File not found" }, { status: 404 });
|
||||
}
|
||||
}
|
||||
|
||||
function getLlamaCloudPipelineIdAndFileName(filePath: string) {
|
||||
const fileName = filePath.split("/").pop() ?? ""; // fileName is the last slug part (pipeline_id$file_name)
|
||||
|
||||
const delimiterIndex = fileName.indexOf("$"); // delimiter is the first dollar sign in the fileName
|
||||
if (delimiterIndex === -1) {
|
||||
return { pipeline_id: "", file_name: "" };
|
||||
}
|
||||
|
||||
const pipeline_id = fileName.slice(0, delimiterIndex); // before delimiter
|
||||
const file_name = fileName.slice(delimiterIndex + 1); // after delimiter
|
||||
|
||||
return { pipeline_id, file_name };
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import https from "node:https";
|
||||
import path from "node:path";
|
||||
|
||||
import { type ServerFile } from "@llamaindex/server";
|
||||
|
||||
export const UPLOADED_FOLDER = "output/uploaded";
|
||||
|
||||
export async function storeFile(
|
||||
name: string,
|
||||
fileBuffer: Buffer,
|
||||
): Promise<ServerFile> {
|
||||
const parts = name.split(".");
|
||||
const fileName = parts[0];
|
||||
const fileExt = parts[1];
|
||||
if (!fileName) {
|
||||
throw new Error("File name is required");
|
||||
}
|
||||
if (!fileExt) {
|
||||
throw new Error("File extension is required");
|
||||
}
|
||||
|
||||
const id = crypto.randomUUID();
|
||||
const fileId = `${sanitizeFileName(fileName)}_${id}.${fileExt}`;
|
||||
const filepath = path.join(UPLOADED_FOLDER, fileId);
|
||||
const fileUrl = await saveFile(filepath, fileBuffer);
|
||||
return {
|
||||
id: fileId,
|
||||
size: fileBuffer.length,
|
||||
type: fileExt,
|
||||
url: fileUrl,
|
||||
path: filepath,
|
||||
};
|
||||
}
|
||||
|
||||
// Save document to file server and return the file url
|
||||
async function saveFile(filepath: string, content: string | Buffer) {
|
||||
if (path.isAbsolute(filepath)) {
|
||||
throw new Error("Absolute file paths are not allowed.");
|
||||
}
|
||||
|
||||
const dirPath = path.dirname(filepath);
|
||||
await fs.promises.mkdir(dirPath, { recursive: true });
|
||||
|
||||
if (typeof content === "string") {
|
||||
await fs.promises.writeFile(filepath, content, "utf-8");
|
||||
} else {
|
||||
await fs.promises.writeFile(filepath, content);
|
||||
}
|
||||
|
||||
const fileurl = `/api/files/${filepath}`;
|
||||
return fileurl;
|
||||
}
|
||||
|
||||
function sanitizeFileName(fileName: string) {
|
||||
return fileName.replace(/[^a-zA-Z0-9_-]/g, "_");
|
||||
}
|
||||
export async function downloadFile(
|
||||
urlToDownload: string,
|
||||
downloadedPath: string,
|
||||
): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const dir = path.dirname(downloadedPath);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const file = fs.createWriteStream(downloadedPath);
|
||||
|
||||
https
|
||||
.get(urlToDownload, (response) => {
|
||||
if (response.statusCode !== 200) {
|
||||
reject(
|
||||
new Error(`Failed to download file: Status ${response.statusCode}`),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
response.pipe(file);
|
||||
|
||||
file.on("finish", () => {
|
||||
file.close();
|
||||
resolve();
|
||||
});
|
||||
|
||||
file.on("error", (err) => {
|
||||
fs.unlink(downloadedPath, () => reject(err));
|
||||
});
|
||||
})
|
||||
.on("error", (err) => {
|
||||
fs.unlink(downloadedPath, () => reject(err));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import { type FileAnnotation } from "@llamaindex/server";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { storeFile } from "./helpers";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const {
|
||||
name,
|
||||
base64,
|
||||
}: {
|
||||
name: string;
|
||||
base64: string;
|
||||
} = await request.json();
|
||||
if (!base64 || !name) {
|
||||
return NextResponse.json(
|
||||
{ error: "base64 and name is required in the request body" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const parts = base64.split(",");
|
||||
if (parts.length !== 2) {
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid base64 format" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const [header, content] = parts;
|
||||
if (!header || !content) {
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid base64 format" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const fileBuffer = Buffer.from(content, "base64");
|
||||
|
||||
const file = await storeFile(name, fileBuffer);
|
||||
|
||||
return NextResponse.json(file as FileAnnotation);
|
||||
} catch (error) {
|
||||
console.error("[Upload API]", error);
|
||||
return NextResponse.json(
|
||||
{ error: (error as Error).message },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { NextRequest } from "next/server";
|
||||
import { handleComponentRoute } from "../shared/component-handler";
|
||||
|
||||
const LAYOUT_TYPES = ["header", "footer"] as const;
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const params = request.nextUrl.searchParams;
|
||||
const directory = params.get("layoutDir") || "layout";
|
||||
return handleComponentRoute(directory, LAYOUT_TYPES);
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
import fs from "fs";
|
||||
import { NextResponse } from "next/server";
|
||||
import path from "path";
|
||||
import { promisify } from "util";
|
||||
|
||||
const VALID_EXTENSIONS = [".tsx", ".jsx"];
|
||||
|
||||
export type Item = {
|
||||
type: string;
|
||||
filename: string;
|
||||
code: string;
|
||||
};
|
||||
|
||||
function filterDuplicateFiles(files: string[]): string[] {
|
||||
const fileMap = new Map<string, string>();
|
||||
|
||||
for (const file of files) {
|
||||
const type = path.basename(file, path.extname(file));
|
||||
|
||||
if (fileMap.has(type)) {
|
||||
const existingFile = fileMap.get(type)!;
|
||||
// Prefer .tsx files
|
||||
if (file.endsWith(".tsx") && !existingFile.endsWith(".tsx")) {
|
||||
console.warn(`Preferring ${file} over ${existingFile}`);
|
||||
fileMap.set(type, file);
|
||||
}
|
||||
} else {
|
||||
fileMap.set(type, file);
|
||||
}
|
||||
}
|
||||
return Array.from(fileMap.values());
|
||||
}
|
||||
|
||||
export async function handleComponentRoute(
|
||||
directory: string,
|
||||
itemTypes?: readonly string[],
|
||||
): Promise<NextResponse> {
|
||||
try {
|
||||
const exists = await promisify(fs.exists)(directory);
|
||||
if (!exists) {
|
||||
return NextResponse.json(
|
||||
{ error: `Directory not found at ${directory}` },
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
const filesInDir = await promisify(fs.readdir)(directory);
|
||||
const validFiles = filesInDir.filter((file) =>
|
||||
VALID_EXTENSIONS.includes(path.extname(file)),
|
||||
);
|
||||
let filesToProcess = filterDuplicateFiles(validFiles);
|
||||
|
||||
if (itemTypes?.length) {
|
||||
// Specific item types provided (e.g., for layouts "header", "footer")
|
||||
filesToProcess = filesToProcess.filter((file) =>
|
||||
itemTypes.includes(path.basename(file, path.extname(file))),
|
||||
);
|
||||
}
|
||||
|
||||
const items: Item[] = await Promise.all(
|
||||
filesToProcess.map(async (file) => {
|
||||
const filePath = path.join(directory, file);
|
||||
const content = await promisify(fs.readFile)(filePath, "utf-8");
|
||||
return {
|
||||
type: path.basename(file, path.extname(file)),
|
||||
code: content,
|
||||
filename: file,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
return NextResponse.json(items, { status: 200 });
|
||||
} catch (error) {
|
||||
console.error(`Error reading directory ${directory}:`, error);
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to read directory ${directory}` },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { cn } from "./lib/utils";
|
||||
|
||||
const Accordion = AccordionPrimitive.Root;
|
||||
|
||||
const AccordionItem = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AccordionPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn("border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AccordionItem.displayName = "AccordionItem";
|
||||
|
||||
const AccordionTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-1 items-center justify-between py-4 text-left text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDown className="h-4 w-4 shrink-0 text-neutral-500 transition-transform duration-200 dark:text-neutral-400" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
));
|
||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
|
||||
|
||||
const AccordionContent = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Content
|
||||
ref={ref}
|
||||
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
));
|
||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
|
||||
|
||||
export { Accordion, AccordionContent, AccordionItem, AccordionTrigger };
|
||||
@@ -1,157 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
|
||||
import * as React from "react";
|
||||
|
||||
import { buttonVariants } from "./button";
|
||||
import { cn } from "./lib/utils";
|
||||
|
||||
function AlertDialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
|
||||
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
|
||||
}
|
||||
|
||||
function AlertDialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Overlay
|
||||
data-slot="alert-dialog-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
|
||||
return (
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.Content
|
||||
data-slot="alert-dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed left-[50%] top-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogHeader({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogFooter({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Title
|
||||
data-slot="alert-dialog-title"
|
||||
className={cn("text-lg font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Description
|
||||
data-slot="alert-dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogAction({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Action
|
||||
className={cn(buttonVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogCancel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Cancel
|
||||
className={cn(buttonVariants({ variant: "outline" }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogPortal,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
};
|
||||
@@ -1,68 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "./lib/utils";
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-card text-card-foreground",
|
||||
destructive:
|
||||
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function Alert({
|
||||
className,
|
||||
variant,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert"
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-title"
|
||||
className={cn(
|
||||
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-description"
|
||||
className={cn(
|
||||
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Alert, AlertDescription, AlertTitle };
|
||||
@@ -1,11 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio";
|
||||
|
||||
function AspectRatio({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AspectRatioPrimitive.Root>) {
|
||||
return <AspectRatioPrimitive.Root data-slot="aspect-ratio" {...props} />;
|
||||
}
|
||||
|
||||
export { AspectRatio };
|
||||
@@ -1,53 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "./lib/utils";
|
||||
|
||||
function Avatar({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
|
||||
return (
|
||||
<AvatarPrimitive.Root
|
||||
data-slot="avatar"
|
||||
className={cn(
|
||||
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AvatarImage({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
||||
return (
|
||||
<AvatarPrimitive.Image
|
||||
data-slot="avatar-image"
|
||||
className={cn("aspect-square size-full", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AvatarFallback({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
||||
return (
|
||||
<AvatarPrimitive.Fallback
|
||||
data-slot="avatar-fallback"
|
||||
className={cn(
|
||||
"bg-muted flex size-full items-center justify-center rounded-full",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Avatar, AvatarFallback, AvatarImage };
|
||||
@@ -1,38 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "./lib/utils";
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants };
|
||||
@@ -1,111 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { ChevronRight, MoreHorizontal } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "./lib/utils";
|
||||
|
||||
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
|
||||
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />;
|
||||
}
|
||||
|
||||
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
|
||||
return (
|
||||
<ol
|
||||
data-slot="breadcrumb-list"
|
||||
className={cn(
|
||||
"text-muted-foreground flex flex-wrap items-center gap-1.5 break-words text-sm sm:gap-2.5",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||
return (
|
||||
<li
|
||||
data-slot="breadcrumb-item"
|
||||
className={cn("inline-flex items-center gap-1.5", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbLink({
|
||||
asChild,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"a"> & {
|
||||
asChild?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "a";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="breadcrumb-link"
|
||||
className={cn("hover:text-foreground transition-colors", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="breadcrumb-page"
|
||||
role="link"
|
||||
aria-disabled="true"
|
||||
aria-current="page"
|
||||
className={cn("text-foreground font-normal", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbSeparator({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"li">) {
|
||||
return (
|
||||
<li
|
||||
data-slot="breadcrumb-separator"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn("[&>svg]:size-3.5", className)}
|
||||
{...props}
|
||||
>
|
||||
{children ?? <ChevronRight />}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbEllipsis({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="breadcrumb-ellipsis"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn("flex size-9 items-center justify-center", className)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontal className="size-4" />
|
||||
<span className="sr-only">More</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Breadcrumb,
|
||||
BreadcrumbEllipsis,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
};
|
||||
@@ -1,58 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "./lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
|
||||
export { Button, buttonVariants };
|
||||
@@ -1,75 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { DayPicker } from "react-day-picker";
|
||||
|
||||
import { buttonVariants } from "./button";
|
||||
import { cn } from "./lib/utils";
|
||||
|
||||
function Calendar({
|
||||
className,
|
||||
classNames,
|
||||
showOutsideDays = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DayPicker>) {
|
||||
return (
|
||||
<DayPicker
|
||||
showOutsideDays={showOutsideDays}
|
||||
className={cn("p-3", className)}
|
||||
classNames={{
|
||||
months: "flex flex-col sm:flex-row gap-2",
|
||||
month: "flex flex-col gap-4",
|
||||
caption: "flex justify-center pt-1 relative items-center w-full",
|
||||
caption_label: "text-sm font-medium",
|
||||
nav: "flex items-center gap-1",
|
||||
nav_button: cn(
|
||||
buttonVariants({ variant: "outline" }),
|
||||
"size-7 bg-transparent p-0 opacity-50 hover:opacity-100",
|
||||
),
|
||||
nav_button_previous: "absolute left-1",
|
||||
nav_button_next: "absolute right-1",
|
||||
table: "w-full border-collapse space-x-1",
|
||||
head_row: "flex",
|
||||
head_cell:
|
||||
"text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
|
||||
row: "flex w-full mt-2",
|
||||
cell: cn(
|
||||
"relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-range-end)]:rounded-r-md",
|
||||
props.mode === "range"
|
||||
? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
|
||||
: "[&:has([aria-selected])]:rounded-md",
|
||||
),
|
||||
day: cn(
|
||||
buttonVariants({ variant: "ghost" }),
|
||||
"size-8 p-0 font-normal aria-selected:opacity-100",
|
||||
),
|
||||
day_range_start:
|
||||
"day-range-start aria-selected:bg-primary aria-selected:text-primary-foreground",
|
||||
day_range_end:
|
||||
"day-range-end aria-selected:bg-primary aria-selected:text-primary-foreground",
|
||||
day_selected:
|
||||
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
||||
day_today: "bg-accent text-accent-foreground",
|
||||
day_outside:
|
||||
"day-outside text-muted-foreground aria-selected:text-muted-foreground",
|
||||
day_disabled: "text-muted-foreground opacity-50",
|
||||
day_range_middle:
|
||||
"aria-selected:bg-accent aria-selected:text-accent-foreground",
|
||||
day_hidden: "invisible",
|
||||
...classNames,
|
||||
}}
|
||||
components={{
|
||||
Chevron: ({ ...props }) =>
|
||||
props.orientation === "left" ? (
|
||||
<ChevronLeft {...props} className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronRight {...props} className="h-4 w-4" />
|
||||
),
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Calendar };
|
||||
@@ -1,83 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { forwardRef } from "react";
|
||||
import { cn } from "./lib/utils";
|
||||
|
||||
const Card = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-xl border border-neutral-200 bg-white text-neutral-950 shadow-sm dark:border-neutral-800 dark:bg-neutral-950 dark:text-neutral-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
Card.displayName = "Card";
|
||||
|
||||
const CardHeader = forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardHeader.displayName = "CardHeader";
|
||||
|
||||
const CardTitle = forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("font-semibold leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardTitle.displayName = "CardTitle";
|
||||
|
||||
const CardDescription = forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("text-sm text-neutral-500 dark:text-neutral-400", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardDescription.displayName = "CardDescription";
|
||||
|
||||
const CardContent = forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
));
|
||||
CardContent.displayName = "CardContent";
|
||||
|
||||
const CardFooter = forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardFooter.displayName = "CardFooter";
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
};
|
||||
@@ -1,241 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import useEmblaCarousel, {
|
||||
type UseEmblaCarouselType,
|
||||
} from "embla-carousel-react";
|
||||
import { ArrowLeft, ArrowRight } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { Button } from "./button";
|
||||
import { cn } from "./lib/utils";
|
||||
|
||||
type CarouselApi = UseEmblaCarouselType[1];
|
||||
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
|
||||
type CarouselOptions = UseCarouselParameters[0];
|
||||
type CarouselPlugin = UseCarouselParameters[1];
|
||||
|
||||
type CarouselProps = {
|
||||
opts?: CarouselOptions;
|
||||
plugins?: CarouselPlugin;
|
||||
orientation?: "horizontal" | "vertical";
|
||||
setApi?: (api: CarouselApi) => void;
|
||||
};
|
||||
|
||||
type CarouselContextProps = {
|
||||
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
|
||||
api: ReturnType<typeof useEmblaCarousel>[1];
|
||||
scrollPrev: () => void;
|
||||
scrollNext: () => void;
|
||||
canScrollPrev: boolean;
|
||||
canScrollNext: boolean;
|
||||
} & CarouselProps;
|
||||
|
||||
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
|
||||
|
||||
function useCarousel() {
|
||||
const context = React.useContext(CarouselContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useCarousel must be used within a <Carousel />");
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
function Carousel({
|
||||
orientation = "horizontal",
|
||||
opts,
|
||||
setApi,
|
||||
plugins,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & CarouselProps) {
|
||||
const [carouselRef, api] = useEmblaCarousel(
|
||||
{
|
||||
...opts,
|
||||
axis: orientation === "horizontal" ? "x" : "y",
|
||||
},
|
||||
plugins,
|
||||
);
|
||||
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
|
||||
const [canScrollNext, setCanScrollNext] = React.useState(false);
|
||||
|
||||
const onSelect = React.useCallback((api: CarouselApi) => {
|
||||
if (!api) return;
|
||||
setCanScrollPrev(api.canScrollPrev());
|
||||
setCanScrollNext(api.canScrollNext());
|
||||
}, []);
|
||||
|
||||
const scrollPrev = React.useCallback(() => {
|
||||
api?.scrollPrev();
|
||||
}, [api]);
|
||||
|
||||
const scrollNext = React.useCallback(() => {
|
||||
api?.scrollNext();
|
||||
}, [api]);
|
||||
|
||||
const handleKeyDown = React.useCallback(
|
||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.key === "ArrowLeft") {
|
||||
event.preventDefault();
|
||||
scrollPrev();
|
||||
} else if (event.key === "ArrowRight") {
|
||||
event.preventDefault();
|
||||
scrollNext();
|
||||
}
|
||||
},
|
||||
[scrollPrev, scrollNext],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api || !setApi) return;
|
||||
setApi(api);
|
||||
}, [api, setApi]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api) return;
|
||||
onSelect(api);
|
||||
api.on("reInit", onSelect);
|
||||
api.on("select", onSelect);
|
||||
|
||||
return () => {
|
||||
api?.off("select", onSelect);
|
||||
};
|
||||
}, [api, onSelect]);
|
||||
|
||||
return (
|
||||
<CarouselContext.Provider
|
||||
value={{
|
||||
carouselRef,
|
||||
api: api,
|
||||
opts,
|
||||
orientation:
|
||||
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
|
||||
scrollPrev,
|
||||
scrollNext,
|
||||
canScrollPrev,
|
||||
canScrollNext,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
onKeyDownCapture={handleKeyDown}
|
||||
className={cn("relative", className)}
|
||||
role="region"
|
||||
aria-roledescription="carousel"
|
||||
data-slot="carousel"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</CarouselContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
const { carouselRef, orientation } = useCarousel();
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={carouselRef}
|
||||
className="overflow-hidden"
|
||||
data-slot="carousel-content"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex",
|
||||
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
|
||||
const { orientation } = useCarousel();
|
||||
|
||||
return (
|
||||
<div
|
||||
role="group"
|
||||
aria-roledescription="slide"
|
||||
data-slot="carousel-item"
|
||||
className={cn(
|
||||
"min-w-0 shrink-0 grow-0 basis-full",
|
||||
orientation === "horizontal" ? "pl-4" : "pt-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CarouselPrevious({
|
||||
className,
|
||||
variant = "outline",
|
||||
size = "icon",
|
||||
...props
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
|
||||
|
||||
return (
|
||||
<Button
|
||||
data-slot="carousel-previous"
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
"absolute size-8 rounded-full",
|
||||
orientation === "horizontal"
|
||||
? "-left-12 top-1/2 -translate-y-1/2"
|
||||
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
className,
|
||||
)}
|
||||
disabled={!canScrollPrev}
|
||||
onClick={scrollPrev}
|
||||
{...props}
|
||||
>
|
||||
<ArrowLeft />
|
||||
<span className="sr-only">Previous slide</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function CarouselNext({
|
||||
className,
|
||||
variant = "outline",
|
||||
size = "icon",
|
||||
...props
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
const { orientation, scrollNext, canScrollNext } = useCarousel();
|
||||
|
||||
return (
|
||||
<Button
|
||||
data-slot="carousel-next"
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
"absolute size-8 rounded-full",
|
||||
orientation === "horizontal"
|
||||
? "-right-12 top-1/2 -translate-y-1/2"
|
||||
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
className,
|
||||
)}
|
||||
disabled={!canScrollNext}
|
||||
onClick={scrollNext}
|
||||
{...props}
|
||||
>
|
||||
<ArrowRight />
|
||||
<span className="sr-only">Next slide</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Carousel,
|
||||
CarouselContent,
|
||||
CarouselItem,
|
||||
CarouselNext,
|
||||
CarouselPrevious,
|
||||
type CarouselApi,
|
||||
};
|
||||
@@ -1,353 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as RechartsPrimitive from "recharts";
|
||||
|
||||
import { cn } from "./lib/utils";
|
||||
|
||||
// Format: { THEME_NAME: CSS_SELECTOR }
|
||||
const THEMES = { light: "", dark: ".dark" } as const;
|
||||
|
||||
export type ChartConfig = {
|
||||
[k in string]: {
|
||||
label?: React.ReactNode;
|
||||
icon?: React.ComponentType;
|
||||
} & (
|
||||
| { color?: string; theme?: never }
|
||||
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
||||
);
|
||||
};
|
||||
|
||||
type ChartContextProps = {
|
||||
config: ChartConfig;
|
||||
};
|
||||
|
||||
const ChartContext = React.createContext<ChartContextProps | null>(null);
|
||||
|
||||
function useChart() {
|
||||
const context = React.useContext(ChartContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useChart must be used within a <ChartContainer />");
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
function ChartContainer({
|
||||
id,
|
||||
className,
|
||||
children,
|
||||
config,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
config: ChartConfig;
|
||||
children: React.ComponentProps<
|
||||
typeof RechartsPrimitive.ResponsiveContainer
|
||||
>["children"];
|
||||
}) {
|
||||
const uniqueId = React.useId();
|
||||
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
|
||||
|
||||
return (
|
||||
<ChartContext.Provider value={{ config }}>
|
||||
<div
|
||||
data-slot="chart"
|
||||
data-chart={chartId}
|
||||
className={cn(
|
||||
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-surface]:outline-hidden flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-sector[stroke='#fff']]:stroke-transparent",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChartStyle id={chartId} config={config} />
|
||||
<RechartsPrimitive.ResponsiveContainer>
|
||||
{children}
|
||||
</RechartsPrimitive.ResponsiveContainer>
|
||||
</div>
|
||||
</ChartContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
||||
const colorConfig = Object.entries(config).filter(
|
||||
([, config]) => config.theme || config.color,
|
||||
);
|
||||
|
||||
if (!colorConfig.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: Object.entries(THEMES)
|
||||
.map(
|
||||
([theme, prefix]) => `
|
||||
${prefix} [data-chart=${id}] {
|
||||
${colorConfig
|
||||
.map(([key, itemConfig]) => {
|
||||
const color =
|
||||
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
|
||||
itemConfig.color;
|
||||
return color ? ` --color-${key}: ${color};` : null;
|
||||
})
|
||||
.join("\n")}
|
||||
}
|
||||
`,
|
||||
)
|
||||
.join("\n"),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const ChartTooltip = RechartsPrimitive.Tooltip;
|
||||
|
||||
function ChartTooltipContent({
|
||||
active,
|
||||
payload,
|
||||
className,
|
||||
indicator = "dot",
|
||||
hideLabel = false,
|
||||
hideIndicator = false,
|
||||
label,
|
||||
labelFormatter,
|
||||
labelClassName,
|
||||
formatter,
|
||||
color,
|
||||
nameKey,
|
||||
labelKey,
|
||||
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
||||
React.ComponentProps<"div"> & {
|
||||
hideLabel?: boolean;
|
||||
hideIndicator?: boolean;
|
||||
indicator?: "line" | "dot" | "dashed";
|
||||
nameKey?: string;
|
||||
labelKey?: string;
|
||||
}) {
|
||||
const { config } = useChart();
|
||||
|
||||
const tooltipLabel = React.useMemo(() => {
|
||||
if (hideLabel || !payload?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [item] = payload;
|
||||
const key = `${labelKey || item?.dataKey || item?.name || "value"}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
const value =
|
||||
!labelKey && typeof label === "string"
|
||||
? config[label as keyof typeof config]?.label || label
|
||||
: itemConfig?.label;
|
||||
|
||||
if (labelFormatter) {
|
||||
return (
|
||||
<div className={cn("font-medium", labelClassName)}>
|
||||
{labelFormatter(value, payload)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <div className={cn("font-medium", labelClassName)}>{value}</div>;
|
||||
}, [
|
||||
label,
|
||||
labelFormatter,
|
||||
payload,
|
||||
hideLabel,
|
||||
labelClassName,
|
||||
config,
|
||||
labelKey,
|
||||
]);
|
||||
|
||||
if (!active || !payload?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nestLabel = payload.length === 1 && indicator !== "dot";
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{!nestLabel ? tooltipLabel : null}
|
||||
<div className="grid gap-1.5">
|
||||
{payload.map((item, index) => {
|
||||
const key = `${nameKey || item.name || item.dataKey || "value"}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
const indicatorColor = color || item.payload.fill || item.color;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.dataKey}
|
||||
className={cn(
|
||||
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
|
||||
indicator === "dot" && "items-center",
|
||||
)}
|
||||
>
|
||||
{formatter && item?.value !== undefined && item.name ? (
|
||||
formatter(item.value, item.name, item, index, item.payload)
|
||||
) : (
|
||||
<>
|
||||
{itemConfig?.icon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
!hideIndicator && (
|
||||
<div
|
||||
className={cn(
|
||||
"border-(--color-border) bg-(--color-bg) shrink-0 rounded-[2px]",
|
||||
{
|
||||
"h-2.5 w-2.5": indicator === "dot",
|
||||
"w-1": indicator === "line",
|
||||
"w-0 border-[1.5px] border-dashed bg-transparent":
|
||||
indicator === "dashed",
|
||||
"my-0.5": nestLabel && indicator === "dashed",
|
||||
},
|
||||
)}
|
||||
style={
|
||||
{
|
||||
"--color-bg": indicatorColor,
|
||||
"--color-border": indicatorColor,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-1 justify-between leading-none",
|
||||
nestLabel ? "items-end" : "items-center",
|
||||
)}
|
||||
>
|
||||
<div className="grid gap-1.5">
|
||||
{nestLabel ? tooltipLabel : null}
|
||||
<span className="text-muted-foreground">
|
||||
{itemConfig?.label || item.name}
|
||||
</span>
|
||||
</div>
|
||||
{item.value && (
|
||||
<span className="text-foreground font-mono font-medium tabular-nums">
|
||||
{item.value.toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const ChartLegend = RechartsPrimitive.Legend;
|
||||
|
||||
function ChartLegendContent({
|
||||
className,
|
||||
hideIcon = false,
|
||||
payload,
|
||||
verticalAlign = "bottom",
|
||||
nameKey,
|
||||
}: React.ComponentProps<"div"> &
|
||||
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
||||
hideIcon?: boolean;
|
||||
nameKey?: string;
|
||||
}) {
|
||||
const { config } = useChart();
|
||||
|
||||
if (!payload?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-center gap-4",
|
||||
verticalAlign === "top" ? "pb-3" : "pt-3",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{payload.map((item) => {
|
||||
const key = `${nameKey || item.dataKey || "value"}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.value}
|
||||
className={cn(
|
||||
"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3",
|
||||
)}
|
||||
>
|
||||
{itemConfig?.icon && !hideIcon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
<div
|
||||
className="h-2 w-2 shrink-0 rounded-[2px]"
|
||||
style={{
|
||||
backgroundColor: item.color,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{itemConfig?.label}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Helper to extract item config from a payload.
|
||||
function getPayloadConfigFromPayload(
|
||||
config: ChartConfig,
|
||||
payload: unknown,
|
||||
key: string,
|
||||
) {
|
||||
if (typeof payload !== "object" || payload === null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const payloadPayload =
|
||||
"payload" in payload &&
|
||||
typeof payload.payload === "object" &&
|
||||
payload.payload !== null
|
||||
? payload.payload
|
||||
: undefined;
|
||||
|
||||
let configLabelKey: string = key;
|
||||
|
||||
if (
|
||||
key in payload &&
|
||||
typeof payload[key as keyof typeof payload] === "string"
|
||||
) {
|
||||
configLabelKey = payload[key as keyof typeof payload] as string;
|
||||
} else if (
|
||||
payloadPayload &&
|
||||
key in payloadPayload &&
|
||||
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
|
||||
) {
|
||||
configLabelKey = payloadPayload[
|
||||
key as keyof typeof payloadPayload
|
||||
] as string;
|
||||
}
|
||||
|
||||
return configLabelKey in config
|
||||
? config[configLabelKey]
|
||||
: config[key as keyof typeof config];
|
||||
}
|
||||
|
||||
export {
|
||||
ChartContainer,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartStyle,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
};
|
||||
@@ -1,24 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { ChatCanvas, useChatCanvas } from "@llamaindex/chat-ui";
|
||||
import { ResizableHandle, ResizablePanel } from "../../resizable";
|
||||
import { CodeArtifactRenderer } from "./preview";
|
||||
|
||||
export function ChatCanvasPanel() {
|
||||
const { displayedArtifact, isCanvasOpen } = useChatCanvas();
|
||||
if (!displayedArtifact || !isCanvasOpen) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ResizableHandle withHandle />
|
||||
<ResizablePanel defaultSize={60} minSize={50}>
|
||||
<ChatCanvas className="w-full">
|
||||
<ChatCanvas.CodeArtifact
|
||||
tabs={{ preview: <CodeArtifactRenderer /> }}
|
||||
/>
|
||||
<ChatCanvas.DocumentArtifact />
|
||||
</ChatCanvas>
|
||||
</ResizablePanel>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { CodeArtifact, useChatCanvas } from "@llamaindex/chat-ui";
|
||||
import { Loader2, WandSparkles } from "lucide-react";
|
||||
import React, { FunctionComponent, useEffect, useState } from "react";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "../../accordion";
|
||||
import { buttonVariants } from "../../button";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { DynamicComponentErrorBoundary } from "../custom/events/error-boundary";
|
||||
import { parseComponent } from "../custom/events/loader";
|
||||
|
||||
const SUPPORTED_FRONTEND_PREVIEW = [
|
||||
"js",
|
||||
"ts",
|
||||
"jsx",
|
||||
"tsx",
|
||||
"javascript",
|
||||
"typescript",
|
||||
];
|
||||
|
||||
export function CodeArtifactRenderer() {
|
||||
const { displayedArtifact } = useChatCanvas();
|
||||
|
||||
if (displayedArtifact?.type !== "code") return null;
|
||||
const codeArtifact = displayedArtifact as CodeArtifact;
|
||||
|
||||
if (!SUPPORTED_FRONTEND_PREVIEW.includes(codeArtifact.data.language)) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center gap-2">
|
||||
<p className="text-sm text-gray-500">
|
||||
Preview is not supported for this language
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <CodeArtifactRendererComp artifact={codeArtifact} />;
|
||||
}
|
||||
|
||||
function CodeArtifactRendererComp({ artifact }: { artifact: CodeArtifact }) {
|
||||
const { appendErrors } = useChatCanvas();
|
||||
const [isRendering, setIsRendering] = useState(true);
|
||||
const [component, setComponent] = useState<FunctionComponent | null>(null);
|
||||
|
||||
const {
|
||||
data: { code, file_name },
|
||||
} = artifact;
|
||||
|
||||
useEffect(() => {
|
||||
const renderComponent = async () => {
|
||||
setIsRendering(true);
|
||||
const { component: parsedComponent, error } = await parseComponent(
|
||||
code,
|
||||
file_name,
|
||||
);
|
||||
|
||||
if (error) {
|
||||
setComponent(null);
|
||||
appendErrors(artifact, [error]);
|
||||
} else {
|
||||
setComponent(() => parsedComponent);
|
||||
}
|
||||
|
||||
setIsRendering(false);
|
||||
};
|
||||
|
||||
renderComponent();
|
||||
}, [artifact]);
|
||||
|
||||
if (isRendering) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center gap-2">
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
<p className="text-sm text-gray-500">Rendering Artifact...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!component) {
|
||||
return <CodeErrors artifact={artifact} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<DynamicComponentErrorBoundary
|
||||
onError={(error) => appendErrors(artifact, [error])}
|
||||
>
|
||||
{React.createElement(component)}
|
||||
</DynamicComponentErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
function CodeErrors({ artifact }: { artifact: CodeArtifact }) {
|
||||
const { getCodeErrors, fixCodeErrors } = useChatCanvas();
|
||||
const uniqueErrors = getCodeErrors(artifact);
|
||||
|
||||
if (uniqueErrors.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-10 px-10 pt-10">
|
||||
<p className="text-center text-sm text-gray-500">
|
||||
Error when rendering code, please check the details and try fixing them.
|
||||
</p>
|
||||
<Accordion
|
||||
type="single"
|
||||
defaultValue="errors"
|
||||
collapsible
|
||||
className="w-full rounded-xl border border-gray-100 bg-white shadow-md"
|
||||
>
|
||||
<AccordionItem value="errors" className="border-none px-4">
|
||||
<AccordionTrigger className="py-2 hover:no-underline">
|
||||
<div className="flex flex-1 items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-muted-foreground font-bold">
|
||||
Rendering errors
|
||||
</span>
|
||||
<span className="inline-flex h-5 w-5 items-center justify-center rounded-full bg-yellow-500 text-xs text-white">
|
||||
{uniqueErrors.length}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={cn(
|
||||
buttonVariants({ variant: "default", size: "sm" }),
|
||||
"mr-2 h-8 cursor-pointer bg-gradient-to-r from-blue-500 to-purple-500 text-white hover:from-blue-600 hover:to-purple-600",
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
fixCodeErrors(artifact);
|
||||
}}
|
||||
>
|
||||
<WandSparkles className="mr-2 h-4 w-4" />
|
||||
<span>Fix errors</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="pb-4">
|
||||
<div className="space-y-2">
|
||||
{uniqueErrors.map((error, index) => (
|
||||
<p key={index} className="text-muted-foreground text-sm">
|
||||
{error}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { ChatMessage } from "@llamaindex/chat-ui";
|
||||
import { LLAMA_LOGO_URL } from "../../../constants";
|
||||
|
||||
export function ChatMessageAvatar() {
|
||||
return (
|
||||
<ChatMessage.Avatar>
|
||||
<img
|
||||
className="border-1 rounded-full border-[#e711dd]"
|
||||
src={LLAMA_LOGO_URL}
|
||||
alt="Llama Logo"
|
||||
/>
|
||||
</ChatMessage.Avatar>
|
||||
);
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* The default border color has changed to `currentColor` in Tailwind CSS v4,
|
||||
* so adding these compatibility styles to make sure everything still
|
||||
* looks the same as it did with Tailwind CSS v3.
|
||||
*/
|
||||
const tailwindConfig = `
|
||||
@import "tailwindcss";
|
||||
|
||||
@layer base {
|
||||
*,
|
||||
::after,
|
||||
::before,
|
||||
::backdrop,
|
||||
::file-selector-button {
|
||||
border-color: var(--color-gray-200, currentColor);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export function ChatInjection() {
|
||||
return (
|
||||
<>
|
||||
<script
|
||||
async
|
||||
src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"
|
||||
></script>
|
||||
<style type="text/tailwindcss">{tailwindConfig}</style>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { ChatInput, useChatUI, useFile } from "@llamaindex/chat-ui";
|
||||
import { DocumentInfo, ImagePreview } from "@llamaindex/chat-ui/widgets";
|
||||
import { getConfig } from "../lib/utils";
|
||||
import { LlamaCloudSelector } from "./custom/llama-cloud-selector";
|
||||
|
||||
export default function CustomChatInput() {
|
||||
const { requestData, isLoading, input } = useChatUI();
|
||||
const uploadAPI = getConfig("UPLOAD_API") ?? "";
|
||||
const llamaCloudAPI =
|
||||
getConfig("LLAMA_CLOUD_API") ??
|
||||
(process.env.NEXT_PUBLIC_SHOW_LLAMACLOUD_SELECTOR === "true"
|
||||
? "/api/chat/config/llamacloud"
|
||||
: "");
|
||||
const {
|
||||
imageUrl,
|
||||
setImageUrl,
|
||||
uploadFile,
|
||||
files,
|
||||
removeDoc,
|
||||
reset,
|
||||
getAnnotations,
|
||||
} = useFile({ uploadAPI });
|
||||
|
||||
/**
|
||||
* Handles file uploads. Overwrite to hook into the file upload behavior.
|
||||
* @param file The file to upload
|
||||
*/
|
||||
const handleUploadFile = async (file: File) => {
|
||||
// There's already an image uploaded, only allow one image at a time
|
||||
if (imageUrl) {
|
||||
alert("You can only upload one image at a time.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Upload the file and send with it the current request data
|
||||
await uploadFile(file, requestData);
|
||||
} catch (error: unknown) {
|
||||
// Show error message if upload fails
|
||||
alert(
|
||||
error instanceof Error ? error.message : "An unknown error occurred",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Get references to the upload files in message annotations format, see https://github.com/run-llama/chat-ui/blob/main/packages/chat-ui/src/hook/use-file.tsx#L56
|
||||
const annotations = getAnnotations();
|
||||
|
||||
return (
|
||||
<ChatInput resetUploadedFiles={reset} annotations={annotations}>
|
||||
{/* Image preview section */}
|
||||
{imageUrl && (
|
||||
<ImagePreview url={imageUrl} onRemove={() => setImageUrl(null)} />
|
||||
)}
|
||||
{/* Document previews section */}
|
||||
{files.length > 0 && (
|
||||
<div className="flex w-full gap-4 overflow-auto py-2">
|
||||
{files.map((file) => (
|
||||
<DocumentInfo
|
||||
key={file.id}
|
||||
document={{ url: file.url, sources: [] }}
|
||||
className="mb-2 mt-2"
|
||||
onRemove={() => removeDoc(file)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<ChatInput.Form>
|
||||
<ChatInput.Field />
|
||||
{uploadAPI && <ChatInput.Upload onUpload={handleUploadFile} />}
|
||||
{llamaCloudAPI && <LlamaCloudSelector />}
|
||||
<ChatInput.Submit
|
||||
disabled={
|
||||
isLoading || (!input.trim() && files.length === 0 && !imageUrl)
|
||||
}
|
||||
/>
|
||||
</ChatInput.Form>
|
||||
</ChatInput>
|
||||
);
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { ChatMessage } from "@llamaindex/chat-ui";
|
||||
import { DynamicEvents } from "./custom/events/dynamic-events";
|
||||
import { ComponentDef } from "./custom/events/types";
|
||||
import { ToolAnnotations } from "./tools/chat-tools";
|
||||
|
||||
export function ChatMessageContent({
|
||||
componentDefs,
|
||||
appendError,
|
||||
}: {
|
||||
componentDefs: ComponentDef[];
|
||||
appendError: (error: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<ChatMessage.Content>
|
||||
<ChatMessage.Content.Event />
|
||||
<ChatMessage.Content.AgentEvent />
|
||||
<ToolAnnotations />
|
||||
<ChatMessage.Content.Image />
|
||||
<DynamicEvents componentDefs={componentDefs} appendError={appendError} />
|
||||
<ChatMessage.Content.Markdown />
|
||||
<ChatMessage.Content.DocumentFile />
|
||||
<ChatMessage.Content.Source />
|
||||
<ChatMessage.Content.SuggestedQuestions />
|
||||
</ChatMessage.Content>
|
||||
);
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
"use client";
|
||||
|
||||
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 "./custom/events/types";
|
||||
|
||||
export default function CustomChatMessages({
|
||||
componentDefs,
|
||||
appendError,
|
||||
}: {
|
||||
componentDefs: ComponentDef[];
|
||||
appendError: (error: string) => void;
|
||||
}) {
|
||||
const { messages } = useChatUI();
|
||||
|
||||
return (
|
||||
<ChatMessages>
|
||||
<ChatMessages.List>
|
||||
{messages.map((message, index) => (
|
||||
<ChatMessage
|
||||
key={index}
|
||||
message={message}
|
||||
isLast={index === messages.length - 1}
|
||||
>
|
||||
<ChatMessageAvatar />
|
||||
<ChatMessageContent
|
||||
componentDefs={componentDefs}
|
||||
appendError={appendError}
|
||||
/>
|
||||
<ChatMessage.Actions />
|
||||
</ChatMessage>
|
||||
))}
|
||||
<ChatMessages.Empty
|
||||
heading="Hello there!"
|
||||
subheading="I'm here to help you with your questions."
|
||||
/>
|
||||
<ChatMessages.Loading />
|
||||
</ChatMessages.List>
|
||||
<ChatStarter />
|
||||
</ChatMessages>
|
||||
);
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { ChatSection as ChatUI, useChatWorkflow } from "@llamaindex/chat-ui";
|
||||
import { useChat } from "ai/react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { getConfig } from "../lib/utils";
|
||||
import { ResizablePanel, ResizablePanelGroup } from "../resizable";
|
||||
import { ChatCanvasPanel } from "./canvas/panel";
|
||||
import { ChatInjection } from "./chat-injection";
|
||||
import CustomChatInput from "./chat-input";
|
||||
import CustomChatMessages from "./chat-messages";
|
||||
import { DynamicEventsErrors } from "./custom/events/dynamic-events-errors";
|
||||
import { fetchComponentDefinitions } from "./custom/events/loader";
|
||||
import { ComponentDef } from "./custom/events/types";
|
||||
import { DevModePanel } from "./dev-mode-panel";
|
||||
import { ChatLayout } from "./layout";
|
||||
|
||||
export default function ChatSection() {
|
||||
const deployment = getConfig("DEPLOYMENT") || "";
|
||||
const workflow = getConfig("WORKFLOW") || "";
|
||||
const shouldUseChatWorkflow = deployment && workflow;
|
||||
|
||||
const handleError = (error: unknown) => {
|
||||
if (!(error instanceof Error)) throw error;
|
||||
let errorMessage: string;
|
||||
try {
|
||||
errorMessage = JSON.parse(error.message).detail;
|
||||
} catch (e) {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
alert(errorMessage);
|
||||
};
|
||||
|
||||
const useChatHandler = useChat({
|
||||
api: getConfig("CHAT_API") || "/api/chat",
|
||||
onError: handleError,
|
||||
experimental_throttle: 100,
|
||||
});
|
||||
|
||||
const useChatWorkflowHandler = useChatWorkflow({
|
||||
fileServerUrl: getConfig("FILE_SERVER_URL"),
|
||||
deployment,
|
||||
workflow,
|
||||
onError: handleError,
|
||||
});
|
||||
|
||||
const handler = shouldUseChatWorkflow
|
||||
? useChatWorkflowHandler
|
||||
: useChatHandler;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ChatLayout>
|
||||
<ChatUI
|
||||
handler={handler}
|
||||
className="relative flex min-h-0 flex-1 flex-row justify-center gap-4 px-4 py-0"
|
||||
>
|
||||
<ResizablePanelGroup direction="horizontal">
|
||||
<ChatSectionPanel />
|
||||
<ChatCanvasPanel />
|
||||
</ResizablePanelGroup>
|
||||
<DevModePanel />
|
||||
</ChatUI>
|
||||
</ChatLayout>
|
||||
<ChatInjection />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ChatSectionPanel() {
|
||||
const [componentDefs, setComponentDefs] = useState<ComponentDef[]>([]);
|
||||
const [dynamicEventsErrors, setDynamicEventsErrors] = useState<string[]>([]); // contain all errors when rendering dynamic events from componentDir
|
||||
|
||||
const appendError = (error: string) => {
|
||||
setDynamicEventsErrors((prev) => [...prev, error]);
|
||||
};
|
||||
|
||||
const uniqueErrors = useMemo(() => {
|
||||
return Array.from(new Set(dynamicEventsErrors));
|
||||
}, [dynamicEventsErrors]);
|
||||
|
||||
// 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(({ components, errors }) => {
|
||||
setComponentDefs(components);
|
||||
if (errors.length > 0) {
|
||||
setDynamicEventsErrors((prev) => [...prev, ...errors]);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ResizablePanel defaultSize={40} minSize={30} className="max-w-1/2 mx-auto">
|
||||
<div className="flex h-full min-w-0 flex-1 flex-col gap-4">
|
||||
<DynamicEventsErrors
|
||||
errors={uniqueErrors}
|
||||
clearErrors={() => setDynamicEventsErrors([])}
|
||||
/>
|
||||
<CustomChatMessages
|
||||
componentDefs={componentDefs}
|
||||
appendError={appendError}
|
||||
/>
|
||||
<CustomChatInput />
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
);
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useChatUI } from "@llamaindex/chat-ui";
|
||||
import { StarterQuestions } from "@llamaindex/chat-ui/widgets";
|
||||
import { getConfig } from "../lib/utils";
|
||||
|
||||
export function ChatStarter({ className }: { className?: string }) {
|
||||
const { append, messages, requestData } = useChatUI();
|
||||
const starterQuestionsFromConfig = getConfig("STARTER_QUESTIONS");
|
||||
|
||||
const starterQuestions =
|
||||
Array.isArray(starterQuestionsFromConfig) &&
|
||||
starterQuestionsFromConfig?.length > 0
|
||||
? starterQuestionsFromConfig
|
||||
: JSON.parse(process.env.NEXT_PUBLIC_STARTER_QUESTIONS || "[]");
|
||||
|
||||
if (starterQuestions.length === 0 || messages.length > 0) return null;
|
||||
return (
|
||||
<StarterQuestions
|
||||
append={(message) => append(message, { data: requestData })}
|
||||
questions={starterQuestions}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import "@llamaindex/chat-ui/styles/markdown.css";
|
||||
import "@llamaindex/chat-ui/styles/pdf.css";
|
||||
import { XIcon } from "lucide-react";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "../../../accordion";
|
||||
import { buttonVariants } from "../../../button";
|
||||
import { cn } from "../../../lib/utils";
|
||||
|
||||
export function DynamicEventsErrors({
|
||||
errors,
|
||||
clearErrors,
|
||||
}: {
|
||||
errors: string[];
|
||||
clearErrors: () => void;
|
||||
}) {
|
||||
if (errors.length === 0) return null;
|
||||
return (
|
||||
<Accordion
|
||||
type="single"
|
||||
defaultValue="errors"
|
||||
collapsible
|
||||
className="rounded-xl border border-gray-100 bg-white shadow-md"
|
||||
>
|
||||
<AccordionItem value="errors" className="border-none px-4">
|
||||
<AccordionTrigger className="py-2 hover:no-underline">
|
||||
<div className="flex flex-1 items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-muted-foreground font-bold">
|
||||
Errors when rendering dynamic events from components directory
|
||||
</span>
|
||||
<span className="inline-flex h-5 w-5 items-center justify-center rounded-full bg-yellow-500 text-xs text-white">
|
||||
{errors.length}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className={cn(buttonVariants({ variant: "ghost", size: "sm" }))}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
clearErrors();
|
||||
}}
|
||||
>
|
||||
<XIcon className="mr-2 h-4 w-4" />
|
||||
<span>Clear all</span>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="pb-4">
|
||||
<div className="space-y-2">
|
||||
{errors.map((error, index) => (
|
||||
<p key={index} className="text-muted-foreground text-sm">
|
||||
{error}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
);
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
getAnnotationData,
|
||||
JSONValue,
|
||||
MessageAnnotation,
|
||||
MessageAnnotationType,
|
||||
useChatMessage,
|
||||
} from "@llamaindex/chat-ui";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { DynamicComponentErrorBoundary } from "./error-boundary";
|
||||
import { ComponentDef } from "./types";
|
||||
|
||||
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,
|
||||
appendError,
|
||||
}: {
|
||||
componentDefs: ComponentDef[];
|
||||
appendError: (error: string) => void;
|
||||
}) => {
|
||||
const { message } = useChatMessage();
|
||||
const annotations = message.annotations;
|
||||
|
||||
const shownWarningsRef = useRef<Set<string>>(new Set()); // track warnings
|
||||
const [hasErrors, setHasErrors] = useState(false);
|
||||
|
||||
const handleError = (error: string) => {
|
||||
setHasErrors(true);
|
||||
appendError(error);
|
||||
};
|
||||
|
||||
// Check for missing components in annotations
|
||||
useEffect(() => {
|
||||
if (!annotations?.length) return;
|
||||
|
||||
const availableComponents = new Set(componentDefs.map((comp) => comp.type));
|
||||
|
||||
annotations.forEach((item: JSONValue) => {
|
||||
const annotation = item as MessageAnnotation;
|
||||
const type = annotation.type;
|
||||
if (!type) return; // Skip if annotation doesn't have a type
|
||||
|
||||
const events = getAnnotationData<JSONValue>(message, type);
|
||||
|
||||
// Skip if it's a built-in component or if we've already shown the warning
|
||||
if (
|
||||
BUILT_IN_CHATUI_COMPONENTS.includes(type as MessageAnnotationType) ||
|
||||
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} or having error when rendering it. Ensure there is a component file named ${type}.tsx or ${type}.jsx in your components directory, and verify the code for any errors.`,
|
||||
);
|
||||
shownWarningsRef.current.add(type);
|
||||
}
|
||||
});
|
||||
}, [annotations, componentDefs]);
|
||||
|
||||
const components: EventComponent[] = componentDefs
|
||||
.map((comp) => {
|
||||
const events = getAnnotationData<JSONValue>(message, comp.type);
|
||||
if (!events?.length) return null;
|
||||
return { ...comp, events };
|
||||
})
|
||||
.filter((comp) => comp !== null);
|
||||
|
||||
if (components.length === 0) return null;
|
||||
if (hasErrors) return null;
|
||||
|
||||
return (
|
||||
<div className="components-container">
|
||||
{components.map((component, index) => {
|
||||
return (
|
||||
<React.Fragment key={`${component.type}-${index}`}>
|
||||
{renderEventComponent(component, handleError)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function renderEventComponent(
|
||||
component: EventComponent,
|
||||
appendError: (error: string) => void,
|
||||
) {
|
||||
return (
|
||||
<DynamicComponentErrorBoundary
|
||||
onError={appendError}
|
||||
eventType={component.type}
|
||||
>
|
||||
{React.createElement(component.comp, { events: component.events })}
|
||||
</DynamicComponentErrorBoundary>
|
||||
);
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
fallback?: React.ReactNode;
|
||||
onError?: (errMsg: string) => void;
|
||||
eventType?: string;
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
export class DynamicComponentErrorBoundary extends React.Component<
|
||||
Props,
|
||||
State
|
||||
> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = { hasError: false, error: null };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): State {
|
||||
return { hasError: true, error };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||
console.warn(this.props.eventType, error, errorInfo);
|
||||
const errorMessage = `Fail to render ${this.props.eventType} component: ${error.message}`;
|
||||
this.props.onError?.(errorMessage);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return this.props.fallback || null;
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
"use client";
|
||||
|
||||
import { parse } from "@babel/parser";
|
||||
import traverse from "@babel/traverse";
|
||||
import React from "react";
|
||||
|
||||
export const SHADCN_IMPORT_PREFIX = "@/components/ui"; // all 46 Shadcn components
|
||||
|
||||
// Maps import paths in component code to Shadcn components and ChatUI widgets
|
||||
export const SOURCE_MAP: Record<string, () => Promise<any>> = {
|
||||
///// REACT /////
|
||||
[`react`]: () => import("react"),
|
||||
[`react-dom`]: () => import("react-dom"),
|
||||
|
||||
///// SHADCN COMPONENTS /////
|
||||
[`${SHADCN_IMPORT_PREFIX}/accordion`]: () => import("../../../accordion"),
|
||||
[`${SHADCN_IMPORT_PREFIX}/alert`]: () => import("../../../alert"),
|
||||
[`${SHADCN_IMPORT_PREFIX}/alert-dialog`]: () =>
|
||||
import("../../../alert-dialog"),
|
||||
[`${SHADCN_IMPORT_PREFIX}/aspect-ratio`]: () =>
|
||||
import("../../../aspect-ratio"),
|
||||
[`${SHADCN_IMPORT_PREFIX}/avatar`]: () => import("../../../avatar"),
|
||||
[`${SHADCN_IMPORT_PREFIX}/badge`]: () => import("../../../badge"),
|
||||
[`${SHADCN_IMPORT_PREFIX}/breadcrumb`]: () => import("../../../breadcrumb"),
|
||||
[`${SHADCN_IMPORT_PREFIX}/button`]: () => import("../../../button"),
|
||||
[`${SHADCN_IMPORT_PREFIX}/calendar`]: () => import("../../../calendar"),
|
||||
[`${SHADCN_IMPORT_PREFIX}/card`]: () => import("../../../card"),
|
||||
[`${SHADCN_IMPORT_PREFIX}/carousel`]: () => import("../../../carousel"),
|
||||
[`${SHADCN_IMPORT_PREFIX}/chart`]: () => import("../../../chart"),
|
||||
[`${SHADCN_IMPORT_PREFIX}/checkbox`]: () => import("../../../checkbox"),
|
||||
[`${SHADCN_IMPORT_PREFIX}/collapsible`]: () => import("../../../collapsible"),
|
||||
[`${SHADCN_IMPORT_PREFIX}/command`]: () => import("../../../command"),
|
||||
[`${SHADCN_IMPORT_PREFIX}/context-menu`]: () =>
|
||||
import("../../../context-menu"),
|
||||
[`${SHADCN_IMPORT_PREFIX}/dialog`]: () => import("../../../dialog"),
|
||||
[`${SHADCN_IMPORT_PREFIX}/drawer`]: () => import("../../../drawer"),
|
||||
[`${SHADCN_IMPORT_PREFIX}/dropdown-menu`]: () =>
|
||||
import("../../../dropdown-menu"),
|
||||
[`${SHADCN_IMPORT_PREFIX}/form`]: () => import("../../../form"),
|
||||
[`${SHADCN_IMPORT_PREFIX}/hover-card`]: () => import("../../../hover-card"),
|
||||
[`${SHADCN_IMPORT_PREFIX}/input`]: () => import("../../../input"),
|
||||
[`${SHADCN_IMPORT_PREFIX}/input-otp`]: () => import("../../../input-otp"),
|
||||
[`${SHADCN_IMPORT_PREFIX}/label`]: () => import("../../../label"),
|
||||
[`${SHADCN_IMPORT_PREFIX}/menubar`]: () => import("../../../menubar"),
|
||||
[`${SHADCN_IMPORT_PREFIX}/navigation-menu`]: () =>
|
||||
import("../../../navigation-menu"),
|
||||
[`${SHADCN_IMPORT_PREFIX}/pagination`]: () => import("../../../pagination"),
|
||||
[`${SHADCN_IMPORT_PREFIX}/popover`]: () => import("../../../popover"),
|
||||
[`${SHADCN_IMPORT_PREFIX}/progress`]: () => import("../../../progress"),
|
||||
[`${SHADCN_IMPORT_PREFIX}/radio-group`]: () => import("../../../radio-group"),
|
||||
[`${SHADCN_IMPORT_PREFIX}/resizable`]: () => import("../../../resizable"),
|
||||
[`${SHADCN_IMPORT_PREFIX}/scroll-area`]: () => import("../../../scroll-area"),
|
||||
[`${SHADCN_IMPORT_PREFIX}/select`]: () => import("../../../select"),
|
||||
[`${SHADCN_IMPORT_PREFIX}/separator`]: () => import("../../../separator"),
|
||||
[`${SHADCN_IMPORT_PREFIX}/sheet`]: () => import("../../../sheet"),
|
||||
[`${SHADCN_IMPORT_PREFIX}/sidebar`]: () => import("../../../sidebar"),
|
||||
[`${SHADCN_IMPORT_PREFIX}/skeleton`]: () => import("../../../skeleton"),
|
||||
[`${SHADCN_IMPORT_PREFIX}/slider`]: () => import("../../../slider"),
|
||||
[`${SHADCN_IMPORT_PREFIX}/sonner`]: () => import("../../../sonner"),
|
||||
[`${SHADCN_IMPORT_PREFIX}/switch`]: () => import("../../../switch"),
|
||||
[`${SHADCN_IMPORT_PREFIX}/table`]: () => import("../../../table"),
|
||||
[`${SHADCN_IMPORT_PREFIX}/tabs`]: () => import("../../../tabs"),
|
||||
[`${SHADCN_IMPORT_PREFIX}/textarea`]: () => import("../../../textarea"),
|
||||
[`${SHADCN_IMPORT_PREFIX}/toggle`]: () => import("../../../toggle"),
|
||||
[`${SHADCN_IMPORT_PREFIX}/toggle-group`]: () =>
|
||||
import("../../../toggle-group"),
|
||||
[`${SHADCN_IMPORT_PREFIX}/tooltip`]: () => import("../../../tooltip"),
|
||||
|
||||
///// CHAT_UI GENERAL /////
|
||||
[`@llamaindex/chat-ui`]: () => import("@llamaindex/chat-ui"),
|
||||
|
||||
///// WIDGETS FROM CHAT_UI /////
|
||||
[`@llamaindex/chat-ui/widgets`]: () => import("@llamaindex/chat-ui/widgets"),
|
||||
|
||||
///// ICONS /////
|
||||
[`lucide-react`]: () => import("lucide-react"),
|
||||
|
||||
///// UTILS /////
|
||||
[`@/components/lib/utils`]: () => import("../../../lib/utils"),
|
||||
[`@/lib/utils`]: () => import("../../../lib/utils"), // for v0 compatibility
|
||||
|
||||
///// ZOD /////
|
||||
[`zod`]: () => import("zod"),
|
||||
};
|
||||
|
||||
// parse imports from code to get Function constructor arguments and component name
|
||||
export async function parseImports(code: string) {
|
||||
const imports: { name: string; source: string }[] = []; // e.g., [{ name: "Button", source: "@/components/ui/button" }]
|
||||
let componentName: string | null = null;
|
||||
|
||||
const ast = parse(code, {
|
||||
sourceType: "module",
|
||||
plugins: ["jsx", "typescript"],
|
||||
});
|
||||
|
||||
// Traverse the AST to find import declarations
|
||||
traverse(ast, {
|
||||
// Find import declarations
|
||||
ImportDeclaration(path) {
|
||||
path.node.specifiers.forEach((specifier) => {
|
||||
if (
|
||||
specifier.type === "ImportSpecifier" ||
|
||||
specifier.type === "ImportDefaultSpecifier"
|
||||
) {
|
||||
imports.push({
|
||||
name: specifier.local.name, // e.g., "Button"
|
||||
source: path.node.source.value, // e.g., "@/components/ui/button"
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
// Find export default declaration
|
||||
ExportDefaultDeclaration(path) {
|
||||
const declaration = path.node.declaration;
|
||||
if (declaration.type === "FunctionDeclaration" && declaration.id) {
|
||||
componentName = declaration.id.name; // e.g., "EventTimeline"
|
||||
} else if (
|
||||
declaration.type === "Identifier" &&
|
||||
path.scope.hasBinding(declaration.name)
|
||||
) {
|
||||
componentName = declaration.name; // e.g., named function assigned to export
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Dynamically import the modules
|
||||
const importPromises = imports.map(async ({ name, source }) => {
|
||||
if (!(source in SOURCE_MAP)) {
|
||||
throw new Error(
|
||||
`Fail to import ${name} from ${source}. Reason: Module not found. \nCurrently we only support importing UI components from Shadcn components, widgets and hooks from "llamaindex/chat-ui", icons from "lucide-react" and zod for data validation.`,
|
||||
);
|
||||
}
|
||||
try {
|
||||
const module = await SOURCE_MAP[source]();
|
||||
return { name, module: module[name] };
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to resolve import ${name}. Please check the code and try again.`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const resolvedImports = await Promise.all(importPromises);
|
||||
|
||||
// Create a map of import names to their resolved modules (always include React)
|
||||
const importMap: Record<string, any> = { React };
|
||||
resolvedImports.forEach(({ name, module }) => {
|
||||
if (module) {
|
||||
importMap[name] = module;
|
||||
}
|
||||
});
|
||||
|
||||
return { componentName, importMap };
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
"use client";
|
||||
|
||||
import * as Babel from "@babel/standalone";
|
||||
import { FunctionComponent } from "react";
|
||||
import { getConfig } from "../../../lib/utils";
|
||||
import { parseImports } from "./import";
|
||||
import { ComponentDef } from "./types";
|
||||
|
||||
export type SourceComponentDef = {
|
||||
type: string;
|
||||
code: string;
|
||||
filename: string;
|
||||
};
|
||||
|
||||
export async function fetchComponentDefinitions(): Promise<{
|
||||
components: ComponentDef[];
|
||||
errors: string[];
|
||||
}> {
|
||||
const endpoint =
|
||||
getConfig("COMPONENTS_API") ??
|
||||
(process.env.NEXT_PUBLIC_USE_COMPONENTS_DIR === "true"
|
||||
? "/api/components"
|
||||
: undefined);
|
||||
if (!endpoint) {
|
||||
console.warn("/api/components endpoint is not defined in config");
|
||||
return { components: [], errors: [] };
|
||||
}
|
||||
|
||||
const response = await fetch(endpoint);
|
||||
const components = (await response.json()) as SourceComponentDef[];
|
||||
|
||||
// Only need to handle transpilation now
|
||||
const transpiledComponents = await Promise.all(
|
||||
components.map(async (comp) => {
|
||||
const { component, error } = await parseComponent(
|
||||
comp.code,
|
||||
comp.filename,
|
||||
);
|
||||
return {
|
||||
type: comp.type,
|
||||
comp: component,
|
||||
error,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const validComponents = transpiledComponents
|
||||
.map((comp) => ({
|
||||
type: comp.type,
|
||||
comp: comp.comp,
|
||||
}))
|
||||
.filter((comp): comp is ComponentDef => comp.comp !== null);
|
||||
|
||||
const uniqueErrors = transpiledComponents
|
||||
.map((comp) => comp.error)
|
||||
.filter((error): error is string => error !== undefined);
|
||||
|
||||
return {
|
||||
components: validComponents,
|
||||
errors: uniqueErrors,
|
||||
};
|
||||
}
|
||||
|
||||
// create React component from code
|
||||
export async function parseComponent(
|
||||
code: string,
|
||||
filename: string,
|
||||
): Promise<{ component: FunctionComponent<any> | null; error?: string }> {
|
||||
try {
|
||||
const [transpiledCode, resolvedImports] = await Promise.all([
|
||||
transpileCode(code, filename),
|
||||
parseImports(code),
|
||||
]);
|
||||
|
||||
const component = await createComponentFromCode(
|
||||
transpiledCode,
|
||||
resolvedImports.importMap,
|
||||
resolvedImports.componentName,
|
||||
);
|
||||
|
||||
return { component };
|
||||
} catch (error) {
|
||||
console.warn(`Failed to parse component from ${filename}`, error);
|
||||
return {
|
||||
component: null,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// convert TSX code to JS code using Babel, also remove all import declarations in the top of code
|
||||
async function transpileCode(code: string, filename: string) {
|
||||
const transpilationCustomPlugin = () => ({
|
||||
visitor: {
|
||||
ImportDeclaration(path: any) {
|
||||
// remove all import declarations in the top of code (already passed imports to Function constructor)
|
||||
// eg: import { Button } from "@/components/ui/button" -> remove
|
||||
path.remove();
|
||||
},
|
||||
ExportDefaultDeclaration(path: any) {
|
||||
// remove export default declaration (already passed component name to Function constructor)
|
||||
// eg: export default function EventTimeline() { ... } -> function EventTimeline() { ... }
|
||||
path.replaceWith(path.node.declaration);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const transpiledCode = Babel.transform(code, {
|
||||
presets: ["react", "typescript"],
|
||||
plugins: [transpilationCustomPlugin],
|
||||
filename,
|
||||
}).code;
|
||||
|
||||
if (!transpiledCode) {
|
||||
throw new Error(`Transpiled code is empty for ${filename}`);
|
||||
}
|
||||
|
||||
return transpiledCode;
|
||||
}
|
||||
|
||||
async function createComponentFromCode(
|
||||
transpiledCode: string,
|
||||
importMap: Record<string, any>,
|
||||
componentName: string | null = "Component",
|
||||
): Promise<FunctionComponent<any> | null> {
|
||||
const argNames = Object.keys(importMap); // e.g., ["React", "Button", "Badge"]
|
||||
const argValues = Object.values(importMap); // list of corresponding modules
|
||||
|
||||
// Create the component function
|
||||
const componentFn = new Function(
|
||||
...argNames,
|
||||
`${transpiledCode}; return ${componentName};`,
|
||||
);
|
||||
|
||||
// Call the component function with the imported modules
|
||||
return componentFn(...argValues);
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { JSONValue } from "@llamaindex/chat-ui";
|
||||
import { FunctionComponent } from "react";
|
||||
|
||||
export type ComponentDef = {
|
||||
type: string; // eg. deep_research_event
|
||||
comp: FunctionComponent<{ events: JSONValue[] }>;
|
||||
};
|
||||
@@ -1,191 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useChatUI } from "@llamaindex/chat-ui";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { getConfig } from "../../lib/utils";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "../../select";
|
||||
|
||||
type LLamaCloudPipeline = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
type LLamaCloudProject = {
|
||||
id: string;
|
||||
organization_id: string;
|
||||
name: string;
|
||||
is_default: boolean;
|
||||
pipelines: Array<LLamaCloudPipeline>;
|
||||
};
|
||||
|
||||
type PipelineConfig = {
|
||||
project: string; // project name
|
||||
pipeline: string; // pipeline name
|
||||
};
|
||||
|
||||
type LlamaCloudConfig = {
|
||||
projects?: LLamaCloudProject[];
|
||||
pipeline?: PipelineConfig;
|
||||
};
|
||||
|
||||
export interface LlamaCloudSelectorProps {
|
||||
onSelect?: (pipeline: PipelineConfig | undefined) => void;
|
||||
defaultPipeline?: PipelineConfig;
|
||||
shouldCheckValid?: boolean;
|
||||
}
|
||||
|
||||
export function LlamaCloudSelector({
|
||||
onSelect,
|
||||
defaultPipeline,
|
||||
shouldCheckValid = false,
|
||||
}: LlamaCloudSelectorProps) {
|
||||
const { setRequestData } = useChatUI();
|
||||
const [config, setConfig] = useState<LlamaCloudConfig>();
|
||||
|
||||
const updateRequestParams = useCallback(
|
||||
(pipeline?: PipelineConfig) => {
|
||||
if (setRequestData) {
|
||||
setRequestData({
|
||||
llamaCloudPipeline: pipeline,
|
||||
});
|
||||
} else {
|
||||
onSelect?.(pipeline);
|
||||
}
|
||||
},
|
||||
[onSelect, setRequestData],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const llamaCloudAPI =
|
||||
getConfig("LLAMA_CLOUD_API") ??
|
||||
(process.env.NEXT_PUBLIC_SHOW_LLAMACLOUD_SELECTOR === "true"
|
||||
? "/api/chat/config/llamacloud"
|
||||
: "");
|
||||
|
||||
if (!config && llamaCloudAPI) {
|
||||
fetch(llamaCloudAPI)
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
return response.json().then((errorData) => {
|
||||
window.alert(
|
||||
`Error: ${JSON.stringify(errorData) || "Unknown error occurred"}`,
|
||||
);
|
||||
});
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => {
|
||||
const pipeline = defaultPipeline ?? data.pipeline; // defaultPipeline will override pipeline in .env
|
||||
setConfig({ ...data, pipeline });
|
||||
updateRequestParams(pipeline);
|
||||
})
|
||||
.catch((error) => console.error("Error fetching config", error));
|
||||
}
|
||||
}, [config, defaultPipeline, updateRequestParams]);
|
||||
|
||||
const setPipeline = (pipelineConfig?: PipelineConfig) => {
|
||||
setConfig((prevConfig) => ({
|
||||
...prevConfig,
|
||||
pipeline: pipelineConfig,
|
||||
}));
|
||||
updateRequestParams(pipelineConfig);
|
||||
};
|
||||
|
||||
const handlePipelineSelect = async (value: string) => {
|
||||
setPipeline(JSON.parse(value) as PipelineConfig);
|
||||
};
|
||||
|
||||
if (!config) {
|
||||
return (
|
||||
<div className="flex items-center justify-center p-3">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (shouldCheckValid && !isValid(config.projects, config.pipeline)) {
|
||||
return (
|
||||
<p className="text-red-500">
|
||||
Invalid LlamaCloud configuration. Check console logs.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
const { projects, pipeline } = config;
|
||||
|
||||
return (
|
||||
<Select
|
||||
onValueChange={handlePipelineSelect}
|
||||
defaultValue={
|
||||
isValid(projects, pipeline, false)
|
||||
? JSON.stringify(pipeline)
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-[200px]">
|
||||
<SelectValue placeholder="Select a pipeline" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{projects!.map((project: LLamaCloudProject) => (
|
||||
<SelectGroup key={project.id}>
|
||||
<SelectLabel className="capitalize">
|
||||
Project: {project.name}
|
||||
</SelectLabel>
|
||||
{project.pipelines.map((pipeline) => (
|
||||
<SelectItem
|
||||
key={pipeline.id}
|
||||
className="last:border-b"
|
||||
value={JSON.stringify({
|
||||
pipeline: pipeline.name,
|
||||
project: project.name,
|
||||
})}
|
||||
>
|
||||
<span className="pl-2">{pipeline.name}</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
function isValid(
|
||||
projects: LLamaCloudProject[] | undefined,
|
||||
pipeline: PipelineConfig | undefined,
|
||||
logErrors: boolean = true,
|
||||
): boolean {
|
||||
if (!projects?.length) return false;
|
||||
if (!pipeline) return false;
|
||||
const matchedProject = projects.find(
|
||||
(project: LLamaCloudProject) => project.name === pipeline.project,
|
||||
);
|
||||
if (!matchedProject) {
|
||||
if (logErrors) {
|
||||
console.error(
|
||||
`LlamaCloud project ${pipeline.project} not found. Check LLAMA_CLOUD_PROJECT_NAME variable`,
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const pipelineExists = matchedProject.pipelines.some(
|
||||
(p) => p.name === pipeline.pipeline,
|
||||
);
|
||||
if (!pipelineExists) {
|
||||
if (logErrors) {
|
||||
console.error(
|
||||
`LlamaCloud pipeline ${pipeline.pipeline} not found. Check LLAMA_CLOUD_INDEX_NAME variable`,
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Markdown as MarkdownUI,
|
||||
SourceData,
|
||||
} from "@llamaindex/chat-ui/widgets";
|
||||
import { getConfig } from "../../lib/utils";
|
||||
const preprocessMedia = (content: string) => {
|
||||
// Remove `sandbox:` from the beginning of the URL before rendering markdown
|
||||
// OpenAI models sometimes prepend `sandbox:` to relative URLs - this fixes it
|
||||
return content.replace(/(sandbox|attachment|snt):/g, "");
|
||||
};
|
||||
|
||||
export function Markdown({
|
||||
content,
|
||||
sources,
|
||||
}: {
|
||||
content: string;
|
||||
sources?: SourceData;
|
||||
}) {
|
||||
const processedContent = preprocessMedia(content);
|
||||
return (
|
||||
<MarkdownUI
|
||||
content={processedContent}
|
||||
backend={getConfig("BACKEND")}
|
||||
sources={sources}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,274 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
CodeEditor,
|
||||
fileExtensionToEditorLang,
|
||||
} from "@llamaindex/chat-ui/widgets";
|
||||
import { AlertCircle, Loader2 } from "lucide-react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Button } from "../button";
|
||||
import { getConfig } from "../lib/utils";
|
||||
|
||||
const API_PATH = "/api/dev/files/workflow";
|
||||
const POLLING_TIMEOUT = 30_000; // 30 seconds
|
||||
|
||||
type WorkflowFile = {
|
||||
last_modified: number;
|
||||
file_path: string;
|
||||
content: string;
|
||||
};
|
||||
|
||||
export function DevModePanel() {
|
||||
const devModeEnabled =
|
||||
getConfig("DEV_MODE") ?? process.env.NEXT_PUBLIC_DEV_MODE === "true";
|
||||
if (!devModeEnabled) return null;
|
||||
return <DevModePanelComp />;
|
||||
}
|
||||
|
||||
function DevModePanelComp() {
|
||||
const [devModeOpen, setDevModeOpen] = useState(false);
|
||||
|
||||
const [isFetching, setIsFetching] = useState(false);
|
||||
const [fetchingError, setFetchingError] = useState<string | null>();
|
||||
const [workflowFile, setWorkflowFile] = useState<WorkflowFile | null>(null);
|
||||
|
||||
const [updatedCode, setUpdatedCode] = useState<string | null>(null);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [saveError, setSaveError] = useState<string | null>(null);
|
||||
|
||||
const [isPolling, setIsPolling] = useState(false);
|
||||
const [pollingError, setPollingError] = useState<string | null>(null);
|
||||
|
||||
async function fetchWorkflowCode() {
|
||||
try {
|
||||
setIsFetching(true);
|
||||
const response = await fetch(API_PATH);
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data?.detail ?? "Unknown error");
|
||||
}
|
||||
|
||||
setWorkflowFile(data);
|
||||
setFetchingError(null);
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "Unknown error";
|
||||
setFetchingError(errorMessage);
|
||||
console.warn("Error fetching workflow code:", error);
|
||||
} finally {
|
||||
setIsFetching(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function restartingWorkflow() {
|
||||
if (!workflowFile) return;
|
||||
|
||||
const initialLastModified = workflowFile.last_modified;
|
||||
setIsPolling(true);
|
||||
setPollingError(null);
|
||||
|
||||
const pollStartTime = Date.now();
|
||||
|
||||
// interval refetching the updated workflow code
|
||||
const poll = async () => {
|
||||
if (Date.now() - pollStartTime > POLLING_TIMEOUT) {
|
||||
setPollingError(
|
||||
`Server not responding after ${POLLING_TIMEOUT / 1000} seconds.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const pollResponse = await fetch(API_PATH);
|
||||
const pollData = (await pollResponse.json()) as WorkflowFile;
|
||||
if (pollData.last_modified !== initialLastModified) {
|
||||
setWorkflowFile(pollData);
|
||||
setUpdatedCode(pollData.content);
|
||||
setIsPolling(false);
|
||||
setPollingError(null);
|
||||
setDevModeOpen(false);
|
||||
} else {
|
||||
setTimeout(poll, 2000);
|
||||
}
|
||||
} catch (error) {
|
||||
console.info("Polling error", error);
|
||||
setTimeout(poll, 2000);
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(poll, 2000);
|
||||
}
|
||||
|
||||
const handleResetCode = () => {
|
||||
setUpdatedCode(workflowFile?.content ?? null);
|
||||
setSaveError(null);
|
||||
};
|
||||
|
||||
const handleSaveCode = async () => {
|
||||
if (!workflowFile) return;
|
||||
|
||||
try {
|
||||
setIsSaving(true);
|
||||
const response = await fetch(API_PATH, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
content: updatedCode,
|
||||
file_path: workflowFile.file_path,
|
||||
}),
|
||||
});
|
||||
const data = await response.json();
|
||||
if (!response.ok) {
|
||||
throw new Error(data?.detail ?? "Unknown error");
|
||||
}
|
||||
setSaveError(null);
|
||||
await restartingWorkflow();
|
||||
} catch (error) {
|
||||
console.warn("Error saving workflow code:", error);
|
||||
setSaveError(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Unknown error happened when saving workflow code",
|
||||
);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (devModeOpen) {
|
||||
fetchWorkflowCode();
|
||||
}
|
||||
}, [devModeOpen]);
|
||||
|
||||
const codeEditorLanguage = useMemo(() => {
|
||||
if (!workflowFile?.file_path) return undefined;
|
||||
return fileExtensionToEditorLang(
|
||||
workflowFile.file_path.split(".").pop() ?? "",
|
||||
);
|
||||
}, [workflowFile]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => setDevModeOpen(!devModeOpen)}
|
||||
className="fixed right-2 top-1/2 origin-right -translate-y-1/2 rotate-90 transform rounded-l-md shadow-md transition-transform hover:-translate-x-1"
|
||||
>
|
||||
Dev Mode
|
||||
</Button>
|
||||
|
||||
{isPolling && (
|
||||
<div className="fixed inset-0 z-50 flex flex-col items-center justify-center bg-black/50 backdrop-blur-sm">
|
||||
{!pollingError && (
|
||||
<>
|
||||
<Loader2 className="mb-4 h-16 w-16 animate-spin text-white" />
|
||||
<p className="text-lg font-semibold text-white">
|
||||
Applying changes and restarting server...
|
||||
</p>
|
||||
<p className="mt-2 text-sm text-slate-300">
|
||||
Please wait for a while then you can start chatting with the
|
||||
updated workflow.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
{pollingError && (
|
||||
<div className="bg-destructive/20 text-destructive-foreground mt-4 max-w-md rounded-md p-4 text-center">
|
||||
<div className="mb-2 flex items-center justify-center gap-2">
|
||||
<AlertCircle className="shrink-0" size={16} />
|
||||
<h6 className="text-sm font-medium">Server Starting Error</h6>
|
||||
</div>
|
||||
<p className="text-sm">{pollingError}</p>
|
||||
|
||||
<p className="text-sm">
|
||||
Please reload the page and check server logs.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={`border-border fixed right-0 top-0 z-10 h-full w-full border-l shadow-xl transition-all duration-300 ease-in-out ${
|
||||
devModeOpen ? "translate-x-0 bg-black/50" : "translate-x-full"
|
||||
}`}
|
||||
onClick={() => setDevModeOpen(false)}
|
||||
>
|
||||
<div
|
||||
className={`bg-background ml-auto flex h-full w-[800px] flex-col p-4`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold">Workflow Editor</h2>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{isFetching ? (
|
||||
"Loading..."
|
||||
) : workflowFile ? (
|
||||
<>
|
||||
Edit the code of <b>{workflowFile.file_path}</b> and save to
|
||||
apply changes to your workflow.
|
||||
</>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setDevModeOpen(false)}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto">
|
||||
{fetchingError ? (
|
||||
<div className="bg-destructive/10 text-destructive/70 mb-4 flex items-center gap-2 rounded-md p-4">
|
||||
<AlertCircle className="shrink-0" size={16} />
|
||||
<p className="text-sm font-medium">{fetchingError}</p>
|
||||
</div>
|
||||
) : (
|
||||
<CodeEditor
|
||||
code={updatedCode ?? workflowFile?.content ?? ""}
|
||||
onChange={setUpdatedCode}
|
||||
language={codeEditorLanguage}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-4 flex flex-col">
|
||||
{saveError && (
|
||||
<div className="bg-destructive/10 text-destructive/70 mb-4 rounded-md p-4">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<AlertCircle className="shrink-0" size={16} />
|
||||
<h6 className="text-sm font-medium">Error Saving Code</h6>
|
||||
</div>
|
||||
<p className="whitespace-pre-wrap text-sm">{saveError}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="mr-2"
|
||||
onClick={handleResetCode}
|
||||
>
|
||||
Reset Code
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSaveCode}
|
||||
disabled={isSaving || !updatedCode || !workflowFile}
|
||||
>
|
||||
Save & Restart Server
|
||||
{isSaving && <Loader2 className="ml-2 h-4 w-4 animate-spin" />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
|
||||
export interface useCopyToClipboardProps {
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
export function useCopyToClipboard({
|
||||
timeout = 2000,
|
||||
}: useCopyToClipboardProps) {
|
||||
const [isCopied, setIsCopied] = React.useState(false);
|
||||
|
||||
const copyToClipboard = (value: string) => {
|
||||
if (typeof window === "undefined" || !navigator.clipboard?.writeText) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(value).then(() => {
|
||||
setIsCopied(true);
|
||||
|
||||
setTimeout(() => {
|
||||
setIsCopied(false);
|
||||
}, timeout);
|
||||
});
|
||||
};
|
||||
|
||||
return { isCopied, copyToClipboard };
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Sparkles, Star } from "lucide-react";
|
||||
import { LLAMA_LOGO_URL } from "../../../../constants";
|
||||
|
||||
export function DefaultHeader() {
|
||||
return (
|
||||
<div className="flex items-center justify-between p-2 px-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Sparkles className="size-4" />
|
||||
<h1 className="font-semibold">LlamaIndex App</h1>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<a
|
||||
href="https://www.llamaindex.ai/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
>
|
||||
Built by LlamaIndex
|
||||
</a>
|
||||
<img
|
||||
className="h-[24px] w-[24px] rounded-sm"
|
||||
src={LLAMA_LOGO_URL}
|
||||
alt="Llama Logo"
|
||||
/>
|
||||
</div>
|
||||
<a
|
||||
href="https://github.com/run-llama/LlamaIndexTS"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:bg-accent flex items-center gap-2 rounded-md border border-gray-300 px-2 py-1 text-sm"
|
||||
>
|
||||
<Star className="size-4" />
|
||||
Star on GitHub
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Loader2 } from "lucide-react";
|
||||
import React, { FunctionComponent, useEffect, useState } from "react";
|
||||
import { getConfig } from "../../lib/utils";
|
||||
import { DynamicComponentErrorBoundary } from "../custom/events/error-boundary";
|
||||
import { parseComponent } from "../custom/events/loader";
|
||||
import { DefaultHeader } from "./header";
|
||||
|
||||
type LayoutFile = {
|
||||
type: "header" | "footer";
|
||||
code: string;
|
||||
filename: string;
|
||||
};
|
||||
|
||||
type LayoutComponent = LayoutFile & {
|
||||
component?: FunctionComponent | null;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
export function ChatLayout({ children }: { children: React.ReactNode }) {
|
||||
const [layoutComponents, setLayoutComponents] = useState<LayoutComponent[]>(
|
||||
[],
|
||||
);
|
||||
const [isRendering, setIsRendering] = useState(false);
|
||||
const [errors, setErrors] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const loadLayout = async () => {
|
||||
setIsRendering(true);
|
||||
const layoutFiles = await fetchLayoutFiles();
|
||||
if (layoutFiles.length) {
|
||||
const layoutComponents = await parseLayoutComponents(layoutFiles);
|
||||
setLayoutComponents(layoutComponents);
|
||||
setErrors((errors) => [
|
||||
...errors,
|
||||
...(layoutComponents.map((c) => c.error).filter(Boolean) as string[]),
|
||||
]);
|
||||
}
|
||||
setIsRendering(false);
|
||||
};
|
||||
|
||||
loadLayout();
|
||||
}, []);
|
||||
|
||||
const handleError = (error: string) => {
|
||||
setErrors((prev) => [...prev, error]);
|
||||
};
|
||||
|
||||
const getLayoutCode = (type: "header" | "footer") => {
|
||||
return layoutComponents.find((c) => c.type === type)?.component;
|
||||
};
|
||||
|
||||
if (isRendering) {
|
||||
return (
|
||||
<div className="flex h-screen w-screen flex-col items-center justify-center overflow-hidden">
|
||||
<Loader2 className="text-muted-foreground animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const uniqueErrors = [...new Set(errors)];
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-screen flex-col overflow-hidden">
|
||||
{uniqueErrors.length > 0 && (
|
||||
<div className="w-full bg-yellow-100 px-4 py-2 text-black/70">
|
||||
<h2 className="mb-2 font-semibold">
|
||||
Errors happened while rendering the layout:
|
||||
</h2>
|
||||
{uniqueErrors.map((error) => (
|
||||
<div key={error} className="text-sm">
|
||||
{error}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<LayoutRenderer
|
||||
component={getLayoutCode("header")}
|
||||
onError={handleError}
|
||||
fallback={<DefaultHeader />}
|
||||
/>
|
||||
|
||||
{children}
|
||||
|
||||
<LayoutRenderer
|
||||
component={getLayoutCode("footer")}
|
||||
onError={handleError}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LayoutRenderer({
|
||||
component,
|
||||
onError,
|
||||
fallback,
|
||||
}: {
|
||||
component?: FunctionComponent | null;
|
||||
onError: (error: string) => void;
|
||||
fallback?: React.ReactNode;
|
||||
}) {
|
||||
if (!component) return fallback;
|
||||
return (
|
||||
<DynamicComponentErrorBoundary onError={onError} fallback={fallback}>
|
||||
{React.createElement(component)}
|
||||
</DynamicComponentErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
async function parseLayoutComponents(layoutFiles: LayoutFile[]) {
|
||||
const layoutComponents: LayoutComponent[] = await Promise.all(
|
||||
layoutFiles.map(async (layoutFile) => {
|
||||
const result = await parseComponent(layoutFile.code, layoutFile.filename);
|
||||
return { ...layoutFile, ...result };
|
||||
}),
|
||||
);
|
||||
return layoutComponents;
|
||||
}
|
||||
|
||||
async function fetchLayoutFiles(): Promise<LayoutFile[]> {
|
||||
try {
|
||||
const layoutApi = getConfig("LAYOUT_API");
|
||||
if (!layoutApi) return [];
|
||||
const response = await fetch(layoutApi);
|
||||
const layoutFiles: LayoutFile[] = await response.json();
|
||||
return layoutFiles;
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "Unknown error";
|
||||
console.warn("Error fetching layout files: ", errorMessage);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Message,
|
||||
getAnnotationData,
|
||||
useChatMessage,
|
||||
useChatUI,
|
||||
} from "@llamaindex/chat-ui";
|
||||
import { JSONValue } from "ai";
|
||||
import { useMemo } from "react";
|
||||
import { WeatherCard, WeatherData } from "./weather-card";
|
||||
|
||||
export function ToolAnnotations() {
|
||||
// TODO: This is a bit of a hack to get the artifact version. better to generate the version in the tool call and
|
||||
// store it in CodeArtifact
|
||||
const { messages } = useChatUI();
|
||||
const { message } = useChatMessage();
|
||||
const artifactVersion = useMemo(
|
||||
() => getArtifactVersion(messages, message),
|
||||
[messages, message],
|
||||
);
|
||||
// Get the tool data from the message annotations
|
||||
const toolData = getAnnotationData<ToolData>(message, "tools");
|
||||
if (toolData.length === 0) return null;
|
||||
|
||||
return <ChatTools data={toolData[0]} artifactVersion={artifactVersion} />;
|
||||
}
|
||||
|
||||
// TODO: Used to render outputs of tools. If needed, add more renderers here.
|
||||
function ChatTools({
|
||||
data,
|
||||
artifactVersion,
|
||||
}: {
|
||||
data: ToolData;
|
||||
artifactVersion: number | undefined;
|
||||
}) {
|
||||
if (!data) return null;
|
||||
const { toolCall, toolOutput } = data;
|
||||
|
||||
if (toolOutput.isError) {
|
||||
return (
|
||||
<div className="border-l-2 border-red-400 pl-2">
|
||||
There was an error when calling the tool {toolCall.name} with input:{" "}
|
||||
<br />
|
||||
{JSON.stringify(toolCall.input)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
switch (toolCall.name) {
|
||||
case "get_weather_information": {
|
||||
const weatherData = toolOutput.output as unknown as WeatherData;
|
||||
return <WeatherCard data={weatherData} />;
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
type ToolData = {
|
||||
toolCall: {
|
||||
id: string;
|
||||
name: string;
|
||||
input: {
|
||||
[key: string]: JSONValue;
|
||||
};
|
||||
};
|
||||
toolOutput: {
|
||||
output: JSONValue;
|
||||
isError: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
function getArtifactVersion(
|
||||
messages: Message[],
|
||||
message: Message,
|
||||
): number | undefined {
|
||||
const messageId = "id" in message ? message.id : undefined;
|
||||
if (!messageId) return undefined;
|
||||
let versionIndex = 1;
|
||||
for (const m of messages) {
|
||||
const toolData = getAnnotationData<ToolData>(m, "tools");
|
||||
|
||||
if (toolData?.some((t) => t.toolCall.name === "artifact")) {
|
||||
if ("id" in m && m.id === messageId) {
|
||||
return versionIndex;
|
||||
}
|
||||
versionIndex++;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -1,215 +0,0 @@
|
||||
"use client";
|
||||
|
||||
export interface WeatherData {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
generationtime_ms: number;
|
||||
utc_offset_seconds: number;
|
||||
timezone: string;
|
||||
timezone_abbreviation: string;
|
||||
elevation: number;
|
||||
current_units: {
|
||||
time: string;
|
||||
interval: string;
|
||||
temperature_2m: string;
|
||||
weather_code: string;
|
||||
};
|
||||
current: {
|
||||
time: string;
|
||||
interval: number;
|
||||
temperature_2m: number;
|
||||
weather_code: number;
|
||||
};
|
||||
hourly_units: {
|
||||
time: string;
|
||||
temperature_2m: string;
|
||||
weather_code: string;
|
||||
};
|
||||
hourly: {
|
||||
time: string[];
|
||||
temperature_2m: number[];
|
||||
weather_code: number[];
|
||||
};
|
||||
daily_units: {
|
||||
time: string;
|
||||
weather_code: string;
|
||||
};
|
||||
daily: {
|
||||
time: string[];
|
||||
weather_code: number[];
|
||||
};
|
||||
}
|
||||
|
||||
// Follow WMO Weather interpretation codes (WW)
|
||||
const weatherCodeDisplayMap: Record<
|
||||
string,
|
||||
{
|
||||
icon: React.ReactNode;
|
||||
status: string;
|
||||
}
|
||||
> = {
|
||||
"0": {
|
||||
icon: <span>☀️</span>,
|
||||
status: "Clear sky",
|
||||
},
|
||||
"1": {
|
||||
icon: <span>🌤️</span>,
|
||||
status: "Mainly clear",
|
||||
},
|
||||
"2": {
|
||||
icon: <span>☁️</span>,
|
||||
status: "Partly cloudy",
|
||||
},
|
||||
"3": {
|
||||
icon: <span>☁️</span>,
|
||||
status: "Overcast",
|
||||
},
|
||||
"45": {
|
||||
icon: <span>🌫️</span>,
|
||||
status: "Fog",
|
||||
},
|
||||
"48": {
|
||||
icon: <span>🌫️</span>,
|
||||
status: "Depositing rime fog",
|
||||
},
|
||||
"51": {
|
||||
icon: <span>🌧️</span>,
|
||||
status: "Drizzle",
|
||||
},
|
||||
"53": {
|
||||
icon: <span>🌧️</span>,
|
||||
status: "Drizzle",
|
||||
},
|
||||
"55": {
|
||||
icon: <span>🌧️</span>,
|
||||
status: "Drizzle",
|
||||
},
|
||||
"56": {
|
||||
icon: <span>🌧️</span>,
|
||||
status: "Freezing Drizzle",
|
||||
},
|
||||
"57": {
|
||||
icon: <span>🌧️</span>,
|
||||
status: "Freezing Drizzle",
|
||||
},
|
||||
"61": {
|
||||
icon: <span>🌧️</span>,
|
||||
status: "Rain",
|
||||
},
|
||||
"63": {
|
||||
icon: <span>🌧️</span>,
|
||||
status: "Rain",
|
||||
},
|
||||
"65": {
|
||||
icon: <span>🌧️</span>,
|
||||
status: "Rain",
|
||||
},
|
||||
"66": {
|
||||
icon: <span>🌧️</span>,
|
||||
status: "Freezing Rain",
|
||||
},
|
||||
"67": {
|
||||
icon: <span>🌧️</span>,
|
||||
status: "Freezing Rain",
|
||||
},
|
||||
"71": {
|
||||
icon: <span>❄️</span>,
|
||||
status: "Snow fall",
|
||||
},
|
||||
"73": {
|
||||
icon: <span>❄️</span>,
|
||||
status: "Snow fall",
|
||||
},
|
||||
"75": {
|
||||
icon: <span>❄️</span>,
|
||||
status: "Snow fall",
|
||||
},
|
||||
"77": {
|
||||
icon: <span>❄️</span>,
|
||||
status: "Snow grains",
|
||||
},
|
||||
"80": {
|
||||
icon: <span>🌧️</span>,
|
||||
status: "Rain showers",
|
||||
},
|
||||
"81": {
|
||||
icon: <span>🌧️</span>,
|
||||
status: "Rain showers",
|
||||
},
|
||||
"82": {
|
||||
icon: <span>🌧️</span>,
|
||||
status: "Rain showers",
|
||||
},
|
||||
"85": {
|
||||
icon: <span>❄️</span>,
|
||||
status: "Snow showers",
|
||||
},
|
||||
"86": {
|
||||
icon: <span>❄️</span>,
|
||||
status: "Snow showers",
|
||||
},
|
||||
"95": {
|
||||
icon: <span>⛈️</span>,
|
||||
status: "Thunderstorm",
|
||||
},
|
||||
"96": {
|
||||
icon: <span>⛈️</span>,
|
||||
status: "Thunderstorm",
|
||||
},
|
||||
"99": {
|
||||
icon: <span>⛈️</span>,
|
||||
status: "Thunderstorm",
|
||||
},
|
||||
};
|
||||
|
||||
const displayDay = (time: string) => {
|
||||
return new Date(time).toLocaleDateString("en-US", {
|
||||
weekday: "long",
|
||||
});
|
||||
};
|
||||
|
||||
export function WeatherCard({ data }: { data: WeatherData }) {
|
||||
const currentDayString = new Date(data.current.time).toLocaleDateString(
|
||||
"en-US",
|
||||
{
|
||||
weekday: "long",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="w-fit space-y-4 rounded-2xl bg-[#61B9F2] p-5 text-white shadow-xl">
|
||||
<div className="flex justify-between">
|
||||
<div className="space-y-2">
|
||||
<div className="text-xl">{currentDayString}</div>
|
||||
<div className="flex gap-4 text-5xl font-semibold">
|
||||
<span>
|
||||
{data.current.temperature_2m} {data.current_units.temperature_2m}
|
||||
</span>
|
||||
{weatherCodeDisplayMap[data.current.weather_code].icon}
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xl">
|
||||
{weatherCodeDisplayMap[data.current.weather_code].status}
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-6 gap-2">
|
||||
{data.daily.time.map((time, index) => {
|
||||
if (index === 0) return null; // skip the current day
|
||||
return (
|
||||
<div key={time} className="flex flex-col items-center gap-4">
|
||||
<span>{displayDay(time)}</span>
|
||||
<div className="text-4xl">
|
||||
{weatherCodeDisplayMap[data.daily.weather_code[index]].icon}
|
||||
</div>
|
||||
<span className="text-sm">
|
||||
{weatherCodeDisplayMap[data.daily.weather_code[index]].status}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "./lib/utils";
|
||||
|
||||
function Checkbox({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
||||
return (
|
||||
<CheckboxPrimitive.Root
|
||||
data-slot="checkbox"
|
||||
className={cn(
|
||||
"border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive shadow-xs peer size-4 shrink-0 rounded-[4px] border outline-none transition-shadow focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
data-slot="checkbox-indicator"
|
||||
className="flex items-center justify-center text-current transition-none"
|
||||
>
|
||||
<CheckIcon className="size-3.5" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
||||
export { Checkbox };
|
||||
@@ -1,11 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
|
||||
|
||||
const Collapsible = CollapsiblePrimitive.Root;
|
||||
|
||||
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
|
||||
|
||||
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
|
||||
|
||||
export { Collapsible, CollapsibleContent, CollapsibleTrigger };
|
||||
@@ -1,177 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Command as CommandPrimitive } from "cmdk";
|
||||
import { SearchIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "./dialog";
|
||||
import { cn } from "./lib/utils";
|
||||
|
||||
function Command({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive>) {
|
||||
return (
|
||||
<CommandPrimitive
|
||||
data-slot="command"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandDialog({
|
||||
title = "Command Palette",
|
||||
description = "Search for a command to run...",
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Dialog> & {
|
||||
title?: string;
|
||||
description?: string;
|
||||
}) {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogHeader className="sr-only">
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogContent className="overflow-hidden p-0">
|
||||
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandInput({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="command-input-wrapper"
|
||||
className="flex h-9 items-center gap-2 border-b px-3"
|
||||
>
|
||||
<SearchIcon className="size-4 shrink-0 opacity-50" />
|
||||
<CommandPrimitive.Input
|
||||
data-slot="command-input"
|
||||
className={cn(
|
||||
"placeholder:text-muted-foreground outline-hidden flex h-10 w-full rounded-md bg-transparent py-3 text-sm disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandList({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.List>) {
|
||||
return (
|
||||
<CommandPrimitive.List
|
||||
data-slot="command-list"
|
||||
className={cn(
|
||||
"max-h-[300px] scroll-py-1 overflow-y-auto overflow-x-hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandEmpty({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
||||
return (
|
||||
<CommandPrimitive.Empty
|
||||
data-slot="command-empty"
|
||||
className="py-6 text-center text-sm"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandGroup({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
|
||||
return (
|
||||
<CommandPrimitive.Group
|
||||
data-slot="command-group"
|
||||
className={cn(
|
||||
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
|
||||
return (
|
||||
<CommandPrimitive.Separator
|
||||
data-slot="command-separator"
|
||||
className={cn("bg-border -mx-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
|
||||
return (
|
||||
<CommandPrimitive.Item
|
||||
data-slot="command-item"
|
||||
className={cn(
|
||||
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="command-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
CommandSeparator,
|
||||
CommandShortcut,
|
||||
};
|
||||
@@ -1,252 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "./lib/utils";
|
||||
|
||||
function ContextMenu({
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
|
||||
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />;
|
||||
}
|
||||
|
||||
function ContextMenuTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuSub({
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
|
||||
return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />;
|
||||
}
|
||||
|
||||
function ContextMenuRadioGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.RadioGroup
|
||||
data-slot="context-menu-radio-group"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuSubTrigger({
|
||||
className,
|
||||
inset,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<ContextMenuPrimitive.SubTrigger
|
||||
data-slot="context-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground outline-hidden flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm data-[inset]:pl-8 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto" />
|
||||
</ContextMenuPrimitive.SubTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuSubContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.SubContent
|
||||
data-slot="context-menu-sub-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-context-menu-content-transform-origin) z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Portal>
|
||||
<ContextMenuPrimitive.Content
|
||||
data-slot="context-menu-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 max-h-(--radix-context-menu-content-available-height) origin-(--radix-context-menu-content-transform-origin) z-50 min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border p-1 shadow-md",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</ContextMenuPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuItem({
|
||||
className,
|
||||
inset,
|
||||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
variant?: "default" | "destructive";
|
||||
}) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Item
|
||||
data-slot="context-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm data-[disabled]:pointer-events-none data-[inset]:pl-8 data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuCheckboxItem({
|
||||
className,
|
||||
children,
|
||||
checked,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.CheckboxItem
|
||||
data-slot="context-menu-checkbox-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-8 pr-2 text-sm data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<ContextMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</ContextMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</ContextMenuPrimitive.CheckboxItem>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuRadioItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.RadioItem
|
||||
data-slot="context-menu-radio-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-8 pr-2 text-sm data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<ContextMenuPrimitive.ItemIndicator>
|
||||
<CircleIcon className="size-2 fill-current" />
|
||||
</ContextMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</ContextMenuPrimitive.RadioItem>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuLabel({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Label
|
||||
data-slot="context-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Separator
|
||||
data-slot="context-menu-separator"
|
||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="context-menu-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
ContextMenu,
|
||||
ContextMenuCheckboxItem,
|
||||
ContextMenuContent,
|
||||
ContextMenuGroup,
|
||||
ContextMenuItem,
|
||||
ContextMenuLabel,
|
||||
ContextMenuPortal,
|
||||
ContextMenuRadioGroup,
|
||||
ContextMenuRadioItem,
|
||||
ContextMenuSeparator,
|
||||
ContextMenuShortcut,
|
||||
ContextMenuSub,
|
||||
ContextMenuSubContent,
|
||||
ContextMenuSubTrigger,
|
||||
ContextMenuTrigger,
|
||||
};
|
||||
@@ -1,135 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { XIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "./lib/utils";
|
||||
|
||||
function Dialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
|
||||
}
|
||||
|
||||
function DialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function DialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
|
||||
}
|
||||
|
||||
function DialogClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
|
||||
}
|
||||
|
||||
function DialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<DialogPrimitive.Overlay
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content>) {
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed left-[50%] top-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground rounded-xs focus:outline-hidden absolute right-4 top-4 opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0">
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn("text-lg font-semibold leading-none", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
};
|
||||
@@ -1,132 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { Drawer as DrawerPrimitive } from "vaul";
|
||||
|
||||
import { cn } from "./lib/utils";
|
||||
|
||||
function Drawer({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
|
||||
return <DrawerPrimitive.Root data-slot="drawer" {...props} />;
|
||||
}
|
||||
|
||||
function DrawerTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
|
||||
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function DrawerPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
|
||||
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />;
|
||||
}
|
||||
|
||||
function DrawerClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
|
||||
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />;
|
||||
}
|
||||
|
||||
function DrawerOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
|
||||
return (
|
||||
<DrawerPrimitive.Overlay
|
||||
data-slot="drawer-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DrawerContent({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
|
||||
return (
|
||||
<DrawerPortal data-slot="drawer-portal">
|
||||
<DrawerOverlay />
|
||||
<DrawerPrimitive.Content
|
||||
data-slot="drawer-content"
|
||||
className={cn(
|
||||
"group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
|
||||
"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b",
|
||||
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
|
||||
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
|
||||
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
|
||||
{children}
|
||||
</DrawerPrimitive.Content>
|
||||
</DrawerPortal>
|
||||
);
|
||||
}
|
||||
|
||||
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="drawer-header"
|
||||
className={cn("flex flex-col gap-1.5 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="drawer-footer"
|
||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DrawerTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
|
||||
return (
|
||||
<DrawerPrimitive.Title
|
||||
data-slot="drawer-title"
|
||||
className={cn("text-foreground font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DrawerDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
|
||||
return (
|
||||
<DrawerPrimitive.Description
|
||||
data-slot="drawer-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Drawer,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerDescription,
|
||||
DrawerFooter,
|
||||
DrawerHeader,
|
||||
DrawerOverlay,
|
||||
DrawerPortal,
|
||||
DrawerTitle,
|
||||
DrawerTrigger,
|
||||
};
|
||||
@@ -1,257 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "./lib/utils";
|
||||
|
||||
function DropdownMenu({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Trigger
|
||||
data-slot="dropdown-menu-trigger"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuContent({
|
||||
className,
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
data-slot="dropdown-menu-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 max-h-(--radix-dropdown-menu-content-available-height) origin-(--radix-dropdown-menu-content-transform-origin) z-50 min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border p-1 shadow-md",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuItem({
|
||||
className,
|
||||
inset,
|
||||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
variant?: "default" | "destructive";
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Item
|
||||
data-slot="dropdown-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm data-[disabled]:pointer-events-none data-[inset]:pl-8 data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuCheckboxItem({
|
||||
className,
|
||||
children,
|
||||
checked,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-8 pr-2 text-sm data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuRadioGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioGroup
|
||||
data-slot="dropdown-menu-radio-group"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuRadioItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-8 pr-2 text-sm data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CircleIcon className="size-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuLabel({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Label
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
data-slot="dropdown-menu-separator"
|
||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSub({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuSubTrigger({
|
||||
className,
|
||||
inset,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground outline-hidden flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm data-[inset]:pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto size-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSubContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-dropdown-menu-content-transform-origin) z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
};
|
||||
@@ -1,168 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import * as React from "react";
|
||||
import {
|
||||
Controller,
|
||||
FormProvider,
|
||||
useFormContext,
|
||||
useFormState,
|
||||
type ControllerProps,
|
||||
type FieldPath,
|
||||
type FieldValues,
|
||||
} from "react-hook-form";
|
||||
|
||||
import { Label } from "./label";
|
||||
import { cn } from "./lib/utils";
|
||||
|
||||
const Form = FormProvider;
|
||||
|
||||
type FormFieldContextValue<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
> = {
|
||||
name: TName;
|
||||
};
|
||||
|
||||
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||
{} as FormFieldContextValue,
|
||||
);
|
||||
|
||||
const FormField = <
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
>({
|
||||
...props
|
||||
}: ControllerProps<TFieldValues, TName>) => {
|
||||
return (
|
||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||
<Controller {...props} />
|
||||
</FormFieldContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const useFormField = () => {
|
||||
const fieldContext = React.useContext(FormFieldContext);
|
||||
const itemContext = React.useContext(FormItemContext);
|
||||
const { getFieldState } = useFormContext();
|
||||
const formState = useFormState({ name: fieldContext.name });
|
||||
const fieldState = getFieldState(fieldContext.name, formState);
|
||||
|
||||
if (!fieldContext) {
|
||||
throw new Error("useFormField should be used within <FormField>");
|
||||
}
|
||||
|
||||
const { id } = itemContext;
|
||||
|
||||
return {
|
||||
id,
|
||||
name: fieldContext.name,
|
||||
formItemId: `${id}-form-item`,
|
||||
formDescriptionId: `${id}-form-item-description`,
|
||||
formMessageId: `${id}-form-item-message`,
|
||||
...fieldState,
|
||||
};
|
||||
};
|
||||
|
||||
type FormItemContextValue = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
const FormItemContext = React.createContext<FormItemContextValue>(
|
||||
{} as FormItemContextValue,
|
||||
);
|
||||
|
||||
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
|
||||
const id = React.useId();
|
||||
|
||||
return (
|
||||
<FormItemContext.Provider value={{ id }}>
|
||||
<div
|
||||
data-slot="form-item"
|
||||
className={cn("grid gap-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
</FormItemContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function FormLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||
const { error, formItemId } = useFormField();
|
||||
|
||||
return (
|
||||
<Label
|
||||
data-slot="form-label"
|
||||
data-error={!!error}
|
||||
className={cn("data-[error=true]:text-destructive", className)}
|
||||
htmlFor={formItemId}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
|
||||
const { error, formItemId, formDescriptionId, formMessageId } =
|
||||
useFormField();
|
||||
|
||||
return (
|
||||
<Slot
|
||||
data-slot="form-control"
|
||||
id={formItemId}
|
||||
aria-describedby={
|
||||
!error
|
||||
? `${formDescriptionId}`
|
||||
: `${formDescriptionId} ${formMessageId}`
|
||||
}
|
||||
aria-invalid={!!error}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||
const { formDescriptionId } = useFormField();
|
||||
|
||||
return (
|
||||
<p
|
||||
data-slot="form-description"
|
||||
id={formDescriptionId}
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
|
||||
const { error, formMessageId } = useFormField();
|
||||
const body = error ? String(error?.message ?? "") : props.children;
|
||||
|
||||
if (!body) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<p
|
||||
data-slot="form-message"
|
||||
id={formMessageId}
|
||||
className={cn("text-destructive text-sm", className)}
|
||||
{...props}
|
||||
>
|
||||
{body}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
useFormField,
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
|
||||
const MOBILE_BREAKPOINT = 768;
|
||||
|
||||
export function useIsMobile() {
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
||||
const onChange = () => {
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||
};
|
||||
mql.addEventListener("change", onChange);
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||
return () => mql.removeEventListener("change", onChange);
|
||||
}, []);
|
||||
|
||||
return !!isMobile;
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "./lib/utils";
|
||||
|
||||
function HoverCard({
|
||||
...props
|
||||
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
|
||||
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />;
|
||||
}
|
||||
|
||||
function HoverCardTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
|
||||
return (
|
||||
<HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function HoverCardContent({
|
||||
className,
|
||||
align = "center",
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
|
||||
return (
|
||||
<HoverCardPrimitive.Portal data-slot="hover-card-portal">
|
||||
<HoverCardPrimitive.Content
|
||||
data-slot="hover-card-content"
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-hover-card-content-transform-origin) outline-hidden z-50 w-64 rounded-md border p-4 shadow-md",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</HoverCardPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
export { HoverCard, HoverCardContent, HoverCardTrigger };
|
||||
@@ -1,77 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { OTPInput, OTPInputContext } from "input-otp";
|
||||
import { MinusIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "./lib/utils";
|
||||
|
||||
function InputOTP({
|
||||
className,
|
||||
containerClassName,
|
||||
...props
|
||||
}: React.ComponentProps<typeof OTPInput> & {
|
||||
containerClassName?: string;
|
||||
}) {
|
||||
return (
|
||||
<OTPInput
|
||||
data-slot="input-otp"
|
||||
containerClassName={cn(
|
||||
"flex items-center gap-2 has-disabled:opacity-50",
|
||||
containerClassName,
|
||||
)}
|
||||
className={cn("disabled:cursor-not-allowed", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="input-otp-group"
|
||||
className={cn("flex items-center", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function InputOTPSlot({
|
||||
index,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
index: number;
|
||||
}) {
|
||||
const inputOTPContext = React.useContext(OTPInputContext);
|
||||
const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {};
|
||||
|
||||
return (
|
||||
<div
|
||||
data-slot="input-otp-slot"
|
||||
data-active={isActive}
|
||||
className={cn(
|
||||
"data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input shadow-xs relative flex h-9 w-9 items-center justify-center border-y border-r text-sm outline-none transition-all first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{char}
|
||||
{hasFakeCaret && (
|
||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||
<div className="animate-caret-blink bg-foreground h-4 w-px duration-1000" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div data-slot="input-otp-separator" role="separator" {...props}>
|
||||
<MinusIcon />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot };
|
||||
@@ -1,23 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "./lib/utils";
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input shadow-xs flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base outline-none transition-[color,box-shadow] file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Input };
|
||||
@@ -1,24 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "./lib/utils";
|
||||
|
||||
function Label({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||
return (
|
||||
<LabelPrimitive.Root
|
||||
data-slot="label"
|
||||
className={cn(
|
||||
"flex select-none items-center gap-2 text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-50 group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Label };
|
||||
@@ -1,13 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
export const getConfig = (name: string) => {
|
||||
if (typeof window === "undefined") return undefined;
|
||||
return (window as any).LLAMAINDEX?.[name];
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user