Merge branch 'main' into add-mcp-flows

This commit is contained in:
Edwin Jose 2025-10-03 11:09:29 -04:00
commit c3b9bf8e90
64 changed files with 2995 additions and 2008 deletions

View file

@ -6,8 +6,7 @@ on:
- main
paths:
- 'docs/**'
# Review gh actions docs if you want to further define triggers, paths, etc
# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#on
workflow_dispatch:
jobs:
deploy:

2
.gitignore vendored
View file

@ -20,4 +20,4 @@ wheels/
!flows/*.json
.DS_Store
config.yaml
config/

View file

@ -33,8 +33,8 @@ RUN uv sync --frozen --no-install-project --no-editable --extra postgresql
# Build frontend
WORKDIR /app/src/frontend
RUN npm ci && \
npm run build && \
RUN NODE_OPTIONS=--max_old_space_size=4096 npm ci && \
NODE_OPTIONS=--max_old_space_size=4096 npm run build && \
mkdir -p /app/src/backend/base/langflow/frontend && \
cp -r build/* /app/src/backend/base/langflow/frontend/

View file

@ -62,7 +62,7 @@ LANGFLOW_CHAT_FLOW_ID=your_chat_flow_id
LANGFLOW_INGEST_FLOW_ID=your_ingest_flow_id
NUDGES_FLOW_ID=your_nudges_flow_id
```
See extended configuration, including ingestion and optional variables: [docs/configure/configuration.md](docs/docs/configure/configuration.md)
See extended configuration, including ingestion and optional variables: [docs/reference/configuration.md](docs/docs/reference/configuration.md)
### 3. Start OpenRAG
```bash

View file

@ -0,0 +1,4 @@
:::info
OpenRAG is is currently in public preview.
Development is ongoing, and the features and functionality are subject to change.
:::

View file

@ -2,4 +2,4 @@ import Icon from "@site/src/components/icon/icon";
All flows included with OpenRAG are designed to be modular, performant, and provider-agnostic.
To modify a flow, click <Icon name="Settings2" aria-hidden="true"/> **Settings**, and click **Edit in Langflow**.
Flows are edited in the same way as in the [Langflow visual editor](https://docs.langflow.org/concepts-overview).
OpenRAG's visual editor is based on the [Langflow visual editor](https://docs.langflow.org/concepts-overview), so you can edit your flows to match your specific use case.

View file

@ -0,0 +1,49 @@
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
## Application onboarding
The first time you start OpenRAG, whether using the TUI or a `.env` file, you must complete application onboarding.
Most values from onboarding can be changed later in the OpenRAG **Settings** page, but there are important restrictions.
The **language model provider** and **embeddings model provider** can only be selected at onboarding, and you must use the same provider for your language model and embedding model.
To change your provider selection later, you must completely reinstall OpenRAG.
The **language model** can be changed later in **Settings**, but the **embeddings model** cannot be changed later.
<Tabs groupId="Provider">
<TabItem value="OpenAI" label="OpenAI" default>
1. Enable **Get API key from environment variable** to automatically enter your key from the TUI-generated `.env` file.
2. Under **Advanced settings**, select your **Embedding Model** and **Language Model**.
3. To load 2 sample PDFs, enable **Sample dataset**.
This is recommended, but not required.
4. Click **Complete**.
5. Continue with the [Quickstart](/quickstart).
</TabItem>
<TabItem value="IBM watsonx.ai" label="IBM watsonx.ai">
1. Complete the fields for **watsonx.ai API Endpoint**, **IBM API key**, and **IBM Project ID**.
These values are found in your IBM watsonx deployment.
2. Under **Advanced settings**, select your **Embedding Model** and **Language Model**.
3. To load 2 sample PDFs, enable **Sample dataset**.
This is recommended, but not required.
4. Click **Complete**.
5. Continue with the [Quickstart](/quickstart).
</TabItem>
<TabItem value="Ollama" label="Ollama">
:::tip
Ollama is not included with OpenRAG. To install Ollama, see the [Ollama documentation](https://docs.ollama.com/).
:::
1. Enter your Ollama server's base URL address.
The default Ollama server address is `http://localhost:11434`.
OpenRAG automatically transforms `localhost` to access services outside of the container, and sends a test connection to your Ollama server to confirm connectivity.
2. Select the **Embedding Model** and **Language Model** your Ollama server is running.
OpenRAG retrieves the available models from your Ollama server.
3. To load 2 sample PDFs, enable **Sample dataset**.
This is recommended, but not required.
4. Click **Complete**.
5. Continue with the [Quickstart](/quickstart).
</TabItem>
</Tabs>

View file

@ -1,108 +0,0 @@
---
title: Configuration
slug: /configure/configuration
---
# Configuration
OpenRAG supports multiple configuration methods with the following priority:
1. **Environment Variables** (highest priority)
2. **Configuration File** (`config.yaml`)
3. **Langflow Flow Settings** (runtime override)
4. **Default Values** (fallback)
## Configuration File
Create a `config.yaml` file in the project root to configure OpenRAG:
```yaml
# OpenRAG Configuration File
provider:
model_provider: "openai" # openai, anthropic, azure, etc.
api_key: "your-api-key" # or use OPENAI_API_KEY env var
knowledge:
embedding_model: "text-embedding-3-small"
chunk_size: 1000
chunk_overlap: 200
ocr: true
picture_descriptions: false
agent:
llm_model: "gpt-4o-mini"
system_prompt: "You are a helpful AI assistant..."
```
## Environment Variables
Environment variables will override configuration file settings. You can still use `.env` files:
```bash
cp .env.example .env
```
## Required Variables
| Variable | Description |
| ----------------------------- | ------------------------------------------- |
| `OPENAI_API_KEY` | Your OpenAI API key |
| `OPENSEARCH_PASSWORD` | Password for OpenSearch admin user |
| `LANGFLOW_SUPERUSER` | Langflow admin username |
| `LANGFLOW_SUPERUSER_PASSWORD` | Langflow admin password |
| `LANGFLOW_CHAT_FLOW_ID` | ID of your Langflow chat flow |
| `LANGFLOW_INGEST_FLOW_ID` | ID of your Langflow ingestion flow |
| `NUDGES_FLOW_ID` | ID of your Langflow nudges/suggestions flow |
## Ingestion Configuration
| Variable | Description |
| ------------------------------ | ------------------------------------------------------ |
| `DISABLE_INGEST_WITH_LANGFLOW` | Disable Langflow ingestion pipeline (default: `false`) |
- `false` or unset: Uses Langflow pipeline (upload → ingest → delete)
- `true`: Uses traditional OpenRAG processor for document ingestion
## Optional Variables
| Variable | Description |
| ------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| `LANGFLOW_PUBLIC_URL` | Public URL for Langflow (default: `http://localhost:7860`) |
| `GOOGLE_OAUTH_CLIENT_ID` / `GOOGLE_OAUTH_CLIENT_SECRET` | Google OAuth authentication |
| `MICROSOFT_GRAPH_OAUTH_CLIENT_ID` / `MICROSOFT_GRAPH_OAUTH_CLIENT_SECRET` | Microsoft OAuth |
| `WEBHOOK_BASE_URL` | Base URL for webhook endpoints |
| `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` | AWS integrations |
| `SESSION_SECRET` | Session management (default: auto-generated, change in production) |
| `LANGFLOW_KEY` | Explicit Langflow API key (auto-generated if not provided) |
| `LANGFLOW_SECRET_KEY` | Secret key for Langflow internal operations |
## OpenRAG Configuration Variables
These environment variables override settings in `config.yaml`:
### Provider Settings
| Variable | Description | Default |
| ------------------ | ---------------------------------------- | -------- |
| `MODEL_PROVIDER` | Model provider (openai, anthropic, etc.) | `openai` |
| `PROVIDER_API_KEY` | API key for the model provider | |
| `OPENAI_API_KEY` | OpenAI API key (backward compatibility) | |
### Knowledge Settings
| Variable | Description | Default |
| ------------------------------ | --------------------------------------- | ------------------------ |
| `EMBEDDING_MODEL` | Embedding model for vector search | `text-embedding-3-small` |
| `CHUNK_SIZE` | Text chunk size for document processing | `1000` |
| `CHUNK_OVERLAP` | Overlap between chunks | `200` |
| `OCR_ENABLED` | Enable OCR for image processing | `true` |
| `PICTURE_DESCRIPTIONS_ENABLED` | Enable picture descriptions | `false` |
### Agent Settings
| Variable | Description | Default |
| --------------- | --------------------------------- | ------------------------ |
| `LLM_MODEL` | Language model for the chat agent | `gpt-4o-mini` |
| `SYSTEM_PROMPT` | System prompt for the agent | Default assistant prompt |
See `.env.example` for a complete list with descriptions, and `docker-compose*.yml` for runtime usage.

View file

@ -1,19 +1,25 @@
---
title: Agents powered by Langflow
title: Langflow Agents
slug: /agents
---
import Icon from "@site/src/components/icon/icon";
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import PartialModifyFlows from '@site/docs/_partial-modify-flows.mdx';
import PartialModifyFlows from '@site/docs/_partial-modify-flows.mdx';
import PartialExternalPreview from '@site/docs/_partial-external-preview.mdx';
<PartialExternalPreview />
OpenRAG leverages Langflow's Agent component to power the OpenRAG OpenSearch Agent flow.
This flow intelligently chats with your knowledge by embedding your query, comparing it the vector database embeddings, and generating a response with the LLM.
[Flows](https://docs.langflow.org/concepts-overview) in Langflow are functional representations of application workflows, with multiple [component](https://docs.langflow.org/concepts-components) nodes connected as single steps in a workflow.
The Agent component shines here in its ability to make decisions on not only what query should be sent, but when a query is necessary to solve the problem at hand.
In the OpenRAG OpenSearch Agent flow, components like the Langflow [**Agent** component](https://docs.langflow.org/agents) and [**OpenSearch** component](https://docs.langflow.org/bundles-elastic#opensearch) are connected to intelligently chat with your knowledge by embedding your query, comparing it the vector database embeddings, and generating a response with the LLM.
![OpenRAG Open Search Agent Flow](/img/opensearch-agent-flow.png)
The Agent component shines here in its ability to make decisions on not only what query should be sent, but when a query is necessary to solve the problem at hand.
<details closed>
<summary>How do agents work?</summary>
@ -31,22 +37,32 @@ In an agentic context, tools are functions that the agent can run to perform tas
## Use the OpenRAG OpenSearch Agent flow
If you've chatted with your knowledge in OpenRAG, you've already experienced the OpenRAG OpenSearch Agent chat flow.
To view the flow, click <Icon name="Settings2" aria-hidden="true"/> **Settings**, and then click **Edit in Langflow**.
This flow contains seven components:
To switch OpenRAG over to the [Langflow visual editor](https://docs.langflow.org/concepts-overview) and view the OpenRAG OpenSearch Agentflow, click <Icon name="Settings2" aria-hidden="true"/> **Settings**, and then click **Edit in Langflow**.
This flow contains seven components connected together to chat with your data:
* The Agent component orchestrates the entire flow by deciding when to search the knowledge base, how to formulate search queries, and how to combine retrieved information with the user's question to generate a comprehensive response.
The Agent behaves according to the prompt in the **Agent Instructions** field.
* The Chat Input component is connected to the Agent component's Input port. This allows to flow to be triggered by an incoming prompt from a user or application.
* The OpenSearch component is connected to the Agent component's Tools port. The agent may not use this database for every request; the agent only uses this connection if it decides the knowledge can help respond to the prompt.
* The Language Model component is connected to the Agent component's Language Model port. The agent uses the connected LLM to reason through the request sent through Chat Input.
* The Embedding Model component is connected to the OpenSearch component's Embedding port. This component converts text queries into vector representations that are compared with document embeddings stored in OpenSearch for semantic similarity matching. This gives your Agent's queries context.
* The Text Input component is populated with the global variable `OPENRAG-QUERY-FILTER`.
This filter is the Knowledge filter, and filters which knowledge sources to search through.
* The Agent component's Output port is connected to the Chat Output component, which returns the final response to the user or application.
* The [**Agent** component](https://docs.langflow.org/agents) orchestrates the entire flow by deciding when to search the knowledge base, how to formulate search queries, and how to combine retrieved information with the user's question to generate a comprehensive response.
The **Agent** behaves according to the prompt in the **Agent Instructions** field.
* The [**Chat Input** component](https://docs.langflow.org/components-io) is connected to the Agent component's Input port. This allows to flow to be triggered by an incoming prompt from a user or application.
* The [**OpenSearch** component](https://docs.langflow.org/bundles-elastic#opensearch) is connected to the Agent component's Tools port. The agent may not use this database for every request; the agent only uses this connection if it decides the knowledge can help respond to the prompt.
* The [**Language Model** component](https://docs.langflow.org/components-models) is connected to the Agent component's Language Model port. The agent uses the connected LLM to reason through the request sent through Chat Input.
* The [**Embedding Model** component](https://docs.langflow.org/components-embedding-models) is connected to the OpenSearch component's Embedding port. This component converts text queries into vector representations that are compared with document embeddings stored in OpenSearch for semantic similarity matching. This gives your Agent's queries context.
* The [**Text Input** component](https://docs.langflow.org/components-io) is populated with the global variable `OPENRAG-QUERY-FILTER`.
This filter is the [Knowledge filter](/knowledge#create-knowledge-filters), and filters which knowledge sources to search through.
* The **Agent** component's Output port is connected to the [**Chat Output** component](https://docs.langflow.org/components-io), which returns the final response to the user or application.
<PartialModifyFlows />
For an example of changing out the agent's LLM in OpenRAG, see the [Quickstart](/quickstart#change-components).
For an example of changing out the agent's language model in OpenRAG, see the [Quickstart](/quickstart#change-components).
To restore the flow to its initial state, in OpenRAG, click <Icon name="Settings" aria-hidden="true"/> **Settings**, and then click **Restore Flow**.
OpenRAG warns you that this discards all custom settings. Click **Restore** to restore the flow.
OpenRAG warns you that this discards all custom settings. Click **Restore** to restore the flow.
## Additional Langflow functionality
Langflow includes features beyond Agents to help you integrate OpenRAG into your application, and all Langflow features are included in OpenRAG.
* Langflow can serve your flows as an [MCP server](https://docs.langflow.org/mcp-server), or consume other MCP servers as an [MCP client](https://docs.langflow.org/mcp-client). Get started with the [MCP tutorial](https://docs.langflow.org/mcp-tutorial).
* If you don't see the component you need, extend Langflow's functionality by creating [custom Python components](https://docs.langflow.org/components-custom-components).
* Langflow offers component [bundles](https://docs.langflow.org/components-bundle-components) to integrate with many popular vector stores, AI/ML providers, and search APIs.

View file

@ -7,6 +7,9 @@ import Icon from "@site/src/components/icon/icon";
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import PartialModifyFlows from '@site/docs/_partial-modify-flows.mdx';
import PartialExternalPreview from '@site/docs/_partial-external-preview.mdx';
<PartialExternalPreview />
OpenRAG uses [Docling](https://docling-project.github.io/docling/) for its document ingestion pipeline.
More specifically, OpenRAG uses [Docling Serve](https://github.com/docling-project/docling-serve), which starts a `docling-serve` process on your local machine and runs Docling ingestion through an API service.
@ -43,7 +46,7 @@ If OpenRAG detects that the local machine is running on macOS, OpenRAG uses the
## Use OpenRAG default ingestion instead of Docling serve
If you want to use OpenRAG's built-in pipeline instead of Docling serve, set `DISABLE_INGEST_WITH_LANGFLOW=true` in [Environment variables](/configure/configuration#ingestion-configuration).
If you want to use OpenRAG's built-in pipeline instead of Docling serve, set `DISABLE_INGEST_WITH_LANGFLOW=true` in [Environment variables](/reference/configuration#document-processing).
The built-in pipeline still uses the Docling processor, but uses it directly without the Docling Serve API.

View file

@ -7,21 +7,14 @@ import Icon from "@site/src/components/icon/icon";
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import PartialModifyFlows from '@site/docs/_partial-modify-flows.mdx';
import PartialExternalPreview from '@site/docs/_partial-external-preview.mdx';
<PartialExternalPreview />
OpenRAG uses [OpenSearch](https://docs.opensearch.org/latest/) for its vector-backed knowledge store.
This is a specialized database for storing and retrieving embeddings, which helps your Agent efficiently find relevant information.
OpenSearch provides powerful hybrid search capabilities with enterprise-grade security and multi-tenancy support.
## Explore knowledge
The Knowledge page lists the documents OpenRAG has ingested into the OpenSearch vector database's `documents` index.
To explore your current knowledge, click <Icon name="Library" aria-hidden="true"/> **Knowledge**.
Click on a document to display the chunks derived from splitting the default documents into the vector database.
Documents are processed with the default **Knowledge Ingest** flow, so if you want to split your documents differently, edit the **Knowledge Ingest** flow.
<PartialModifyFlows />
## Ingest knowledge
OpenRAG supports knowledge ingestion through direct file uploads and OAuth connectors.
@ -30,7 +23,7 @@ OpenRAG supports knowledge ingestion through direct file uploads and OAuth conne
The **Knowledge Ingest** flow uses Langflow's [**File** component](https://docs.langflow.org/components-data#file) to split and embed files loaded from your local machine into the OpenSearch database.
The default path to your local folder is mounted from the `./documents` folder in your OpenRAG project directory to the `/app/documents/` directory inside the Docker container. Files added to the host or the container will be visible in both locations. To configure this location, modify the **Documents Paths** variable in either the TUI's [Advanced Setup](/install#advanced-setup) or in the `.env` used by Docker Compose.
The default path to your local folder is mounted from the `./documents` folder in your OpenRAG project directory to the `/app/documents/` directory inside the Docker container. Files added to the host or the container will be visible in both locations. To configure this location, modify the **Documents Paths** variable in either the TUI's [Advanced Setup](/install#setup) menu or in the `.env` used by Docker Compose.
To load and process a single file from the mapped location, click <Icon name="Plus" aria-hidden="true"/> **Add Knowledge**, and then click **Add File**.
The file is loaded into your OpenSearch database, and appears in the Knowledge page.
@ -54,7 +47,7 @@ If you wish to use another provider, add the secrets to another provider.
<TabItem value="TUI" label="TUI" default>
1. If OpenRAG is running, stop it with **Status** > **Stop Services**.
2. Click **Advanced Setup**.
3. Add the OAuth provider's client and secret key in the [Advanced Setup](/install#advanced-setup) menu.
3. Add the OAuth provider's client and secret key in the [Advanced Setup](/install#setup) menu.
4. Click **Save Configuration**.
The TUI generates a new `.env` file with your OAuth values.
5. Click **Start Container Services**.
@ -97,6 +90,17 @@ You can monitor the sync progress in the <Icon name="Bell" aria-hidden="true"/>
Once processing is complete, the synced documents become available in your knowledge base and can be searched through the chat interface or Knowledge page.
## Explore knowledge
The **Knowledge** page lists the documents OpenRAG has ingested into the OpenSearch vector database's `documents` index.
To explore your current knowledge, click <Icon name="Library" aria-hidden="true"/> **Knowledge**.
Click on a document to display the chunks derived from splitting the default documents into the vector database.
Documents are processed with the default **Knowledge Ingest** flow, so if you want to split your documents differently, edit the **Knowledge Ingest** flow.
<PartialModifyFlows />
### Knowledge ingestion settings
To configure the knowledge ingestion pipeline parameters, see [Docling Ingestion](/ingestion).
@ -136,7 +140,7 @@ A new filter is created with default settings that match everything.
OpenRAG automatically detects and configures the correct vector dimensions for embedding models, ensuring optimal search performance and compatibility.
The complete list of supported models is available at [/src/services/models_service.py](https://github.com/langflow-ai/openrag/blob/main/src/services/models_service.py).
The complete list of supported models is available at [`models_service.py` in the OpenRAG repository](https://github.com/langflow-ai/openrag/blob/main/src/services/models_service.py).
You can use custom embedding models by specifying them in your configuration.
@ -144,4 +148,4 @@ If you use an unknown embedding model, OpenRAG will automatically fall back to `
The default embedding dimension is `1536` and the default model is `text-embedding-3-small`.
For models with known vector dimensions, see [/src/config/settings.py](https://github.com/langflow-ai/openrag/blob/main/src/config/settings.py).
For models with known vector dimensions, see [`settings.py` in the OpenRAG repository](https://github.com/langflow-ai/openrag/blob/main/src/config/settings.py).

View file

@ -1,8 +1,13 @@
---
title: Docker deployment
title: Deploy with Docker
slug: /get-started/docker
---
import PartialOnboarding from '@site/docs/_partial-onboarding.mdx';
import PartialExternalPreview from '@site/docs/_partial-external-preview.mdx';
<PartialExternalPreview />
There are two different Docker Compose files.
They deploy the same applications and containers, but to different environments.
@ -10,7 +15,18 @@ They deploy the same applications and containers, but to different environments.
- [`docker-compose-cpu.yml`](https://github.com/langflow-ai/openrag/blob/main/docker-compose-cpu.yml) is a CPU-only version of OpenRAG for systems without GPU support. Use this Docker compose file for environments where GPU drivers aren't available.
To install OpenRAG with Docker Compose:
## Prerequisites
- [Python Version 3.10 to 3.13](https://www.python.org/downloads/release/python-3100/)
- [uv](https://docs.astral.sh/uv/getting-started/installation/)
- [Podman](https://podman.io/docs/installation) (recommended) or [Docker](https://docs.docker.com/get-docker/) installed
- [Docker Compose](https://docs.docker.com/compose/install/) installed. If you're using Podman, use [podman-compose](https://docs.podman.io/en/latest/markdown/podman-compose.1.html) or alias Docker compose commands to Podman commands.
- Create an [OpenAI API key](https://platform.openai.com/api-keys). This key is **required** to start OpenRAG, but you can choose a different model provider during [Application Onboarding](#application-onboarding).
- Optional: GPU support requires an NVIDIA GPU with CUDA support and compatible NVIDIA drivers installed on the OpenRAG host machine. If you don't have GPU capabilities, OpenRAG provides an alternate CPU-only deployment.
## Deploy OpenRAG with Docker Compose
To install OpenRAG with Docker Compose, do the following:
1. Clone the OpenRAG repository.
```bash
@ -18,7 +34,7 @@ To install OpenRAG with Docker Compose:
cd openrag
```
2. Copy the example `.env` file that is included in the repository root.
2. Copy the example `.env` file included in the repository root.
The example file includes all environment variables with comments to guide you in finding and setting their values.
```bash
cp .env.example .env
@ -29,18 +45,18 @@ To install OpenRAG with Docker Compose:
touch .env
```
3. Set environment variables. The Docker Compose files are populated with values from your `.env`, so the following values are **required** to be set:
3. Set environment variables. The Docker Compose files will be populated with values from your `.env`.
The following values are **required** to be set:
```bash
OPENSEARCH_PASSWORD=your_secure_password
OPENAI_API_KEY=your_openai_api_key
LANGFLOW_SUPERUSER=admin
LANGFLOW_SUPERUSER_PASSWORD=your_langflow_password
LANGFLOW_SECRET_KEY=your_secret_key
```
For more information on configuring OpenRAG with environment variables, see [Environment variables](/configure/configuration).
For additional configuration values, including `config.yaml`, see [Configuration](/configure/configuration).
For more information on configuring OpenRAG with environment variables, see [Environment variables](/reference/configuration).
4. Deploy OpenRAG with Docker Compose based on your deployment type.
@ -75,14 +91,39 @@ To install OpenRAG with Docker Compose:
- **Backend API**: http://localhost:8000
- **Langflow**: http://localhost:7860
Continue with the [Quickstart](/quickstart).
6. Continue with [Application Onboarding](#application-onboarding).
## Rebuild all Docker containers
<PartialOnboarding />
If you need to reset state and rebuild all of your containers, run the following command.
## Container management commands
Manage your OpenRAG containers with the following commands.
These commands are also available in the TUI's [Status menu](/get-started/tui#status).
### Upgrade containers
Upgrade your containers to the latest version while preserving your data.
```bash
docker compose pull
docker compose up -d --force-recreate
```
### Rebuild containers (destructive)
Reset state by rebuilding all of your containers.
Your OpenSearch and Langflow databases will be lost.
Documents stored in the `./documents` directory will persist, since the directory is mounted as a volume in the OpenRAG backend container.
```bash
docker compose up --build --force-recreate --remove-orphans
```
### Remove all containers and data (destructive)
Completely remove your OpenRAG installation and delete all data.
This deletes all of your data, including OpenSearch data, uploaded documents, and authentication.
```bash
docker compose down --volumes --remove-orphans --rmi local
docker system prune -f
```

View file

@ -5,38 +5,51 @@ slug: /install
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import PartialOnboarding from '@site/docs/_partial-onboarding.mdx';
import PartialExternalPreview from '@site/docs/_partial-external-preview.mdx';
OpenRAG can be installed in multiple ways:
<PartialExternalPreview />
* [**Python wheel**](#install-python-wheel): Install the OpenRAG Python wheel and use the [OpenRAG Terminal User Interface (TUI)](/get-started/tui) to install, run, and configure your OpenRAG deployment without running Docker commands.
[Install the OpenRAG Python wheel](#install-python-wheel), and then run the [OpenRAG Terminal User Interface(TUI)](#setup) to start your OpenRAG deployment with a guided setup process.
* [**Docker Compose**](get-started/docker): Clone the OpenRAG repository and deploy OpenRAG with Docker Compose, including all services and dependencies.
If you prefer running Docker commands and manually editing `.env` files, see [Deploy with Docker](/get-started/docker).
## Prerequisites
- [Python Version 3.10 to 3.13](https://www.python.org/downloads/release/python-3100/)
- [uv](https://docs.astral.sh/uv/getting-started/installation/)
- [Docker](https://docs.docker.com/get-docker/) or [Podman](https://podman.io/docs/installation) installed
- [Podman](https://podman.io/docs/installation) (recommended) or [Docker](https://docs.docker.com/get-docker/) installed
- [Docker Compose](https://docs.docker.com/compose/install/) installed. If using Podman, use [podman-compose](https://docs.podman.io/en/latest/markdown/podman-compose.1.html) or alias Docker compose commands to Podman commands.
- For GPU support: (TBD)
- Create an [OpenAI API key](https://platform.openai.com/api-keys). This key is **required** to start OpenRAG, but you can choose a different model provider during [Application Onboarding](#application-onboarding).
- Optional: GPU support requires an NVIDIA GPU with [CUDA](https://docs.nvidia.com/cuda/) support and compatible NVIDIA drivers installed on the OpenRAG host machine. If you don't have GPU capabilities, OpenRAG provides an alternate CPU-only deployment.
## Python wheel {#install-python-wheel}
## Install the OpenRAG Python wheel {#install-python-wheel}
The Python wheel is currently available internally, but will be available on PyPI at launch.
The wheel installs the OpenRAG wheel, which includes the TUI for installing, running, and managing OpenRAG.
For more information on virtual environments, see [uv](https://docs.astral.sh/uv/pip/environments).
:::important
The `.whl` file is currently available as an internal download during public preview, and will be published to PyPI in a future release.
:::
1. Create a new project with a virtual environment using [uv](https://docs.astral.sh/uv/pip/environments).
The OpenRAG wheel installs the Terminal User Interface (TUI) for configuring and running OpenRAG.
1. Create a new project with a virtual environment using `uv init`.
```bash
uv init YOUR_PROJECT_NAME
cd YOUR_PROJECT_NAME
```
2. Add the OpenRAG wheel to your project and install it in the virtual environment.
Replace `PATH/TO/` and `VERSION` with your OpenRAG wheel location and version.
The `(venv)` prompt doesn't change, but `uv` commands will automatically use the project's virtual environment.
For more information on virtual environments, see the [uv documentation](https://docs.astral.sh/uv/pip/environments).
2. Add the local OpenRAG wheel to your project's virtual environment.
```bash
uv add PATH/TO/openrag-VERSION-py3-none-any.whl
```
Replace `PATH/TO/` and `VERSION` with the path and version of your downloaded OpenRAG `.whl` file.
For example, if your `.whl` file is in the `~/Downloads` directory, the command is `uv add ~/Downloads/openrag-0.1.8-py3-none-any.whl`.
3. Ensure all dependencies are installed and updated in your virtual environment.
```bash
uv sync
@ -47,95 +60,66 @@ For more information on virtual environments, see [uv](https://docs.astral.sh/uv
uv run openrag
```
The OpenRAG TUI opens.
5. Continue with [Setup OpenRAG with the TUI](#setup).
5. To install OpenRAG with Basic Setup, click **Basic Setup** or press <kbd>1</kbd>. Basic Setup does not set up OAuth connections for ingestion from Google Drive, OneDrive, or AWS. For OAuth setup, see [Advanced Setup](#advanced-setup).
The TUI prompts you for the required startup values.
Click **Generate Passwords** to autocomplete fields that contain **Auto-generated Secure Password**, or bring your own passwords.
<details closed>
<summary>Where do I find the required startup values?</summary>
| Variable | Where to Find | Description |
|----------|---------------|-------------|
| `OPENSEARCH_PASSWORD` | Auto-generated secure password | The password for OpenSearch database access. Must be at least 8 characters and must contain at least one uppercase letter, one lowercase letter, one digit, and one special character. |
| `OPENAI_API_KEY` | [OpenAI Platform](https://platform.openai.com/api-keys) | API key from your OpenAI account. |
| `LANGFLOW_SUPERUSER` | User generated | Username for Langflow admin access. For more, see [Langflow docs](https://docs.langflow.org/api-keys-and-authentication#langflow-superuser). |
| `LANGFLOW_SUPERUSER_PASSWORD` | Auto-generated secure password | Password for Langflow admin access. For more, see the [Langflow docs](https://docs.langflow.org/api-keys-and-authentication#langflow-superuser). |
| `LANGFLOW_SECRET_KEY` | Auto-generated secure key | Secret key for Langflow security. For more, see the [Langflow docs](https://docs.langflow.org/api-keys-and-authentication#langflow-secret-key). |
| `LANGFLOW_AUTO_LOGIN` | Auto-generated or manual | Auto-login configuration. For more, see the [Langflow docs](https://docs.langflow.org/api-keys-and-authentication#langflow-auto-login). |
| `LANGFLOW_NEW_USER_IS_ACTIVE` | Langflow | New user activation setting. For more, see the [Langflow docs](https://docs.langflow.org/api-keys-and-authentication#langflow-new-user-is-active). |
| `LANGFLOW_ENABLE_SUPERUSER_CLI` | Langflow server | Superuser CLI access setting. For more, see the [Langflow docs](https://docs.langflow.org/api-keys-and-authentication#langflow-enable-superuser-cli). |
| `DOCUMENTS_PATH` | Set your local path | Path to your document storage directory. |
</details>
To complete credentials, click **Save Configuration**.
## Set up OpenRAG with the TUI {#setup}
6. To start OpenRAG with your credentials, click **Start Container Services**.
Startup pulls container images and starts them, so it can take some time.
The operation has completed when the **Close** button is available and the terminal displays:
```bash
Services started successfully
Command completed successfully
```
The TUI creates a `.env` file in your OpenRAG directory root and starts OpenRAG.
7. To open the OpenRAG application, click **Open App**, press <kbd>6</kbd>, or navigate to `http://localhost:3000`.
The application opens.
8. Select your language model and embedding model provider, and complete the required fields.
**Your provider can only be selected once, and you must use the same provider for your language model and embedding model.**
The language model can be changed, but the embeddings model cannot be changed.
To change your provider selection, you must restart OpenRAG and delete the `config.yml` file.
**Basic Setup** generates all of the required values except the OpenAI API key.
**Basic Setup** does not set up OAuth connections for ingestion from Google Drive, OneDrive, or AWS.
For OAuth setup, use **Advanced Setup**.
<Tabs groupId="Embedding provider">
<TabItem value="OpenAI" label="OpenAI" default>
9. If you already entered a value for `OPENAI_API_KEY` in the TUI in Step 5, enable **Get API key from environment variable**.
10. Under **Advanced settings**, select your **Embedding Model** and **Language Model**.
11. To load 2 sample PDFs, enable **Sample dataset**.
This is recommended, but not required.
12. Click **Complete**.
</TabItem>
<TabItem value="IBM watsonx.ai" label="IBM watsonx.ai">
9. Complete the fields for **watsonx.ai API Endpoint**, **IBM API key**, and **IBM Project ID**.
These values are found in your IBM watsonx deployment.
10. Under **Advanced settings**, select your **Embedding Model** and **Language Model**.
11. To load 2 sample PDFs, enable **Sample dataset**.
This is recommended, but not required.
12. Click **Complete**.
</TabItem>
<TabItem value="Ollama" label="Ollama">
9. Enter your Ollama server's base URL address.
The default Ollama server address is `http://localhost:11434`.
Since OpenRAG is running in a container, you may need to change `localhost` to access services outside of the container. For example, change `http://localhost:11434` to `http://host.docker.internal:11434` to connect to Ollama.
OpenRAG automatically sends a test connection to your Ollama server to confirm connectivity.
10. Select the **Embedding Model** and **Language Model** your Ollama server is running.
OpenRAG automatically lists the available models from your Ollama server.
11. To load 2 sample PDFs, enable **Sample dataset**.
This is recommended, but not required.
12. Click **Complete**.
If the TUI detects OAuth credentials, it enforces the **Advanced Setup** path.
If the TUI detects a `.env` file in the OpenRAG root directory, it will source any variables from the `.env` file.
<Tabs groupId="Setup method">
<TabItem value="Basic setup" label="Basic setup" default>
1. To install OpenRAG with **Basic Setup**, click **Basic Setup** or press <kbd>1</kbd>.
2. Click **Generate Passwords** to generate passwords for OpenSearch and Langflow.
3. Paste your OpenAI API key in the OpenAI API key field.
4. Click **Save Configuration**.
5. To start OpenRAG, click **Start Container Services**.
Startup pulls container images and runs them, so it can take some time.
When startup is complete, the TUI displays the following:
```bash
Services started successfully
Command completed successfully
```
6. To open the OpenRAG application, click **Open App**.
7. Continue with [Application Onboarding](#application-onboarding).
</TabItem>
<TabItem value="Advanced setup" label="Advanced setup">
1. To install OpenRAG with **Advanced Setup**, click **Advanced Setup** or press <kbd>2</kbd>.
2. Click **Generate Passwords** to generate passwords for OpenSearch and Langflow.
3. Paste your OpenAI API key in the OpenAI API key field.
4. Add your client and secret values for Google, Azure, or AWS OAuth.
These values can be found in your OAuth provider.
5. The OpenRAG TUI presents redirect URIs for your OAuth app.
These are the URLs your OAuth provider will redirect back to after user sign-in.
Register these redirect values with your OAuth provider as they are presented in the TUI.
6. Click **Save Configuration**.
7. To start OpenRAG, click **Start Container Services**.
Startup pulls container images and runs them, so it can take some time.
When startup is complete, the TUI displays the following:
```bash
Services started successfully
Command completed successfully
```
8. To open the OpenRAG application, click **Open App**, press <kbd>6</kbd>, or navigate to `http://localhost:3000`.
You will be presented with your provider's OAuth sign-in screen, and be redirected to the redirect URI after sign-in.
Continue with Application Onboarding.
Two additional variables are available for Advanced Setup:
The `LANGFLOW_PUBLIC_URL` controls where the Langflow web interface can be accessed. This is where users interact with their flows in a browser.
The `WEBHOOK_BASE_URL` controls where the endpoint for `/connectors/CONNECTOR_TYPE/webhook` will be available.
This connection enables real-time document synchronization with external services.
For example, for Google Drive file synchronization the webhook URL is `/connectors/google_drive/webhook`.
</TabItem>
9. Continue with [Application Onboarding](#application-onboarding).
</TabItem>
</Tabs>
13. Continue with the [Quickstart](/quickstart).
### Advanced Setup {#advanced-setup}
**Advanced Setup** includes the required values from **Basic Setup**, with additional settings for OAuth credentials.
If the OpenRAG TUI detects OAuth credentials, it enforces the Advanced Setup path.
1. Add your client and secret values for Google, Azure, or AWS OAuth.
These values can be found in your OAuth provider.
2. The OpenRAG TUI presents redirect URIs for your OAuth app.
These are the URLs your OAuth provider will redirect back to after user sign-in.
Register these redirect values with your OAuth provider as they are presented in the TUI.
3. To open the OpenRAG application, click **Open App** or press <kbd>6</kbd>.
You will be presented with your provider's OAuth sign-in screen, and be redirected to the redirect URI after sign-in.
Two additional variables are available for Advanced Setup:
The `LANGFLOW_PUBLIC_URL` controls where the Langflow web interface can be accessed. This is where users interact with their flows in a browser.
The `WEBHOOK_BASE_URL` controls where the endpoint for `/connectors/CONNECTOR_TYPE/webhook` will be available.
This connection enables real-time document synchronization with external services.
For example, for Google Drive file synchronization the webhook URL is `/connectors/google_drive/webhook`.
<PartialOnboarding />

View file

@ -6,46 +6,15 @@ slug: /quickstart
import Icon from "@site/src/components/icon/icon";
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import PartialExternalPreview from '@site/docs/_partial-external-preview.mdx';
<PartialExternalPreview />
Get started with OpenRAG by loading your knowledge, swapping out your language model, and then chatting with the OpenRAG API.
## Prerequisites
- [Install and start OpenRAG](/install)
- Create a [Langflow API key](https://docs.langflow.org/api-keys-and-authentication)
<details>
<summary>Create a Langflow API key</summary>
A Langflow API key is a user-specific token you can use with Langflow.
It is **only** used for sending requests to the Langflow server.
It does **not** access to OpenRAG.
To create a Langflow API key, do the following:
1. In Langflow, click your user icon, and then select **Settings**.
2. Click **Langflow API Keys**, and then click <Icon name="Plus" aria-hidden="true"/> **Add New**.
3. Name your key, and then click **Create API Key**.
4. Copy the API key and store it securely.
5. To use your Langflow API key in a request, set a `LANGFLOW_API_KEY` environment variable in your terminal, and then include an `x-api-key` header or query parameter with your request.
For example:
```bash
# Set variable
export LANGFLOW_API_KEY="sk..."
# Send request
curl --request POST \
--url "http://LANGFLOW_SERVER_ADDRESS/api/v1/run/FLOW_ID" \
--header "Content-Type: application/json" \
--header "x-api-key: $LANGFLOW_API_KEY" \
--data '{
"output_type": "chat",
"input_type": "chat",
"input_value": "Hello"
}'
```
</details>
## Find your way around
@ -96,12 +65,44 @@ You can more quickly access the **Language Model** and **Agent Instructions** fi
## Integrate OpenRAG into your application
To integrate OpenRAG into your application, use the [Langflow API](https://docs.langflow.org/api-reference-api-examples).
Make requests with Python, TypeScript, or any HTTP client to run one of OpenRAG's default flows and get a response, and then modify the flow further to improve results.
Make requests with Python, TypeScript, or any HTTP client to run one of OpenRAG's default flows and get a response, and then modify the flow further to improve results. Langflow provides code snippets to help you get started.
Langflow provides code snippets to help you get started with the Langflow API.
1. To navigate to the OpenRAG OpenSearch Agent flow, click <Icon name="Settings2" aria-hidden="true"/> **Settings**, and then click **Edit in Langflow** in the OpenRAG OpenSearch Agent flow.
2. Click **Share**, and then click **API access**.
1. Create a [Langflow API key](https://docs.langflow.org/api-keys-and-authentication).
<details>
<summary>Create a Langflow API key</summary>
A Langflow API key is a user-specific token you can use with Langflow.
It is **only** used for sending requests to the Langflow server.
It does **not** access to OpenRAG.
To create a Langflow API key, do the following:
1. In Langflow, click your user icon, and then select **Settings**.
2. Click **Langflow API Keys**, and then click <Icon name="Plus" aria-hidden="true"/> **Add New**.
3. Name your key, and then click **Create API Key**.
4. Copy the API key and store it securely.
5. To use your Langflow API key in a request, set a `LANGFLOW_API_KEY` environment variable in your terminal, and then include an `x-api-key` header or query parameter with your request.
For example:
```bash
# Set variable
export LANGFLOW_API_KEY="sk..."
# Send request
curl --request POST \
--url "http://LANGFLOW_SERVER_ADDRESS/api/v1/run/FLOW_ID" \
--header "Content-Type: application/json" \
--header "x-api-key: $LANGFLOW_API_KEY" \
--data '{
"output_type": "chat",
"input_type": "chat",
"input_value": "Hello"
}'
```
</details>
2. To navigate to the OpenRAG OpenSearch Agent flow, click <Icon name="Settings2" aria-hidden="true"/> **Settings**, and then click **Edit in Langflow** in the OpenRAG OpenSearch Agent flow.
3. Click **Share**, and then click **API access**.
The default code in the API access pane constructs a request with the Langflow server `url`, `headers`, and a `payload` of request data. The code snippets automatically include the `LANGFLOW_SERVER_ADDRESS` and `FLOW_ID` values for the flow. Replace these values if you're using the code for a different server or flow. The default Langflow server address is http://localhost:7860.
@ -186,7 +187,7 @@ Langflow provides code snippets to help you get started with the Langflow API.
</TabItem>
</Tabs>
3. Copy the snippet, paste it in a script file, and then run the script to send the request. If you are using the curl snippet, you can run the command directly in your terminal.
4. Copy the snippet, paste it in a script file, and then run the script to send the request. If you are using the curl snippet, you can run the command directly in your terminal.
If the request is successful, the response includes many details about the flow run, including the session ID, inputs, outputs, components, durations, and more.
The following is an example of a response from running the **Simple Agent** template flow:

View file

@ -3,13 +3,14 @@ title: Terminal User Interface (TUI) commands
slug: /get-started/tui
---
# OpenRAG TUI Guide
import PartialExternalPreview from '@site/docs/_partial-external-preview.mdx';
The OpenRAG Terminal User Interface (TUI) provides a streamlined way to set up, configure, and monitor your OpenRAG deployment directly from the terminal, on any operating system.
<PartialExternalPreview />
The OpenRAG Terminal User Interface (TUI) allows you to set up, configure, and monitor your OpenRAG deployment directly from the terminal, on any operating system.
![OpenRAG TUI Interface](@site/static/img/OpenRAG_TUI_2025-09-10T13_04_11_757637.svg)
The TUI offers an easier way to use OpenRAG without sacrificing control.
Instead of starting OpenRAG using Docker commands and manually editing values in the `.env` file, the TUI walks you through the setup. It prompts for variables where required, creates a `.env` file for you, and then starts OpenRAG.
Once OpenRAG is running, use the TUI to monitor your application, control your containers, and retrieve logs.
@ -17,7 +18,6 @@ Once OpenRAG is running, use the TUI to monitor your application, control your c
## Start the TUI
To start the TUI, run the following commands from the directory where you installed OpenRAG.
For more information, see [Install OpenRAG](/install).
```bash
uv sync

View file

@ -3,6 +3,10 @@ title: What is OpenRAG?
slug: /
---
import PartialExternalPreview from '@site/docs/_partial-external-preview.mdx';
<PartialExternalPreview />
OpenRAG is an open-source package for building agentic RAG systems.
It supports integration with a wide range of orchestration tools, vector databases, and LLM providers.
@ -14,6 +18,8 @@ OpenRAG connects and amplifies three popular, proven open-source projects into o
* [Docling](https://docling-project.github.io/docling/) - Docling simplifies document processing, parsing diverse formats — including advanced PDF understanding — and providing seamless integrations with the gen AI ecosystem.
OpenRAG builds on Langflow's familiar interface while adding OpenSearch for vector storage and Docling for simplified document parsing, with opinionated flows that serve as ready-to-use recipes for ingestion, retrieval, and generation from popular sources like OneDrive, Google Drive, and AWS. And don't fear: every part of the stack is swappable. Write your own custom components in Python, try different language models, and customize your flows to build an agentic RAG system that solves problems.
OpenRAG builds on Langflow's familiar interface while adding OpenSearch for vector storage and Docling for simplified document parsing, with opinionated flows that serve as ready-to-use recipes for ingestion, retrieval, and generation from popular sources like OneDrive, Google Drive, and AWS.
Ready to get started? Install OpenRAG and then run the Quickstart to create a powerful RAG pipeline.
What's more, every part of the stack is swappable. Write your own custom components in Python, try different language models, and customize your flows to build an agentic RAG system.
Ready to get started? [Install OpenRAG](/install) and then run the [Quickstart](/quickstart) to create a powerful RAG pipeline.

View file

@ -0,0 +1,162 @@
---
title: Environment variables
slug: /reference/configuration
---
import Icon from "@site/src/components/icon/icon";
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
OpenRAG recognizes [supported environment variables](#supported-environment-variables) from the following sources:
* [Environment variables](#supported-environment-variables) - Values set in the `.env` file.
* [Langflow runtime overrides](#langflow-runtime-overrides) - Langflow components may tweak environment variables at runtime.
* [Default or fallback values](#default-values-and-fallbacks) - These values are default or fallback values if OpenRAG doesn't find a value.
## Configure environment variables
Environment variables are set in a `.env` file in the root of your OpenRAG project directory.
For an example `.env` file, see [`.env.example` in the OpenRAG repository](https://github.com/langflow-ai/openrag/blob/main/.env.example).
The Docker Compose files are populated with values from your `.env`, so you don't need to edit the Docker Compose files manually.
Environment variables always take precedence over other variables.
### Set environment variables
To set environment variables, do the following.
1. Stop OpenRAG.
2. Set the values in the `.env` file:
```bash
LOG_LEVEL=DEBUG
LOG_FORMAT=json
SERVICE_NAME=openrag-dev
```
3. Start OpenRAG.
Updating provider API keys or provider endpoints in the `.env` file will not take effect after [Application onboarding](/install#application-onboarding). To change these values, you must:
1. Stop OpenRAG.
2. Remove the containers:
```
docker-compose down
```
3. Update the values in your `.env` file.
4. Start OpenRAG containers.
```
docker-compose up -d
```
5. Complete [Application onboarding](/install#application-onboarding) again.
## Supported environment variables
All OpenRAG configuration can be controlled through environment variables.
### AI provider settings
Configure which AI models and providers OpenRAG uses for language processing and embeddings.
For more information, see [Application onboarding](/install#application-onboarding).
| Variable | Default | Description |
|----------|---------|-------------|
| `EMBEDDING_MODEL` | `text-embedding-3-small` | Embedding model for vector search. |
| `LLM_MODEL` | `gpt-4o-mini` | Language model for the chat agent. |
| `MODEL_PROVIDER` | `openai` | Model provider, such as OpenAI or IBM watsonx.ai. |
| `OPENAI_API_KEY` | - | Your OpenAI API key. Required. |
| `PROVIDER_API_KEY` | - | API key for the model provider. |
| `PROVIDER_ENDPOINT` | - | Custom provider endpoint. Only used for IBM or Ollama providers. |
| `PROVIDER_PROJECT_ID` | - | Project ID for providers. Only required for the IBM watsonx.ai provider. |
### Document processing
Control how OpenRAG processes and ingests documents into your knowledge base.
For more information, see [Ingestion](/ingestion).
| Variable | Default | Description |
|----------|---------|-------------|
| `CHUNK_OVERLAP` | `200` | Overlap between chunks. |
| `CHUNK_SIZE` | `1000` | Text chunk size for document processing. |
| `DISABLE_INGEST_WITH_LANGFLOW` | `false` | Disable Langflow ingestion pipeline. |
| `DOCLING_OCR_ENGINE` | - | OCR engine for document processing. |
| `OCR_ENABLED` | `false` | Enable OCR for image processing. |
| `OPENRAG_DOCUMENTS_PATHS` | `./documents` | Document paths for ingestion. |
| `PICTURE_DESCRIPTIONS_ENABLED` | `false` | Enable picture descriptions. |
### Langflow settings
Configure Langflow authentication.
| Variable | Default | Description |
|----------|---------|-------------|
| `LANGFLOW_AUTO_LOGIN` | `False` | Enable auto-login for Langflow. |
| `LANGFLOW_CHAT_FLOW_ID` | pre-filled | This value is pre-filled. The default value is found in [.env.example](https://github.com/langflow-ai/openrag/blob/main/.env.example). |
| `LANGFLOW_ENABLE_SUPERUSER_CLI` | `False` | Enable superuser CLI. |
| `LANGFLOW_INGEST_FLOW_ID` | pre-filled | This value is pre-filled. The default value is found in [.env.example](https://github.com/langflow-ai/openrag/blob/main/.env.example). |
| `LANGFLOW_KEY` | auto-generated | Explicit Langflow API key. |
| `LANGFLOW_NEW_USER_IS_ACTIVE` | `False` | New users are active by default. |
| `LANGFLOW_PUBLIC_URL` | `http://localhost:7860` | Public URL for Langflow. |
| `LANGFLOW_SECRET_KEY` | - | Secret key for Langflow internal operations. |
| `LANGFLOW_SUPERUSER` | - | Langflow admin username. Required. |
| `LANGFLOW_SUPERUSER_PASSWORD` | - | Langflow admin password. Required. |
| `LANGFLOW_URL` | `http://localhost:7860` | Langflow URL. |
| `NUDGES_FLOW_ID` | pre-filled | This value is pre-filled. The default value is found in [.env.example](https://github.com/langflow-ai/openrag/blob/main/.env.example). |
| `SYSTEM_PROMPT` | "You are a helpful AI assistant with access to a knowledge base. Answer questions based on the provided context." | System prompt for the Langflow agent. |
### OAuth provider settings
Configure OAuth providers and external service integrations.
| Variable | Default | Description |
|----------|---------|-------------|
| `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` | - | AWS integrations. |
| `GOOGLE_OAUTH_CLIENT_ID` / `GOOGLE_OAUTH_CLIENT_SECRET` | - | Google OAuth authentication. |
| `MICROSOFT_GRAPH_OAUTH_CLIENT_ID` / `MICROSOFT_GRAPH_OAUTH_CLIENT_SECRET` | - | Microsoft OAuth. |
| `WEBHOOK_BASE_URL` | - | Base URL for webhook endpoints. |
### OpenSearch settings
Configure OpenSearch database authentication.
| Variable | Default | Description |
|----------|---------|-------------|
| `OPENSEARCH_HOST` | `localhost` | OpenSearch host. |
| `OPENSEARCH_PASSWORD` | - | Password for OpenSearch admin user. Required. |
| `OPENSEARCH_PORT` | `9200` | OpenSearch port. |
| `OPENSEARCH_USERNAME` | `admin` | OpenSearch username. |
### System settings
Configure general system components, session management, and logging.
| Variable | Default | Description |
|----------|---------|-------------|
| `LANGFLOW_KEY_RETRIES` | `15` | Number of retries for Langflow key generation. |
| `LANGFLOW_KEY_RETRY_DELAY` | `2.0` | Delay between retries in seconds. |
| `LOG_FORMAT` | - | Log format (set to "json" for JSON output). |
| `LOG_LEVEL` | `INFO` | Logging level (DEBUG, INFO, WARNING, ERROR). |
| `MAX_WORKERS` | - | Maximum number of workers for document processing. |
| `SERVICE_NAME` | `openrag` | Service name for logging. |
| `SESSION_SECRET` | auto-generated | Session management. |
## Langflow runtime overrides
Langflow runtime overrides allow you to modify component settings at runtime without changing the base configuration.
Runtime overrides are implemented through **tweaks** - parameter modifications that are passed to specific Langflow components during flow execution.
For more information on tweaks, see [Input schema (tweaks)](https://docs.langflow.org/concepts-publish#input-schema).
## Default values and fallbacks
When no environment variables or configuration file values are provided, OpenRAG uses default values.
These values can be found in the code base at the following locations.
### OpenRAG configuration defaults
These values are defined in [`config_manager.py` in the OpenRAG repository](https://github.com/langflow-ai/openrag/blob/main/src/config/config_manager.py).
### System configuration defaults
These fallback values are defined in [`settings.py` in the OpenRAG repository](https://github.com/langflow-ai/openrag/blob/main/src/config/settings.py).

View file

@ -5,17 +5,20 @@ slug: /support/troubleshoot
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import PartialExternalPreview from '@site/docs/_partial-external-preview.mdx';
<PartialExternalPreview />
This page provides troubleshooting advice for issues you might encounter when using OpenRAG or contributing to OpenRAG.
## OpenSearch fails to start
Check that `OPENSEARCH_PASSWORD` is set and meets requirements.
Check that `OPENSEARCH_PASSWORD` set in [Environment variables](/reference/configuration) meets requirements.
The password must contain at least 8 characters, and must contain at least one uppercase letter, one lowercase letter, one digit, and one special character that is strong.
## Langflow connection issues
Verify the `LANGFLOW_SUPERUSER` credentials are correct.
Verify the `LANGFLOW_SUPERUSER` credentials set in [Environment variables](/reference/configuration) are correct.
## Memory errors
@ -48,60 +51,61 @@ To reset your local containers and pull new images, do the following:
1. Stop your containers and completely remove them.
<Tabs groupId="Container software">
<TabItem value="Docker" label="Docker" default>
```bash
# Stop all running containers
docker stop $(docker ps -q)
<Tabs groupId="Container software">
<TabItem value="Podman" label="Podman">
# Remove all containers (including stopped ones)
docker rm --force $(docker ps -aq)
```bash
# Stop all running containers
podman stop --all
# Remove all containers (including stopped ones)
podman rm --all --force
# Remove all images
podman rmi --all --force
# Remove all volumes
podman volume prune --force
# Remove all networks (except default)
podman network prune --force
# Clean up any leftover data
podman system prune --all --force --volumes
```
# Remove all images
docker rmi --force $(docker images -q)
</TabItem>
<TabItem value="Docker" label="Docker" default>
# Remove all volumes
docker volume prune --force
```bash
# Stop all running containers
docker stop $(docker ps -q)
# Remove all containers (including stopped ones)
docker rm --force $(docker ps -aq)
# Remove all images
docker rmi --force $(docker images -q)
# Remove all volumes
docker volume prune --force
# Remove all networks (except default)
docker network prune --force
# Clean up any leftover data
docker system prune --all --force --volumes
```
# Remove all networks (except default)
docker network prune --force
# Clean up any leftover data
docker system prune --all --force --volumes
```
</TabItem>
<TabItem value="Podman" label="Podman">
```bash
# Stop all running containers
podman stop --all
# Remove all containers (including stopped ones)
podman rm --all --force
# Remove all images
podman rmi --all --force
# Remove all volumes
podman volume prune --force
# Remove all networks (except default)
podman network prune --force
# Clean up any leftover data
podman system prune --all --force --volumes
```
</TabItem>
</Tabs>
</TabItem>
</Tabs>
2. Restart OpenRAG and upgrade to get the latest images for your containers.
```bash
uv sync
uv run openrag
```
3. In the OpenRAG TUI, click **Status**, and then click **Upgrade**.
When the **Close** button is active, the upgrade is complete.
Close the window and open the OpenRAG appplication.
Close the window and open the OpenRAG appplication.

View file

@ -20,10 +20,10 @@ const config = {
},
// Set the production url of your site here
url: 'https://langflow-ai.github.io',
url: 'https://docs.openr.ag',
// Set the /<baseUrl>/ pathname under which your site is served
// For GitHub pages deployment, it is often '/<projectName>/'
baseUrl: process.env.BASE_URL ? process.env.BASE_URL : '/openrag/',
baseUrl: process.env.BASE_URL ? process.env.BASE_URL : '/',
// GitHub pages deployment config.
// If you aren't using GitHub pages, you don't need these.
@ -86,39 +86,28 @@ const config = {
},
items: [
{
href: 'https://github.com/openrag/openrag',
label: 'GitHub',
position: 'right',
position: "right",
href: "https://github.com/langflow-ai/openrag",
className: "header-github-link",
target: "_blank",
rel: null,
'aria-label': 'GitHub repository',
},
],
},
footer: {
style: 'dark',
links: [
{
title: 'Documentation',
title: null,
items: [
{
label: 'Getting Started',
to: '/',
},
],
},
{
title: 'Community',
items: [
{
label: 'GitHub',
href: 'https://github.com/openrag/openrag',
},
{
label: 'Discord',
href: 'https://discord.gg/openrag',
html: `<div class="footer-links">
<span>© ${new Date().getFullYear()} OpenRAG</span>
</div>`,
},
],
},
],
copyright: `Copyright © ${new Date().getFullYear()} OpenRAG. Built with Docusaurus.`,
},
prism: {
theme: prismThemes.github,

View file

@ -28,22 +28,22 @@ const sidebars = {
{
type: "doc",
id: "get-started/install",
label: "Installation"
label: "Install OpenRAG"
},
{
type: "doc",
id: "get-started/docker",
label: "Deploy with Docker"
},
{
type: "doc",
id: "get-started/quickstart",
label: "Quickstart"
},
{
type: "doc",
id: "get-started/docker",
label: "Docker Deployment"
},
{
type: "doc",
id: "get-started/tui",
label: "Terminal Interface (TUI)"
label: "Terminal User Interface (TUI)"
},
],
},
@ -70,12 +70,12 @@ const sidebars = {
},
{
type: "category",
label: "Configuration",
label: "Reference",
items: [
{
type: "doc",
id: "configure/configuration",
label: "Environment Variables"
id: "reference/configuration",
label: "Environment variables"
},
],
},
@ -93,4 +93,4 @@ const sidebars = {
],
};
export default sidebars;
export default sidebars;

View file

@ -28,3 +28,117 @@
--ifm-color-primary-lightest: #4fddbf;
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
}
/* Tabs Styling */
.tabs-container {
border: 1px solid var(--ifm-color-emphasis-300);
border-radius: var(--ifm-global-radius);
padding: 1rem;
margin-bottom: 1rem;
}
.tabs {
margin-bottom: 1rem;
}
.tabs__item {
border: none;
border-bottom: 1px solid var(--ifm-color-emphasis-200);
margin-right: 0rem;
padding-bottom: 0.5rem;
border-radius: 0;
}
.tabs__item:hover {
background-color: var(--ifm-hover-overlay);
}
.tabs__item--active {
border-bottom-color: var(--ifm-tabs-color-active);
}
/* GitHub Icon Button */
.header-github-link:hover {
opacity: 0.6;
}
.header-github-link:before {
content: "";
width: 24px;
height: 24px;
display: flex;
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z'/%3E%3C/svg%3E") no-repeat;
}
[data-theme="dark"] .header-github-link:before {
content: "";
width: 24px;
height: 24px;
display: flex;
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z'/%3E%3C/svg%3E") no-repeat;
}
/* Slim Footer */
.footer {
padding: 1rem 0 0;
background-color: var(--ifm-background-color);
border-top: 1px solid var(--ifm-color-emphasis-200);
}
[data-theme="light"] .footer {
border-top: 1px solid var(--ifm-color-emphasis-300);
}
.footer-content {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 var(--ifm-navbar-padding-horizontal);
max-width: var(--ifm-container-width);
margin: 0 auto;
font-size: 12px;
}
.footer__copyright {
color: var(--ifm-toc-link-color);
}
.footer-links {
display: flex;
gap: 0.5rem;
font-size: 13px;
}
.footer-links a {
color: var(--ifm-toc-link-color);
text-decoration: none;
}
.footer-links a:hover {
text-decoration: underline;
}
.footer .container {
padding: 0 1.25rem;
display: flex;
justify-content: left;
align-items: center;
flex-direction: row;
max-width: 100%;
}
.footer__title {
margin-bottom: 0;
}
/* Inline Icons */
.markdown svg,
.markdown img[src*="icon"],
.markdown .icon {
width: 16px !important;
height: 16px !important;
stroke-width: 2.5 !important;
display: inline-block;
vertical-align: middle;
}

1
docs/static/CNAME vendored Normal file
View file

@ -0,0 +1 @@
docs.openr.ag

View file

@ -133,7 +133,7 @@ export function FilterIconPopover({
type="button"
onClick={() => onColorChange(c)}
className={cn(
"flex items-center justify-center h-6 w-6 rounded-sm transition-colors text-primary",
"flex items-center justify-center h-6 w-6 rounded-sm transition-colors text-white",
colorSwatchClasses[c]
)}
aria-label={c}

View file

@ -66,7 +66,7 @@ export function KnowledgeFilterList({
return (
<>
<div className="flex flex-col gap-1 px-3 !mb-12 mt-0 h-full overflow-y-auto">
<div className="flex flex-col gap-2 px-3 !mb-12 mt-0 h-full overflow-y-auto">
<div className="flex items-center w-full justify-between pl-3">
<div className="text-sm font-medium text-muted-foreground">
Knowledge Filters
@ -76,7 +76,7 @@ export function KnowledgeFilterList({
size="sm"
onClick={handleCreateNew}
title="Create New Filter"
className="h-8 px-3 text-muted-foreground"
className="!h-8 w-8 px-0 text-muted-foreground"
>
<Plus className="h-3 w-3" />
</Button>
@ -106,12 +106,14 @@ export function KnowledgeFilterList({
<div className="flex flex-col gap-1 flex-1 min-w-0">
<div className="flex items-center gap-2">
{(() => {
const parsed = parseQueryData(filter.query_data) as ParsedQueryData;
const parsed = parseQueryData(
filter.query_data
) as ParsedQueryData;
const Icon = iconKeyToComponent(parsed.icon);
return (
<div
className={cn(
"flex items-center justify-center w-5 h-5 rounded transition-colors",
"flex items-center justify-center w-5 h-5 rounded flex-shrink-0 transition-colors",
filterAccentClasses[parsed.color],
parsed.color === "zinc" &&
"group-hover:bg-background group-[.active]:bg-background"

View file

@ -1,9 +1,15 @@
"use client";
import { useState, useEffect } from "react";
import { X, Save, RefreshCw } from "lucide-react";
import { useState, useEffect, useRef } from "react";
import { X, RefreshCw } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Card,
CardContent,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
@ -61,6 +67,8 @@ export function KnowledgeFilterPanel() {
const [isSaving, setIsSaving] = useState(false);
const [color, setColor] = useState<FilterColor>("zinc");
const [iconKey, setIconKey] = useState<IconKey>("filter");
const [nameError, setNameError] = useState<string | null>(null);
const nameInputRef = useRef<HTMLInputElement>(null);
// Filter configuration states (mirror search page exactly)
const [query, setQuery] = useState("");
@ -146,27 +154,12 @@ export function KnowledgeFilterPanel() {
// Don't render if panel is closed or we don't have any data
if (!isPanelOpen || !parsedFilterData) return null;
const selectAllFilters = () => {
// Use wildcards instead of listing all specific items
setSelectedFilters({
data_sources: ["*"],
document_types: ["*"],
owners: ["*"],
connector_types: ["*"],
});
};
const clearAllFilters = () => {
setSelectedFilters({
data_sources: [],
document_types: [],
owners: [],
connector_types: [],
});
};
const handleSaveConfiguration = async () => {
if (!name.trim()) return;
if (!name.trim()) {
setNameError("Name is required");
nameInputRef.current?.focus();
return;
}
const filterData = {
query,
filters: selectedFilters,
@ -238,8 +231,8 @@ export function KnowledgeFilterPanel() {
};
return (
<div className="fixed right-0 top-14 bottom-0 w-80 bg-background border-l border-border/40 z-40 overflow-y-auto">
<Card className="h-full rounded-none border-0 shadow-lg">
<div className="fixed right-0 top-14 bottom-0 w-80 bg-background border-l z-40 overflow-y-auto">
<Card className="h-full rounded-none border-0 shadow-lg flex flex-col">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-lg flex items-center gap-2">
@ -248,7 +241,10 @@ export function KnowledgeFilterPanel() {
<Button
variant="ghost"
size="sm"
onClick={closePanelOnly}
onClick={() => {
setSelectedFilter(null);
closePanelOnly();
}}
className="h-8 w-8 p-0"
>
<X className="h-4 w-4" />
@ -260,7 +256,10 @@ export function KnowledgeFilterPanel() {
{/* Filter Name and Description */}
<div className="space-y-3">
<div className="space-y-2">
<Label htmlFor="filter-name">Filter name</Label>
<Label htmlFor="filter-name" className="gap-1">
Filter name
<span className="text-destructive">*</span>
</Label>
<div className="flex items-center gap-2">
<FilterIconPopover
color={color}
@ -271,8 +270,17 @@ export function KnowledgeFilterPanel() {
<Input
id="filter-name"
value={name}
onChange={(e) => setName(e.target.value)}
onChange={(e) => {
const v = e.target.value;
setName(v);
if (nameError && v.trim()) {
setNameError(null);
}
}}
required
placeholder="Filter name"
ref={nameInputRef}
aria-invalid={!!nameError}
/>
</div>
</div>
@ -282,13 +290,19 @@ export function KnowledgeFilterPanel() {
{formatDate(selectedFilter.created_at)}
</div>
)}
{createMode && (
<div className="space-y-2 text-xs text-right text-muted-foreground">
<span className="text-placeholder-foreground">Created</span>{" "}
{formatDate(new Date().toISOString())}
</div>
)}
<div className="space-y-2">
<Label htmlFor="filter-description">Description</Label>
<Textarea
id="filter-description"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Optional description"
placeholder="Provide a brief description of your knowledge filter..."
rows={3}
/>
</div>
@ -301,17 +315,17 @@ export function KnowledgeFilterPanel() {
</Label>
<Textarea
id="search-query"
placeholder="e.g., 'financial reports from Q4'"
placeholder="Enter your search query..."
value={query}
className="font-mono placeholder:font-mono"
onChange={(e) => setQuery(e.target.value)}
rows={3}
rows={2}
/>
</div>
{/* Filter Dropdowns */}
<div className="space-y-4">
<div className="space-y-2">
<Label className="text-sm font-medium">Data Sources</Label>
<MultiSelect
options={(availableFacets.data_sources || []).map((bucket) => ({
value: bucket.key,
@ -322,13 +336,12 @@ export function KnowledgeFilterPanel() {
onValueChange={(values) =>
handleFilterChange("data_sources", values)
}
placeholder="Select data sources..."
allOptionLabel="All Data Sources"
placeholder="Select sources..."
allOptionLabel="All sources"
/>
</div>
<div className="space-y-2">
<Label className="text-sm font-medium">Document Types</Label>
<MultiSelect
options={(availableFacets.document_types || []).map(
(bucket) => ({
@ -341,13 +354,12 @@ export function KnowledgeFilterPanel() {
onValueChange={(values) =>
handleFilterChange("document_types", values)
}
placeholder="Select document types..."
allOptionLabel="All Document Types"
placeholder="Select types..."
allOptionLabel="All types"
/>
</div>
<div className="space-y-2">
<Label className="text-sm font-medium">Owners</Label>
<MultiSelect
options={(availableFacets.owners || []).map((bucket) => ({
value: bucket.key,
@ -362,7 +374,6 @@ export function KnowledgeFilterPanel() {
</div>
<div className="space-y-2">
<Label className="text-sm font-medium">Sources</Label>
<MultiSelect
options={(availableFacets.connector_types || []).map(
(bucket) => ({
@ -375,33 +386,13 @@ export function KnowledgeFilterPanel() {
onValueChange={(values) =>
handleFilterChange("connector_types", values)
}
placeholder="Select sources..."
allOptionLabel="All Sources"
placeholder="Select connectors..."
allOptionLabel="All connectors"
/>
</div>
{/* All/None buttons */}
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={selectAllFilters}
className="h-auto px-3 py-1.5 text-xs text-muted-foreground hover:text-foreground hover:bg-muted/50 border-border/50"
>
All
</Button>
<Button
variant="outline"
size="sm"
onClick={clearAllFilters}
className="h-auto px-3 py-1.5 text-xs text-muted-foreground hover:text-foreground hover:bg-muted/50 border-border/50"
>
None
</Button>
</div>
{/* Result Limit Control - exactly like search page */}
<div className="space-y-4 pt-4 border-t border-border/50">
<div className="space-y-4">
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-sm font-medium text-nowrap">
@ -462,39 +453,45 @@ export function KnowledgeFilterPanel() {
/>
</div>
</div>
{/* Save Configuration Button */}
<div className="flex flex-col gap-3 pt-4 border-t border-border/50">
<Button
onClick={handleSaveConfiguration}
disabled={isSaving}
className="w-full"
size="sm"
>
{isSaving ? (
<>
<RefreshCw className="h-3 w-3 mr-2 animate-spin" />
Saving...
</>
) : (
<>
<Save className="h-3 w-3 mr-2" />
Save Configuration
</>
)}
</Button>
{!createMode && (
<Button
variant="destructive"
className="w-full"
onClick={handleDeleteFilter}
>
Delete Filter
</Button>
)}
</div>
</div>
</CardContent>
<CardFooter className="mt-auto align-bottom justify-end gap-2">
{/* Save Configuration Button */}
{createMode && (
<Button
onClick={closePanelOnly}
disabled={isSaving}
variant="outline"
size="sm"
>
Cancel
</Button>
)}
{!createMode && (
<Button
variant="destructive"
size="sm"
onClick={handleDeleteFilter}
disabled={isSaving}
>
Delete Filter
</Button>
)}
<Button
onClick={handleSaveConfiguration}
disabled={isSaving}
size="sm"
className="relative"
>
{isSaving && (
<>
<RefreshCw className="h-3 w-3 animate-spin" />
Saving...
</>
)}
{!isSaving && (createMode ? "Create Filter" : "Update Filter")}
</Button>
</CardFooter>
</Card>
</div>
);

View file

@ -37,7 +37,7 @@ export function LabelWrapper({
>
<Label
htmlFor={id}
className="!text-mmd font-medium flex items-center gap-1.5"
className={cn("font-medium flex items-center gap-1.5", description ? "!text-sm" : "!text-mmd")}
>
{label}
{required && <span className="text-red-500">*</span>}

View file

@ -8,11 +8,24 @@ export default function IBMLogo(props: React.SVGProps<SVGSVGElement>) {
fill="none"
{...props}
>
<title>IBM Logo</title>
<path
d="M15.696 10.9901C15.7213 10.9901 15.7356 10.979 15.7356 10.9552V10.9313C15.7356 10.9076 15.7213 10.8964 15.696 10.8964H15.6359V10.9901H15.696ZM15.6359 11.1649H15.5552V10.8329H15.7055C15.7799 10.8329 15.8179 10.8773 15.8179 10.9378C15.8179 10.9901 15.7942 11.0235 15.7577 11.0378L15.8321 11.1649H15.7436L15.6818 11.0504H15.6359V11.1649ZM15.9255 11.0171V10.9759C15.9255 10.8424 15.821 10.7376 15.6833 10.7376C15.5456 10.7376 15.4412 10.8424 15.4412 10.9759V11.0171C15.4412 11.1505 15.5456 11.2554 15.6833 11.2554C15.821 11.2554 15.9255 11.1505 15.9255 11.0171ZM15.3668 10.9964C15.3668 10.8107 15.5077 10.6693 15.6833 10.6693C15.859 10.6693 16 10.8107 16 10.9964C16 11.1823 15.859 11.3237 15.6833 11.3237C15.5077 11.3237 15.3668 11.1823 15.3668 10.9964ZM10.8069 5.74885L10.6627 5.33301H8.28904V5.74885H10.8069ZM11.0821 6.54285L10.9379 6.12691H8.28904V6.54285H11.0821ZM12.8481 11.3067H14.9203V10.8908H12.8481V11.3067ZM12.8481 10.5126H14.9203V10.0968H12.8481V10.5126ZM12.8481 9.71873H14.0914V9.3028H12.8481V9.71873ZM12.8481 8.92474H14.0914V8.50889H12.8481V8.92474ZM12.8481 8.13084H14.0914V7.7149H11.7212L11.6047 8.05102L11.4882 7.7149H9.11794V8.13084H10.3613V7.74863L10.4951 8.13084H12.7143L12.8481 7.74863V8.13084ZM14.0914 6.921H11.9964L11.8522 7.33675H14.0914V6.921ZM9.11794 8.92474H10.3613V8.50889H9.11794V8.92474ZM9.11794 9.71873H10.3613V9.3028H9.11794V9.71873ZM8.28904 10.5126H10.3613V10.0968H8.28904V10.5126ZM8.28904 11.3067H10.3613V10.8908H8.28904V11.3067ZM12.5466 5.33301L12.4025 5.74885H14.9203V5.33301H12.5466ZM12.1273 6.54285H14.9203V6.12691H12.2714L12.1273 6.54285ZM9.11794 7.33675H11.3572L11.213 6.921H9.11794V7.33675ZM10.7727 8.92474H12.4366L12.5821 8.50889H10.6272L10.7727 8.92474ZM11.0505 9.71873H12.1588L12.3042 9.3028H10.9051L11.0505 9.71873ZM11.3283 10.5126H11.881L12.0265 10.0969H11.1828L11.3283 10.5126ZM11.604 11.3067L11.7487 10.8908H11.4606L11.604 11.3067ZM3.31561 11.3026L6.36754 11.3067C6.78195 11.3067 7.15365 11.1491 7.43506 10.8908H3.31561V11.3026ZM6.55592 9.3028V9.71873H7.94994C7.94994 9.57477 7.93029 9.43551 7.89456 9.3028H6.55592ZM4.14452 9.71873H5.38783V9.3028H4.14452V9.71873ZM6.55592 7.33675H7.89456C7.93029 7.20422 7.94994 7.06486 7.94994 6.921H6.55592V7.33675ZM4.14452 7.33675H5.38783V6.9209H4.14452V7.33675ZM6.36754 5.33301H3.31561V5.74885H7.43506C7.15365 5.49061 6.77892 5.33301 6.36754 5.33301ZM7.73778 6.12691H3.31561V6.54285H7.90448C7.86839 6.39502 7.81172 6.25539 7.73778 6.12691ZM4.14452 7.7149V8.13084H7.39152C7.5292 8.01333 7.64621 7.87268 7.73732 7.7149H4.14452ZM7.39152 8.50889H4.14452V8.92474H7.73732C7.64621 8.76695 7.5292 8.62631 7.39152 8.50889ZM3.31561 10.5126H7.73778C7.81172 10.3843 7.86839 10.2447 7.90448 10.0969H3.31561V10.5126ZM0 5.74885H2.90121V5.33301H0V5.74885ZM0 6.54285H2.90121V6.12691H0V6.54285ZM0.828996 7.33684H2.0723V6.921H0.828996V7.33684ZM0.828996 8.13084H2.0723V7.7149H0.828996V8.13084ZM0.828996 8.92474H2.0723V8.50889H0.828996V8.92474ZM0.828996 9.71873H2.0723V9.3028H0.828996V9.71873ZM0 10.5126H2.90121V10.0968H0V10.5126ZM0 11.3067H2.90121V10.8908H0V11.3067Z"
fill="currentColor"
/>
<title>IBM watsonx.ai Logo</title>
<g clip-path="url(#clip0_2620_2081)">
<path
d="M13 12.0007C12.4477 12.0007 12 12.4484 12 13.0007C12 13.0389 12.0071 13.0751 12.0112 13.1122C10.8708 14.0103 9.47165 14.5007 8 14.5007C5.86915 14.5007 4 12.5146 4 10.2507C4 7.90722 5.9065 6.00072 8.25 6.00072H8.5V5.00072H8.25C5.3552 5.00072 3 7.35592 3 10.2507C3 11.1927 3.2652 12.0955 3.71855 12.879C2.3619 11.6868 1.5 9.94447 1.5 8.00072C1.5 6.94312 1.74585 5.93432 2.23095 5.00292L1.34375 4.54102C0.79175 5.60157 0.5 6.79787 0.5 8.00072C0.5 12.1362 3.8645 15.5007 8 15.5007C9.6872 15.5007 11.2909 14.9411 12.6024 13.9176C12.7244 13.9706 12.8586 14.0007 13 14.0007C13.5523 14.0007 14 13.553 14 13.0007C14 12.4484 13.5523 12.0007 13 12.0007Z"
fill="currentColor"
/>
<path d="M6.5 11V10H5.5V11H6.5Z" fill="currentColor" />
<path d="M10.5 6V5H9.5V6H10.5Z" fill="currentColor" />
<path
d="M8 0.5C6.3128 0.5 4.7091 1.05965 3.3976 2.0831C3.2756 2.0301 3.14145 2 3 2C2.4477 2 2 2.4477 2 3C2 3.5523 2.4477 4 3 4C3.5523 4 4 3.5523 4 3C4 2.9618 3.9929 2.9256 3.98875 2.88855C5.12915 1.9904 6.52835 1.5 8 1.5C10.1308 1.5 12 3.4861 12 5.75C12 8.0935 10.0935 10 7.75 10H7.5V11H7.75C10.6448 11 13 8.6448 13 5.75C13 4.80735 12.7339 3.90415 12.28 3.12035C13.6375 4.3125 14.5 6.05555 14.5 8C14.5 9.0576 14.2541 10.0664 13.769 10.9978L14.6562 11.4597C15.2083 10.3991 15.5 9.20285 15.5 8C15.5 3.8645 12.1355 0.5 8 0.5Z"
fill="currentColor"
/>
</g>
<defs>
<clipPath id="clip0_2620_2081">
<rect width="16" height="16" fill="white" />
</clipPath>
</defs>
</svg>
);
}

View file

@ -3,7 +3,7 @@ import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none disabled:select-none [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
@ -11,10 +11,10 @@ const buttonVariants = cva(
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input hover:bg-muted hover:text-accent-foreground disabled:bg-muted disabled:!border-none",
"border border-border hover:bg-muted hover:text-accent-foreground disabled:bg-muted disabled:!border-none",
primary:
"border bg-background text-secondary-foreground hover:bg-muted hover:shadow-sm",
warning: "bg-warning text-secondary hover:bg-warning/90",
warning: "bg-warning text-warning-foreground hover:bg-warning/90",
secondary:
"border border-muted bg-muted text-secondary-foreground hover:bg-secondary-foreground/5",
ghost:

View file

@ -8,8 +8,8 @@ const Card = React.forwardRef<
<div
ref={ref}
className={cn(
"rounded-lg border border-border bg-card text-card-foreground shadow-sm",
className,
"rounded-xl border border-border bg-card text-card-foreground shadow-sm",
className
)}
{...props}
/>
@ -33,8 +33,8 @@ const CardTitle = React.forwardRef<
<h3
ref={ref}
className={cn(
"text-base font-semibold leading-tight tracking-tight",
className,
"text-base font-semibold leading-tight tracking-tight text-[14px]",
className
)}
{...props}
/>

View file

@ -1,39 +1,39 @@
"use client"
"use client";
import * as React from "react"
import { ChevronDown, Check } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import * as React from "react";
import { ChevronDown, Check } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
} from "@/components/ui/command"
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
import { ScrollArea } from "@/components/ui/scroll-area"
} from "@/components/ui/popover";
import { ScrollArea } from "@/components/ui/scroll-area";
interface Option {
value: string
label: string
count?: number
value: string;
label: string;
count?: number;
}
interface MultiSelectProps {
options: Option[]
value: string[]
onValueChange: (value: string[]) => void
placeholder?: string
className?: string
maxSelection?: number
searchPlaceholder?: string
showAllOption?: boolean
allOptionLabel?: string
options: Option[];
value: string[];
onValueChange: (value: string[]) => void;
placeholder?: string;
className?: string;
maxSelection?: number;
searchPlaceholder?: string;
showAllOption?: boolean;
allOptionLabel?: string;
}
export function MultiSelect({
@ -43,60 +43,61 @@ export function MultiSelect({
placeholder = "Select items...",
className,
maxSelection,
searchPlaceholder = "Search...",
searchPlaceholder = "Search options...",
showAllOption = true,
allOptionLabel = "All"
allOptionLabel = "All",
}: MultiSelectProps) {
const [open, setOpen] = React.useState(false)
const [searchValue, setSearchValue] = React.useState("")
const [open, setOpen] = React.useState(false);
const [searchValue, setSearchValue] = React.useState("");
const isAllSelected = value.includes("*")
const filteredOptions = options.filter(option =>
const isAllSelected = value.includes("*");
const filteredOptions = options.filter((option) =>
option.label.toLowerCase().includes(searchValue.toLowerCase())
)
);
const handleSelect = (optionValue: string) => {
if (optionValue === "*") {
// Toggle "All" selection
if (isAllSelected) {
onValueChange([])
onValueChange([]);
} else {
onValueChange(["*"])
onValueChange(["*"]);
}
} else {
let newValue: string[]
let newValue: string[];
if (value.includes(optionValue)) {
// Remove the item
newValue = value.filter(v => v !== optionValue && v !== "*")
newValue = value.filter((v) => v !== optionValue && v !== "*");
} else {
// Add the item and remove "All" if present
newValue = [...value.filter(v => v !== "*"), optionValue]
newValue = [...value.filter((v) => v !== "*"), optionValue];
// Check max selection limit
if (maxSelection && newValue.length > maxSelection) {
return
return;
}
}
onValueChange(newValue)
onValueChange(newValue);
}
}
};
const getDisplayText = () => {
if (isAllSelected) {
return allOptionLabel
return allOptionLabel;
}
if (value.length === 0) {
return placeholder
return placeholder;
}
// Extract the noun from placeholder (e.g., "Select data sources..." -> "data sources")
const noun = placeholder.toLowerCase().replace('select ', '').replace('...', '')
return `${value.length} ${noun}`
}
const noun = placeholder
.toLowerCase()
.replace("select ", "")
.replace("...", "");
return `${value.length} ${noun}`;
};
return (
<Popover open={open} onOpenChange={setOpen}>
@ -106,17 +107,15 @@ export function MultiSelect({
role="combobox"
aria-expanded={open}
className={cn(
"w-full justify-between min-h-[40px] h-auto text-left",
"w-full justify-between h-8 py-0 text-left",
className
)}
>
<span className="text-foreground text-sm">
{getDisplayText()}
</span>
<span className="text-foreground text-sm">{getDisplayText()}</span>
<ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0" align="start">
<PopoverContent className="p-0" align="start">
<Command>
<CommandInput
placeholder={searchPlaceholder}
@ -132,16 +131,13 @@ export function MultiSelect({
onSelect={() => handleSelect("*")}
className="cursor-pointer"
>
<span className="flex-1">{allOptionLabel}</span>
<Check
className={cn(
"mr-2 h-4 w-4",
isAllSelected ? "opacity-100" : "opacity-0"
)}
/>
<span className="flex-1">{allOptionLabel}</span>
<span className="text-xs text-blue-500 bg-blue-500/10 px-1.5 py-0.5 rounded ml-2">
*
</span>
</CommandItem>
)}
{filteredOptions.map((option) => (
@ -149,20 +145,19 @@ export function MultiSelect({
key={option.value}
onSelect={() => handleSelect(option.value)}
className="cursor-pointer"
disabled={isAllSelected}
>
<Check
className={cn(
"mr-2 h-4 w-4",
value.includes(option.value) ? "opacity-100" : "opacity-0"
)}
/>
<span className="flex-1">{option.label}</span>
{option.count !== undefined && (
<span className="text-xs text-muted-foreground bg-muted/50 px-1.5 py-0.5 rounded ml-2">
{option.count}
</span>
)}
<Check
className={cn(
"mr-2 h-4 w-4",
value.includes(option.value) ? "opacity-100" : "opacity-0"
)}
/>
</CommandItem>
))}
</ScrollArea>
@ -170,5 +165,5 @@ export function MultiSelect({
</Command>
</PopoverContent>
</Popover>
)
}
);
}

View file

@ -1,31 +1,33 @@
"use client"
"use client";
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import * as PopoverPrimitive from "@radix-ui/react-popover";
import * as React from "react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const Popover = PopoverPrimitive.Root
const Popover = PopoverPrimitive.Root;
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverTrigger = PopoverPrimitive.Trigger;
const PopoverAnchor = PopoverPrimitive.Anchor;
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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",
className,
)}
{...props}
/>
</PopoverPrimitive.Portal>
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export { Popover, PopoverTrigger, PopoverContent }
export { Popover, PopoverTrigger, PopoverAnchor, PopoverContent };

View file

@ -26,7 +26,7 @@ const SelectTrigger = React.forwardRef<
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:bg-muted [&>span]:line-clamp-1",
"flex h-10 w-full items-center justify-between rounded-md border border-input px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:bg-muted [&>span]:line-clamp-1 disabled:border-none",
className
)}
{...props}
@ -34,7 +34,7 @@ const SelectTrigger = React.forwardRef<
{children}
<SelectPrimitive.Icon asChild>
{props.disabled ? (
<LockIcon className="text-base h-5 w-5 opacity-50" />
<LockIcon className="text-base h-4 w-4 opacity-50" />
) : (
<ChevronsUpDown className="text-base h-5 w-5" />
)}

View file

@ -17,10 +17,10 @@ const Slider = React.forwardRef<
)}
{...props}
>
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-muted/40">
<SliderPrimitive.Range className="absolute h-full bg-primary" />
<SliderPrimitive.Track className="relative h-[5px] w-full grow overflow-hidden rounded-full bg-muted">
<SliderPrimitive.Range className="absolute h-full bg-muted-foreground" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
<SliderPrimitive.Thumb className="block h-4 w-4 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
</SliderPrimitive.Root>
))
Slider.displayName = SliderPrimitive.Root.displayName

View file

@ -7,7 +7,7 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
<textarea
data-slot="textarea"
className={cn(
"primary-input placeholder:font-mono placeholder:text-placeholder-foreground min-h-fit",
"primary-input placeholder:text-placeholder-foreground min-h-fit",
className
)}
{...props}

View file

@ -28,7 +28,6 @@
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/line-clamp": "^0.4.4",
"@tailwindcss/typography": "^0.5.16",
"@tanstack/react-query": "^5.86.0",
"ag-grid-community": "^34.2.0",
@ -45,6 +44,7 @@
"react-icons": "^5.5.0",
"react-markdown": "^10.1.0",
"react-syntax-highlighter": "^15.6.1",
"react-textarea-autosize": "^8.5.9",
"rehype-mathjax": "^7.1.0",
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.1",
@ -2318,14 +2318,6 @@
"tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1"
}
},
"node_modules/@tailwindcss/line-clamp": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/@tailwindcss/line-clamp/-/line-clamp-0.4.4.tgz",
"integrity": "sha512-5U6SY5z8N42VtrCrKlsTAA35gy2VSyYtHWCsg1H87NU1SXnEfekTVlrga9fzUDrrHcGi2Lb5KenUWb4lRQT5/g==",
"peerDependencies": {
"tailwindcss": ">=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1"
}
},
"node_modules/@tailwindcss/typography": {
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz",
@ -8482,6 +8474,23 @@
"react": ">= 0.14.0"
}
},
"node_modules/react-textarea-autosize": {
"version": "8.5.9",
"resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.9.tgz",
"integrity": "sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.20.13",
"use-composed-ref": "^1.3.0",
"use-latest": "^1.2.1"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@ -10135,6 +10144,51 @@
}
}
},
"node_modules/use-composed-ref": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.4.0.tgz",
"integrity": "sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/use-isomorphic-layout-effect": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.1.tgz",
"integrity": "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/use-latest": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/use-latest/-/use-latest-1.3.0.tgz",
"integrity": "sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ==",
"license": "MIT",
"dependencies": {
"use-isomorphic-layout-effect": "^1.1.1"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/use-sidecar": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",

View file

@ -29,7 +29,6 @@
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/line-clamp": "^0.4.4",
"@tailwindcss/typography": "^0.5.16",
"@tanstack/react-query": "^5.86.0",
"ag-grid-community": "^34.2.0",
@ -46,6 +45,7 @@
"react-icons": "^5.5.0",
"react-markdown": "^10.1.0",
"react-syntax-highlighter": "^15.6.1",
"react-textarea-autosize": "^8.5.9",
"rehype-mathjax": "^7.1.0",
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.1",

View file

@ -1,6 +1,6 @@
"use client"
import { useEffect, useState, useRef, Suspense } from "react"
import { useEffect, useState, Suspense } from "react"
import { useRouter, useSearchParams } from "next/navigation"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
@ -14,17 +14,20 @@ function AuthCallbackContent() {
const [status, setStatus] = useState<"processing" | "success" | "error">("processing")
const [error, setError] = useState<string | null>(null)
const [purpose, setPurpose] = useState<string>("app_auth")
const hasProcessed = useRef(false)
useEffect(() => {
// Prevent double execution in React Strict Mode
if (hasProcessed.current) return
hasProcessed.current = true
const code = searchParams.get('code')
const callbackKey = `callback_processed_${code}`
// Prevent double execution across component remounts
if (sessionStorage.getItem(callbackKey)) {
return
}
sessionStorage.setItem(callbackKey, 'true')
const handleCallback = async () => {
try {
// Get parameters from URL
const code = searchParams.get('code')
const state = searchParams.get('state')
const errorParam = searchParams.get('error')

File diff suppressed because it is too large Load diff

View file

@ -25,27 +25,28 @@
--muted-foreground: 240 4% 46%;
--accent: 240 5% 96%;
--accent-foreground: 0 0% 0%;
--destructive: 0 72% 51%;
--destructive-foreground: 0 0% 100%;
--warning: 48, 96%, 89%;
--warning-foreground: 26, 90%, 37%;
--border: 240 4.8% 95.9%;
--input: 240 6% 90%;
--ring: 0 0% 0%;
--placeholder-foreground: 240 5% 65%;
--accent-amber: 48, 96%, 89%; /* amber-100 #fef3c7 */
--accent-amber-foreground: 26, 90%, 37%; /* amber-700 #b45309 */
--accent-emerald: 149, 80%, 90%; /* emerald-100 #d1fae5 */
--accent-emerald-foreground: 161, 94%, 30%; /* emerald-600 #059669 */
--accent-red: 0, 93%, 94%; /* red-100 #fee2e2 */
--accent-red-foreground: 0, 72%, 51%; /* red-600 #dc2626 */
--accent-indigo: 226, 100%, 94%; /* indigo-100 #e0e7ff */
--accent-indigo-foreground: 243, 75%, 59%; /* indigo-600 #4f46e5 */
--accent-pink: 326, 78%, 95%; /* pink-100 #fce7f3 */
--accent-pink-foreground: 333, 71%, 51%; /* pink-600 #db2777 */
--accent-purple: 269, 100%, 95%; /* purple-100 #f3e8ff */
--accent-purple-foreground: 271, 81%, 56%; /* purple-600 #7c3aed */
--accent-amber: 48 96% 89%; /* amber-100 #fef3c7 */
--accent-amber-foreground: 26 90% 37%; /* amber-700 #b45309 */
--accent-emerald: 149 80% 90%; /* emerald-100 #d1fae5 */
--accent-emerald-foreground: 161 94% 30%; /* emerald-600 #059669 */
--accent-red: 0 93% 94%; /* red-100 #fee2e2 */
--accent-red-foreground: 0 72% 51%; /* red-600 #dc2626 */
--accent-indigo: 226 100% 94%; /* indigo-100 #e0e7ff */
--accent-indigo-foreground: 243 75% 59%; /* indigo-600 #4f46e5 */
--accent-pink: 326 78% 95%; /* pink-100 #fce7f3 */
--accent-pink-foreground: 333 71% 51%; /* pink-600 #db2777 */
--accent-purple: 269 100% 95%; /* purple-100 #f3e8ff */
--accent-purple-foreground: 271 81% 56%; /* purple-600 #7c3aed */
--destructive: 0 72% 51%; /* red-600 #dc2626 */
--destructive-foreground: 0 0% 100%;
--warning: 26 90% 37%; /* amber-700 #b45309 */
--warning-foreground: 0 0% 100%;
/* Component Colors */
--component-icon: #d8598a;
@ -71,27 +72,28 @@
--muted-foreground: 240 5% 65%;
--accent: 240 4% 16%;
--accent-foreground: 0 0% 100%;
--destructive: 0 84% 60%;
--destructive-foreground: 0 0% 100%;
--warning: 22, 78%, 26%;
--warning-foreground: 46, 97%, 65%;
--border: 240 3.7% 15.9%;
--input: 240 5% 34%;
--ring: 0 0% 100%;
--placeholder-foreground: 240 4% 46%;
--accent-amber: 22, 78%, 26%; /* amber-900 #78350f */
--accent-amber-foreground: 46, 97%, 65%; /* amber-300 #fcd34d */
--accent-emerald: 164, 86%, 16%; /* emerald-900 #064e3b */
--accent-emerald-foreground: 158, 64%, 52%; /* emerald-400 #34d399 */
--accent-red: 0, 63%, 31%; /* red-900 #7f1d1d */
--accent-red-foreground: 0, 91%, 71%; /* red-400 #f87171 */
--accent-indigo: 242, 47%, 34%; /* indigo-900 #312e81 */
--accent-indigo-foreground: 234, 89%, 74%; /* indigo-400 #818cf8 */
--accent-pink: 336, 69%, 30%; /* pink-900 #831843 */
--accent-pink-foreground: 329, 86%, 70%; /* pink-400 #f472b6 */
--accent-purple: 274, 66%, 32%; /* purple-900 #4c1d95 */
--accent-purple-foreground: 270, 95%, 75%; /* purple-400 #a78bfa */
--accent-amber: 22 78% 26%; /* amber-900 #78350f */
--accent-amber-foreground: 46 97% 65%; /* amber-300 #fcd34d */
--accent-emerald: 164 86% 16%; /* emerald-900 #064e3b */
--accent-emerald-foreground: 158 64% 52%; /* emerald-400 #34d399 */
--accent-red: 0 63% 31%; /* red-900 #7f1d1d */
--accent-red-foreground: 0 91% 71%; /* red-400 #f87171 */
--accent-indigo: 242 47% 34%; /* indigo-900 #312e81 */
--accent-indigo-foreground: 234 89% 74%; /* indigo-400 #818cf8 */
--accent-pink: 336 69% 30%; /* pink-900 #831843 */
--accent-pink-foreground: 329 86% 70%; /* pink-400 #f472b6 */
--accent-purple: 274 66% 32%; /* purple-900 #4c1d95 */
--accent-purple-foreground: 270 95% 75%; /* purple-400 #a78bfa */
--destructive: 0 84% 60%; /* red-500 #ef4444 */
--destructive-foreground: 0 0% 100%;
--warning: 46 97% 65%; /* amber-300 #fcd34d */
--warning-foreground: 0 0% 0%;
}
* {

View file

@ -1,12 +1,6 @@
"use client";
import {
ArrowLeft,
Copy,
File as FileIcon,
Loader2,
Search,
} from "lucide-react";
import { ArrowLeft, Check, Copy, Loader2, Search } from "lucide-react";
import { Suspense, useCallback, useEffect, useMemo, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { ProtectedRoute } from "@/components/protected-route";
@ -41,6 +35,9 @@ function ChunksPageContent() {
ChunkResult[]
>([]);
const [selectedChunks, setSelectedChunks] = useState<Set<number>>(new Set());
const [activeCopiedChunkIndex, setActiveCopiedChunkIndex] = useState<
number | null
>(null);
// Calculate average chunk length
const averageChunkLength = useMemo(
@ -70,8 +67,11 @@ function ChunksPageContent() {
}
}, [queryInputText, chunks]);
const handleCopy = useCallback((text: string) => {
navigator.clipboard.writeText(text);
const handleCopy = useCallback((text: string, index: number) => {
// Trim whitespace and remove new lines/tabs for cleaner copy
navigator.clipboard.writeText(text.trim().replace(/[\n\r\t]/gm, ""));
setActiveCopiedChunkIndex(index);
setTimeout(() => setActiveCopiedChunkIndex(null), 10 * 1000); // 10 seconds
}, []);
const fileData = (data as File[]).find(
@ -148,17 +148,29 @@ function ChunksPageContent() {
<div className="flex-1 flex flex-col min-h-0 px-6 py-6">
{/* Header */}
<div className="flex flex-col mb-6">
<div className="flex items-center gap-3 mb-2">
<Button variant="ghost" onClick={handleBack}>
<ArrowLeft size={18} />
<FileIcon className="text-muted-foreground" size={18} />
<h1 className="text-lg font-semibold">
{filename.replace(/\.[^/.]+$/, "")}
</h1>
<div className="flex flex-row items-center gap-3 mb-6">
<Button variant="ghost" onClick={handleBack} size="sm">
<ArrowLeft size={24} />
</Button>
<h1 className="text-lg font-semibold">
{/* Removes file extension from filename */}
{filename.replace(/\.[^/.]+$/, "")}
</h1>
</div>
<div className="flex items-center gap-3 pl-4 mt-2">
<div className="flex items-center gap-2">
<div className="flex flex-col items-start mt-2">
<div className="flex-1 flex items-center gap-2 w-full max-w-[616px] mb-8">
<Input
name="search-query"
icon={!queryInputText.length ? <Search size={18} /> : null}
id="search-query"
type="text"
defaultValue={parsedFilterData?.query}
value={queryInputText}
onChange={(e) => setQueryInputText(e.target.value)}
placeholder="Search chunks..."
/>
</div>
<div className="flex items-center pl-4 gap-2">
<Checkbox
id="selectAllChunks"
checked={selectAll}
@ -173,26 +185,11 @@ function ChunksPageContent() {
Select all
</Label>
</div>
<div className="flex-1 flex items-center gap-2">
<Input
name="search-query"
id="search-query"
type="text"
defaultValue={parsedFilterData?.query}
value={queryInputText}
onChange={(e) => setQueryInputText(e.target.value)}
placeholder="Search chunks..."
className="flex-1 bg-muted/20 rounded-lg border border-border/50 px-4 py-3 focus-visible:ring-1 focus-visible:ring-ring"
/>
<Button variant="outline" size="sm">
<Search />
</Button>
</div>
</div>
</div>
{/* Content Area - matches knowledge page structure */}
<div className="flex-1 overflow-auto">
<div className="flex-1 overflow-scroll pr-6">
{isFetching ? (
<div className="flex items-center justify-center h-64">
<div className="text-center">
@ -237,12 +234,15 @@ function ChunksPageContent() {
</span>
<div className="py-1">
<Button
className="p-1"
onClick={() => handleCopy(chunk.text)}
onClick={() => handleCopy(chunk.text, index)}
variant="ghost"
size="xs"
size="sm"
>
<Copy className="text-muted-foreground" />
{activeCopiedChunkIndex === index ? (
<Check className="text-muted-foreground" />
) : (
<Copy className="text-muted-foreground" />
)}
</Button>
</div>
</div>

View file

@ -255,7 +255,7 @@ function SearchPage() {
<div className="primary-input min-h-10 !flex items-center flex-nowrap focus-within:border-foreground transition-colors !p-[0.3rem]">
{selectedFilter?.name && (
<div
className={`flex items-center gap-1 h-full px-1.5 py-0.5 rounded max-w-[300px] ${
className={`flex items-center gap-1 h-full px-1.5 py-0.5 mr-1 rounded max-w-[25%] ${
filterAccentClasses[parsedFilterData?.color || "zinc"]
}`}
>
@ -267,8 +267,12 @@ function SearchPage() {
/>
</div>
)}
<Search
className="h-4 w-4 ml-1 flex-shrink-0 text-placeholder-foreground"
strokeWidth={1.5}
/>
<input
className="bg-transparent w-full h-full ml-2 focus:outline-none focus-visible:outline-none placeholder:font-mono"
className="bg-transparent w-full h-full ml-2 focus:outline-none focus-visible:outline-none font-mono placeholder:font-mono"
name="search-query"
id="search-query"
type="text"
@ -319,17 +323,16 @@ function SearchPage() {
rowMultiSelectWithClick={false}
suppressRowClickSelection={true}
getRowId={(params) => params.data.filename}
domLayout="autoHeight"
domLayout="normal"
onSelectionChanged={onSelectionChanged}
noRowsOverlayComponent={() => (
<div className="text-center">
<Search className="h-12 w-12 mx-auto mb-4 text-muted-foreground/50" />
<p className="text-lg text-muted-foreground">
No documents found
</p>
<p className="text-sm text-muted-foreground/70 mt-2">
Try adjusting your search terms
</p>
<div className="text-center pb-[45px]">
<div className="text-lg text-primary font-semibold">
No knowledge
</div>
<div className="text-sm mt-1 text-muted-foreground">
Add files from local or your preferred cloud.
</div>
</div>
)}
/>

View file

@ -69,18 +69,12 @@ function LoginPageContent() {
/>
<div className="flex flex-col items-center justify-center gap-4 z-10">
<Logo className="fill-primary" width={32} height={28} />
<div className="flex flex-col items-center justify-center gap-8">
<h1 className="text-2xl font-medium font-chivo">Welcome to OpenRAG</h1>
<p className="text-sm text-muted-foreground">
All your knowledge at your fingertips.
</p>
<Button onClick={login} className="w-80 gap-1.5" size="lg">
<GoogleLogo className="h-4 w-4" />
Continue with Google
</Button>
</div>
<div className="flex items-center justify-center gap-2 absolute bottom-6 text-xs text-muted-foreground z-10">
<p className="text-accent-emerald-foreground">Systems Operational</p>
<p>Privacy Policy</p>
</Button></div>
</div>
</div>
);

View file

@ -111,6 +111,7 @@ export function IBMOnboarding({
<ModelSelector
options={options}
value={endpoint}
custom
onValueChange={setEndpoint}
searchPlaceholder="Search endpoint..."
noOptionsPlaceholder="No endpoints available"
@ -118,8 +119,17 @@ export function IBMOnboarding({
/>
</LabelWrapper>
<LabelInput
label="IBM API key"
helperText="The API key for your watsonx.ai account."
label="watsonx Project ID"
helperText="Project ID for the model"
id="project-id"
required
placeholder="your-project-id"
value={projectId}
onChange={(e) => setProjectId(e.target.value)}
/>
<LabelInput
label="watsonx API key"
helperText="API key to access watsonx.ai"
id="api-key"
type="password"
required
@ -127,15 +137,6 @@ export function IBMOnboarding({
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
/>
<LabelInput
label="IBM Project ID"
helperText="The project ID for your watsonx.ai account."
id="project-id"
required
placeholder="your-project-id"
value={projectId}
onChange={(e) => setProjectId(e.target.value)}
/>
{isLoadingModels && (
<p className="text-mmd text-muted-foreground">
Validating configuration...

View file

@ -1,115 +1,158 @@
import { CheckIcon, ChevronsUpDownIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { cn } from "@/lib/utils";
export function ModelSelector({
options,
value,
onValueChange,
icon,
placeholder = "Select model...",
searchPlaceholder = "Search model...",
noOptionsPlaceholder = "No models available",
options,
value,
onValueChange,
icon,
placeholder = "Select model...",
searchPlaceholder = "Search model...",
noOptionsPlaceholder = "No models available",
custom = false,
}: {
options: {
value: string;
label: string;
default?: boolean;
}[];
value: string;
icon?: React.ReactNode;
placeholder?: string;
searchPlaceholder?: string;
noOptionsPlaceholder?: string;
onValueChange: (value: string) => void;
options: {
value: string;
label: string;
default?: boolean;
}[];
value: string;
icon?: React.ReactNode;
placeholder?: string;
searchPlaceholder?: string;
noOptionsPlaceholder?: string;
custom?: boolean;
onValueChange: (value: string) => void;
}) {
const [open, setOpen] = useState(false);
useEffect(() => {
if (value && !options.find((option) => option.value === value)) {
onValueChange("");
}
}, [options, value, onValueChange]);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
{/** biome-ignore lint/a11y/useSemanticElements: has to be a Button */}
<Button
variant="outline"
role="combobox"
disabled={options.length === 0}
aria-expanded={open}
className="w-full gap-2 justify-between font-normal text-sm"
>
{value ? (
<div className="flex items-center gap-2">
{icon && <div className="w-4 h-4">{icon}</div>}
{options.find((framework) => framework.value === value)?.label}
{options.find((framework) => framework.value === value)
?.default && (
<span className="text-xs text-foreground p-1 rounded-md bg-muted">
Default
</span>
)}
</div>
) : options.length === 0 ? (
noOptionsPlaceholder
) : (
placeholder
)}
<ChevronsUpDownIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent align="start" className="w-[400px] p-0">
<Command>
<CommandInput placeholder={searchPlaceholder} />
<CommandList>
<CommandEmpty>{noOptionsPlaceholder}</CommandEmpty>
<CommandGroup>
{options.map((option) => (
<CommandItem
key={option.value}
value={option.value}
onSelect={(currentValue) => {
if (currentValue !== value) {
onValueChange(currentValue);
}
setOpen(false);
}}
>
<CheckIcon
className={cn(
"mr-2 h-4 w-4",
value === option.value ? "opacity-100" : "opacity-0",
)}
/>
<div className="flex items-center gap-2">
{option.label}
{option.default && (
<span className="text-xs text-foreground p-1 rounded-md bg-muted">
Default
</span>
)}
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
const [open, setOpen] = useState(false);
const [searchValue, setSearchValue] = useState("");
useEffect(() => {
if (value && value !== "" && (!options.find((option) => option.value === value) && !custom)) {
onValueChange("");
}
}, [options, value, custom, onValueChange]);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
{/** biome-ignore lint/a11y/useSemanticElements: has to be a Button */}
<Button
variant="outline"
role="combobox"
disabled={options.length === 0}
aria-expanded={open}
className="w-full gap-2 justify-between font-normal text-sm"
>
{value ? (
<div className="flex items-center gap-2">
{icon && <div className="w-4 h-4">{icon}</div>}
{options.find((framework) => framework.value === value)?.label ||
value}
{/* {options.find((framework) => framework.value === value)
?.default && (
<span className="text-xs text-foreground p-1 rounded-md bg-muted">
Default
</span>
)} */}
{custom &&
value &&
!options.find((framework) => framework.value === value) && (
<Badge variant="outline" className="text-xs">
CUSTOM
</Badge>
)}
</div>
) : options.length === 0 ? (
noOptionsPlaceholder
) : (
placeholder
)}
<ChevronsUpDownIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent align="start" className=" p-0 w-[var(--radix-popover-trigger-width)]">
<Command>
<CommandInput
placeholder={searchPlaceholder}
value={searchValue}
onValueChange={setSearchValue}
/>
<CommandList>
<CommandEmpty>{noOptionsPlaceholder}</CommandEmpty>
<CommandGroup>
{options.map((option) => (
<CommandItem
key={option.value}
value={option.value}
onSelect={(currentValue) => {
if (currentValue !== value) {
onValueChange(currentValue);
}
setOpen(false);
}}
>
<CheckIcon
className={cn(
"mr-2 h-4 w-4",
value === option.value ? "opacity-100" : "opacity-0",
)}
/>
<div className="flex items-center gap-2">
{option.label}
{/* {option.default && (
<span className="text-xs text-foreground p-1 rounded-md bg-muted"> // DISABLING DEFAULT TAG FOR NOW
Default
</span>
)} */}
</div>
</CommandItem>
))}
{custom &&
searchValue &&
!options.find((option) => option.value === searchValue) && (
<CommandItem
value={searchValue}
onSelect={(currentValue) => {
if (currentValue !== value) {
onValueChange(currentValue);
}
setOpen(false);
}}
>
<CheckIcon
className={cn(
"mr-2 h-4 w-4",
value === searchValue ? "opacity-100" : "opacity-0",
)}
/>
<div className="flex items-center gap-2">
{searchValue}
<span className="text-xs text-foreground p-1 rounded-md bg-muted">
Custom
</span>
</div>
</CommandItem>
)}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}

View file

@ -2,7 +2,7 @@ import { useState } from "react";
import { LabelInput } from "@/components/label-input";
import { LabelWrapper } from "@/components/label-wrapper";
import OpenAILogo from "@/components/logo/openai-logo";
import { Checkbox } from "@/components/ui/checkbox";
import { Switch } from "@/components/ui/switch";
import { useDebouncedValue } from "@/lib/debounce";
import type { OnboardingVariables } from "../../api/mutations/useOnboardingMutation";
import { useGetOpenAIModelsQuery } from "../../api/queries/useGetModelsQuery";
@ -11,121 +11,114 @@ import { useUpdateSettings } from "../hooks/useUpdateSettings";
import { AdvancedOnboarding } from "./advanced";
export function OpenAIOnboarding({
setSettings,
sampleDataset,
setSampleDataset,
setSettings,
sampleDataset,
setSampleDataset,
}: {
setSettings: (settings: OnboardingVariables) => void;
sampleDataset: boolean;
setSampleDataset: (dataset: boolean) => void;
setSettings: (settings: OnboardingVariables) => void;
sampleDataset: boolean;
setSampleDataset: (dataset: boolean) => void;
}) {
const [apiKey, setApiKey] = useState("");
const [getFromEnv, setGetFromEnv] = useState(true);
const debouncedApiKey = useDebouncedValue(apiKey, 500);
const [apiKey, setApiKey] = useState("");
const [getFromEnv, setGetFromEnv] = useState(true);
const debouncedApiKey = useDebouncedValue(apiKey, 500);
// Fetch models from API when API key is provided
const {
data: modelsData,
isLoading: isLoadingModels,
error: modelsError,
} = useGetOpenAIModelsQuery(
getFromEnv
? { apiKey: "" }
: debouncedApiKey
? { apiKey: debouncedApiKey }
: undefined,
{ enabled: debouncedApiKey !== "" || getFromEnv },
);
// Use custom hook for model selection logic
const {
languageModel,
embeddingModel,
setLanguageModel,
setEmbeddingModel,
languageModels,
embeddingModels,
} = useModelSelection(modelsData);
const handleSampleDatasetChange = (dataset: boolean) => {
setSampleDataset(dataset);
};
// Fetch models from API when API key is provided
const {
data: modelsData,
isLoading: isLoadingModels,
error: modelsError,
} = useGetOpenAIModelsQuery(
getFromEnv
? { apiKey: "" }
: debouncedApiKey
? { apiKey: debouncedApiKey }
: undefined,
{ enabled: debouncedApiKey !== "" || getFromEnv },
);
// Use custom hook for model selection logic
const {
languageModel,
embeddingModel,
setLanguageModel,
setEmbeddingModel,
languageModels,
embeddingModels,
} = useModelSelection(modelsData);
const handleSampleDatasetChange = (dataset: boolean) => {
setSampleDataset(dataset);
};
const handleGetFromEnvChange = (fromEnv: boolean) => {
setGetFromEnv(fromEnv);
if (fromEnv) {
setApiKey("");
}
setLanguageModel("");
setEmbeddingModel("");
};
const handleGetFromEnvChange = (fromEnv: boolean) => {
setGetFromEnv(fromEnv);
if (fromEnv) {
setApiKey("");
}
setLanguageModel("");
setEmbeddingModel("");
};
// Update settings when values change
useUpdateSettings(
"openai",
{
apiKey,
languageModel,
embeddingModel,
},
setSettings,
);
return (
<>
<div className="space-y-5">
<LabelWrapper
label="Use environment OpenAI API key"
id="get-api-key"
helperText={
<>
Reuse the key from your environment config.
<br />
Uncheck to enter a different key.
</>
}
flex
start
>
<Checkbox
checked={getFromEnv}
onCheckedChange={handleGetFromEnvChange}
/>
</LabelWrapper>
{!getFromEnv && (
<div className="space-y-1">
<LabelInput
label="OpenAI API key"
helperText="The API key for your OpenAI account."
className={modelsError ? "!border-destructive" : ""}
id="api-key"
type="password"
required
placeholder="sk-..."
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
/>
{isLoadingModels && (
<p className="text-mmd text-muted-foreground">
Validating API key...
</p>
)}
{modelsError && (
<p className="text-mmd text-destructive">
Invalid OpenAI API key. Verify or replace the key.
</p>
)}
</div>
)}
</div>
<AdvancedOnboarding
icon={<OpenAILogo className="w-4 h-4" />}
languageModels={languageModels}
embeddingModels={embeddingModels}
languageModel={languageModel}
embeddingModel={embeddingModel}
sampleDataset={sampleDataset}
setLanguageModel={setLanguageModel}
setSampleDataset={handleSampleDatasetChange}
setEmbeddingModel={setEmbeddingModel}
/>
</>
);
// Update settings when values change
useUpdateSettings(
"openai",
{
apiKey,
languageModel,
embeddingModel,
},
setSettings,
);
return (
<>
<div className="space-y-5">
<LabelWrapper
label="Use environment OpenAI API key"
id="get-api-key"
description="Reuse the key from your environment config. Turn off to enter a different key."
flex
>
<Switch
checked={getFromEnv}
onCheckedChange={handleGetFromEnvChange}
/>
</LabelWrapper>
{!getFromEnv && (
<div className="space-y-1">
<LabelInput
label="OpenAI API key"
helperText="The API key for your OpenAI account."
className={modelsError ? "!border-destructive" : ""}
id="api-key"
type="password"
required
placeholder="sk-..."
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
/>
{isLoadingModels && (
<p className="text-mmd text-muted-foreground">
Validating API key...
</p>
)}
{modelsError && (
<p className="text-mmd text-destructive">
Invalid OpenAI API key. Verify or replace the key.
</p>
)}
</div>
)}
</div>
<AdvancedOnboarding
icon={<OpenAILogo className="w-4 h-4" />}
languageModels={languageModels}
embeddingModels={embeddingModels}
languageModel={languageModel}
embeddingModel={embeddingModel}
sampleDataset={sampleDataset}
setLanguageModel={setLanguageModel}
setSampleDataset={handleSampleDatasetChange}
setEmbeddingModel={setEmbeddingModel}
/>
</>
);
}

View file

@ -68,7 +68,6 @@ function OnboardingPage() {
// Mutations
const onboardingMutation = useOnboardingMutation({
onSuccess: (data) => {
toast.success("Onboarding completed successfully!");
console.log("Onboarding completed successfully", data);
router.push(redirect);
},
@ -137,7 +136,7 @@ function OnboardingPage() {
Connect a model provider
</h1>
</div>
<Card className="w-full max-w-[580px]">
<Card className="w-full max-w-[600px]">
<Tabs
defaultValue={modelProvider}
onValueChange={handleSetModelProvider}
@ -150,7 +149,7 @@ function OnboardingPage() {
</TabsTrigger>
<TabsTrigger value="watsonx">
<IBMLogo className="w-4 h-4" />
IBM
IBM watsonx.ai
</TabsTrigger>
<TabsTrigger value="ollama">
<OllamaLogo className="w-4 h-4" />
@ -192,7 +191,7 @@ function OnboardingPage() {
disabled={!isComplete}
loading={onboardingMutation.isPending}
>
Complete
<span className="select-none">Complete</span>
</Button>
</div>
</TooltipTrigger>

View file

@ -0,0 +1,36 @@
const GoogleDriveIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="16"
viewBox="0 0 18 16"
fill="none"
>
<path
d="M2.03338 13.2368L2.75732 14.4872C2.90774 14.7504 3.12398 14.9573 3.37783 15.1077L5.9633 10.6325H0.792358C0.792358 10.9239 0.867572 11.2154 1.018 11.4786L2.03338 13.2368Z"
fill="#0066DA"
/>
<path
d="M9.00005 5.36753L6.41458 0.892312C6.16073 1.04274 5.94449 1.24958 5.79407 1.51283L1.018 9.78633C0.870339 10.0439 0.792555 10.3356 0.792358 10.6325H5.9633L9.00005 5.36753Z"
fill="#00AC47"
/>
<path
d="M14.6223 15.1077C14.8761 14.9573 15.0924 14.7504 15.2428 14.4872L15.5436 13.9701L16.9821 11.4786C17.1325 11.2154 17.2077 10.9239 17.2077 10.6325H12.0364L13.1368 12.7949L14.6223 15.1077Z"
fill="#EA4335"
/>
<path
d="M9.00005 5.36753L11.5855 0.892313C11.3317 0.741885 11.0402 0.666672 10.7394 0.666672H7.26074C6.95988 0.666672 6.66843 0.751287 6.41458 0.892312L9.00005 5.36753Z"
fill="#00832D"
/>
<path
d="M12.0368 10.6325H5.9633L3.37783 15.1077C3.63167 15.2581 3.92313 15.3333 4.22398 15.3333H13.7761C14.077 15.3333 14.3684 15.2487 14.6223 15.1077L12.0368 10.6325Z"
fill="#2684FC"
/>
<path
d="M14.5941 5.64958L12.206 1.51283C12.0556 1.24958 11.8394 1.04274 11.5855 0.892313L9.00005 5.36753L12.0368 10.6325L17.1983 10.6325C17.1983 10.341 17.1231 10.0496 16.9727 9.78633L14.5941 5.64958Z"
fill="#FFBA00"
/>
</svg>
);
export default GoogleDriveIcon;

View file

@ -0,0 +1,164 @@
const OneDriveIcon = () => (
<svg
width="17"
height="12"
viewBox="0 0 17 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_3016_367)">
<path
d="M5.2316 2.32803C2.88332 2.3281 1.128 4.25034 0.99585 6.39175C1.07765 6.85315 1.34653 7.7643 1.76759 7.71751C2.29391 7.65902 3.61947 7.71751 4.75008 5.67068C5.57599 4.17546 7.27498 2.328 5.2316 2.32803Z"
fill="url(#paint0_radial_3016_367)"
/>
<path
d="M4.68864 3.12741C3.89927 4.37718 2.83674 6.16798 2.47813 6.7315C2.05185 7.40136 0.922937 7.11678 1.01646 6.15663C1.00724 6.23457 1.00016 6.31315 0.995274 6.39226C0.840839 8.89029 2.82143 10.9648 5.28604 10.9648C8.00238 10.9648 14.4806 7.58038 13.825 4.18931C13.134 2.19599 11.1918 0.766266 8.99072 0.766266C6.78965 0.766266 5.37899 2.03436 4.68864 3.12741Z"
fill="url(#paint1_radial_3016_367)"
/>
<path
d="M4.68864 3.12741C3.89927 4.37718 2.83674 6.16798 2.47813 6.7315C2.05185 7.40136 0.922937 7.11678 1.01646 6.15663C1.00724 6.23457 1.00016 6.31315 0.995274 6.39226C0.840839 8.89029 2.82143 10.9648 5.28604 10.9648C8.00238 10.9648 14.4806 7.58038 13.825 4.18931C13.134 2.19599 11.1918 0.766266 8.99072 0.766266C6.78965 0.766266 5.37899 2.03436 4.68864 3.12741Z"
fill="url(#paint2_radial_3016_367)"
fill-opacity="0.4"
/>
<path
d="M4.68864 3.12741C3.89927 4.37718 2.83674 6.16798 2.47813 6.7315C2.05185 7.40136 0.922937 7.11678 1.01646 6.15663C1.00724 6.23457 1.00016 6.31315 0.995274 6.39226C0.840839 8.89029 2.82143 10.9648 5.28604 10.9648C8.00238 10.9648 14.4806 7.58038 13.825 4.18931C13.134 2.19599 11.1918 0.766266 8.99072 0.766266C6.78965 0.766266 5.37899 2.03436 4.68864 3.12741Z"
fill="url(#paint3_radial_3016_367)"
/>
<path
d="M4.68864 3.12741C3.89927 4.37718 2.83674 6.16798 2.47813 6.7315C2.05185 7.40136 0.922937 7.11678 1.01646 6.15663C1.00724 6.23457 1.00016 6.31315 0.995274 6.39226C0.840839 8.89029 2.82143 10.9648 5.28604 10.9648C8.00238 10.9648 14.4806 7.58038 13.825 4.18931C13.134 2.19599 11.1918 0.766266 8.99072 0.766266C6.78965 0.766266 5.37899 2.03436 4.68864 3.12741Z"
fill="url(#paint4_radial_3016_367)"
fill-opacity="0.6"
/>
<path
d="M4.68864 3.12741C3.89927 4.37718 2.83674 6.16798 2.47813 6.7315C2.05185 7.40136 0.922937 7.11678 1.01646 6.15663C1.00724 6.23457 1.00016 6.31315 0.995274 6.39226C0.840839 8.89029 2.82143 10.9648 5.28604 10.9648C8.00238 10.9648 14.4806 7.58038 13.825 4.18931C13.134 2.19599 11.1918 0.766266 8.99072 0.766266C6.78965 0.766266 5.37899 2.03436 4.68864 3.12741Z"
fill="url(#paint5_radial_3016_367)"
fill-opacity="0.9"
/>
<path
d="M5.24634 10.9659C5.24634 10.9659 11.7322 10.9786 12.8323 10.9786C14.8288 10.9786 16.3467 9.34866 16.3468 7.44669C16.3468 5.54468 14.7983 3.92459 12.8323 3.92459C10.8663 3.92459 9.73412 5.39542 8.88374 7.00089C7.8873 8.88221 6.61615 10.9433 5.24634 10.9659Z"
fill="url(#paint6_linear_3016_367)"
/>
<path
d="M5.24634 10.9659C5.24634 10.9659 11.7322 10.9786 12.8323 10.9786C14.8288 10.9786 16.3467 9.34866 16.3468 7.44669C16.3468 5.54468 14.7983 3.92459 12.8323 3.92459C10.8663 3.92459 9.73412 5.39542 8.88374 7.00089C7.8873 8.88221 6.61615 10.9433 5.24634 10.9659Z"
fill="url(#paint7_radial_3016_367)"
fill-opacity="0.4"
/>
<path
d="M5.24634 10.9659C5.24634 10.9659 11.7322 10.9786 12.8323 10.9786C14.8288 10.9786 16.3467 9.34866 16.3468 7.44669C16.3468 5.54468 14.7983 3.92459 12.8323 3.92459C10.8663 3.92459 9.73412 5.39542 8.88374 7.00089C7.8873 8.88221 6.61615 10.9433 5.24634 10.9659Z"
fill="url(#paint8_radial_3016_367)"
fill-opacity="0.9"
/>
</g>
<defs>
<radialGradient
id="paint0_radial_3016_367"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(1.28709 2.88928) rotate(50.1526) scale(4.84121 8.03004)"
>
<stop stop-color="#4894FE" />
<stop offset="0.695072" stop-color="#0934B3" />
</radialGradient>
<radialGradient
id="paint1_radial_3016_367"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(14.2836 -2.68456) rotate(130.923) scale(20.8177 15.4261)"
>
<stop offset="0.165327" stop-color="#23C0FE" />
<stop offset="0.534" stop-color="#1C91FF" />
</radialGradient>
<radialGradient
id="paint2_radial_3016_367"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(4.42852 3.16495) rotate(-139.986) scale(4.23243 9.68892)"
>
<stop stop-color="white" />
<stop offset="0.660528" stop-color="#ADC0FF" stop-opacity="0" />
</radialGradient>
<radialGradient
id="paint3_radial_3016_367"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(9.03076 8.16737) rotate(-139.764) scale(4.77056 7.24512)"
>
<stop stop-color="#033ACC" />
<stop offset="1" stop-color="#368EFF" stop-opacity="0" />
</radialGradient>
<radialGradient
id="paint4_radial_3016_367"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(4.14837 0.44361) rotate(66.5713) scale(10.4677 11.3005)"
>
<stop offset="0.592618" stop-color="#3464E3" stop-opacity="0" />
<stop offset="1" stop-color="#033ACC" />
</radialGradient>
<radialGradient
id="paint5_radial_3016_367"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(14.1157 -1.59739) rotate(135) scale(15.3977 24.123)"
>
<stop stop-color="#4BFDE8" />
<stop offset="0.543937" stop-color="#4BFDE8" stop-opacity="0" />
</radialGradient>
<linearGradient
id="paint6_linear_3016_367"
x1="10.8"
y1="10.9715"
x2="10.8"
y2="4.00825"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#0086FF" />
<stop offset="0.49" stop-color="#00BBFF" />
</linearGradient>
<radialGradient
id="paint7_radial_3016_367"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(7.16132 4.75417) rotate(21.6324) scale(6.97728 13.2126)"
>
<stop stop-color="white" />
<stop offset="0.785262" stop-color="white" stop-opacity="0" />
</radialGradient>
<radialGradient
id="paint8_radial_3016_367"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(16.1298 3.37785) rotate(139.243) scale(9.56565 9.59808)"
>
<stop stop-color="#4BFDE8" />
<stop offset="0.584724" stop-color="#4BFDE8" stop-opacity="0" />
</radialGradient>
<clipPath id="clip0_3016_367">
<rect
width="15.6444"
height="10.6667"
fill="white"
transform="translate(0.844482 0.666672)"
/>
</clipPath>
</defs>
</svg>
);
export default OneDriveIcon;

View file

@ -0,0 +1,211 @@
const SharePointIcon = () => (
<svg
width="15"
height="16"
viewBox="0 0 15 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_3016_409)">
<path
d="M6.1335 9.6C8.78446 9.6 10.9335 7.45096 10.9335 4.8C10.9335 2.14903 8.78446 0 6.1335 0C3.48254 0 1.3335 2.14903 1.3335 4.8C1.3335 7.45096 3.48254 9.6 6.1335 9.6Z"
fill="url(#paint0_linear_3016_409)"
/>
<path
d="M6.1335 9.6C8.78446 9.6 10.9335 7.45096 10.9335 4.8C10.9335 2.14903 8.78446 0 6.1335 0C3.48254 0 1.3335 2.14903 1.3335 4.8C1.3335 7.45096 3.48254 9.6 6.1335 9.6Z"
fill="url(#paint1_radial_3016_409)"
fill-opacity="0.2"
/>
<path
d="M6.1335 9.6C8.78446 9.6 10.9335 7.45096 10.9335 4.8C10.9335 2.14903 8.78446 0 6.1335 0C3.48254 0 1.3335 2.14903 1.3335 4.8C1.3335 7.45096 3.48254 9.6 6.1335 9.6Z"
fill="url(#paint2_radial_3016_409)"
fill-opacity="0.31"
/>
<path
d="M6.1335 9.6C8.78446 9.6 10.9335 7.45096 10.9335 4.8C10.9335 2.14903 8.78446 0 6.1335 0C3.48254 0 1.3335 2.14903 1.3335 4.8C1.3335 7.45096 3.48254 9.6 6.1335 9.6Z"
fill="url(#paint3_radial_3016_409)"
fill-opacity="0.7"
/>
<path
d="M10.5117 12.8C12.7209 12.8 14.5117 11.0091 14.5117 8.8C14.5117 6.59088 12.7209 4.8 10.5117 4.8C8.3026 4.8 6.51172 6.59088 6.51172 8.8C6.51172 11.0091 8.3026 12.8 10.5117 12.8Z"
fill="url(#paint4_linear_3016_409)"
/>
<path
d="M10.5117 12.8C12.7209 12.8 14.5117 11.0091 14.5117 8.8C14.5117 6.59088 12.7209 4.8 10.5117 4.8C8.3026 4.8 6.51172 6.59088 6.51172 8.8C6.51172 11.0091 8.3026 12.8 10.5117 12.8Z"
fill="url(#paint5_radial_3016_409)"
fill-opacity="0.5"
/>
<path
d="M10.5117 12.8C12.7209 12.8 14.5117 11.0091 14.5117 8.8C14.5117 6.59088 12.7209 4.8 10.5117 4.8C8.3026 4.8 6.51172 6.59088 6.51172 8.8C6.51172 11.0091 8.3026 12.8 10.5117 12.8Z"
fill="url(#paint6_radial_3016_409)"
fill-opacity="0.7"
/>
<path
d="M6.7335 16C8.61126 16 10.1335 14.4778 10.1335 12.6C10.1335 10.7222 8.61126 9.2 6.7335 9.2C4.85574 9.2 3.3335 10.7222 3.3335 12.6C3.3335 14.4778 4.85574 16 6.7335 16Z"
fill="url(#paint7_linear_3016_409)"
/>
<path
d="M6.7335 16C8.61126 16 10.1335 14.4778 10.1335 12.6C10.1335 10.7222 8.61126 9.2 6.7335 9.2C4.85574 9.2 3.3335 10.7222 3.3335 12.6C3.3335 14.4778 4.85574 16 6.7335 16Z"
fill="url(#paint8_linear_3016_409)"
fill-opacity="0.32"
/>
<path
d="M5.23354 7.60001H1.43354C0.715575 7.60001 0.133545 8.18204 0.133545 8.90001V12.7C0.133545 13.418 0.715575 14 1.43354 14H5.23354C5.95151 14 6.53354 13.418 6.53354 12.7V8.90001C6.53354 8.18204 5.95151 7.60001 5.23354 7.60001Z"
fill="url(#paint9_radial_3016_409)"
/>
<path
d="M5.23354 7.60001H1.43354C0.715575 7.60001 0.133545 8.18204 0.133545 8.90001V12.7C0.133545 13.418 0.715575 14 1.43354 14H5.23354C5.95151 14 6.53354 13.418 6.53354 12.7V8.90001C6.53354 8.18204 5.95151 7.60001 5.23354 7.60001Z"
fill="url(#paint10_radial_3016_409)"
fill-opacity="0.6"
/>
<path
d="M1.95581 11.8734L2.64917 11.523C2.72733 11.676 2.82929 11.7887 2.95505 11.8611C3.08249 11.9335 3.22185 11.9697 3.37309 11.9697C3.54133 11.9697 3.66965 11.9368 3.75801 11.871C3.84641 11.8036 3.89057 11.7024 3.89057 11.5675C3.89057 11.4622 3.84809 11.3733 3.76313 11.301C3.67817 11.2269 3.52777 11.171 3.31193 11.1332C2.90069 11.0608 2.60157 10.9341 2.41465 10.7531C2.22941 10.5722 2.13679 10.3468 2.13679 10.077C2.13679 9.74136 2.25915 9.4732 2.50387 9.27248C2.74857 9.0718 3.07145 8.97144 3.47253 8.97144C3.74273 8.97144 3.98065 9.02492 4.18629 9.13184C4.39189 9.23876 4.55505 9.39176 4.67569 9.59084L3.99765 9.92892C3.92285 9.81704 3.84213 9.73644 3.75549 9.68708C3.66881 9.63608 3.56005 9.61056 3.42917 9.61056C3.27285 9.61056 3.15389 9.64348 3.07233 9.70928C2.99245 9.77508 2.95249 9.86064 2.95249 9.96592C2.95249 10.0564 2.99073 10.1362 3.06721 10.2053C3.14537 10.2727 3.30173 10.3278 3.53625 10.3706C3.93053 10.443 4.22449 10.5746 4.41825 10.7654C4.61369 10.9546 4.71137 11.194 4.71137 11.4836C4.71137 11.8356 4.59497 12.1145 4.36217 12.3201C4.12933 12.5258 3.79713 12.6286 3.36545 12.6286C3.05277 12.6286 2.77065 12.5628 2.51916 12.4312C2.26935 12.2979 2.08157 12.112 1.95581 11.8734Z"
fill="white"
/>
<path
d="M1.95483 11.9088L2.64867 11.5466C2.72687 11.7047 2.82891 11.8212 2.95475 11.896C3.08227 11.9709 3.22171 12.0083 3.37307 12.0083C3.54143 12.0083 3.66983 11.9743 3.75823 11.9062C3.84667 11.8365 3.89087 11.732 3.89087 11.5925C3.89087 11.4837 3.84835 11.3918 3.76335 11.317C3.67831 11.2405 3.52783 11.1827 3.31187 11.1436C2.90031 11.0688 2.60103 10.9378 2.41397 10.7508C2.22862 10.5637 2.13594 10.3307 2.13594 10.0518C2.13594 9.70491 2.25838 9.42775 2.50325 9.22027C2.74815 9.01279 3.07123 8.90907 3.47255 8.90907C3.74295 8.90907 3.98099 8.96435 4.18679 9.07487C4.39255 9.18539 4.55579 9.34355 4.67651 9.54931L3.99803 9.89879C3.92319 9.78315 3.84243 9.69983 3.75571 9.64879C3.66895 9.59607 3.56015 9.56971 3.42919 9.56971C3.27275 9.56971 3.15371 9.60375 3.07207 9.67175C2.99215 9.73979 2.95219 9.82819 2.95219 9.93703C2.95219 10.0306 2.99047 10.113 3.06699 10.1845C3.14519 10.2542 3.30167 10.3112 3.53631 10.3554C3.93083 10.4302 4.22503 10.5662 4.41891 10.7635C4.61447 10.959 4.71223 11.2065 4.71223 11.5058C4.71223 11.8697 4.59575 12.1579 4.36279 12.3705C4.12979 12.583 3.79735 12.6893 3.36543 12.6893C3.05251 12.6893 2.77023 12.6213 2.51856 12.4853C2.26858 12.3475 2.08067 12.1554 1.95483 11.9088Z"
fill="white"
/>
</g>
<defs>
<linearGradient
id="paint0_linear_3016_409"
x1="2.5335"
y1="1.2"
x2="8.9335"
y2="9.6"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#00E3DF" />
<stop offset="0.410156" stop-color="#0097A8" />
<stop offset="1" stop-color="#007791" />
</linearGradient>
<radialGradient
id="paint1_radial_3016_409"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(7.60222 10.9279) rotate(-112.448) scale(7.37044 13.2516)"
>
<stop offset="0.28573" stop-color="#003B5D" />
<stop offset="0.612265" stop-color="#004A6C" stop-opacity="0.688298" />
<stop offset="0.968041" stop-color="#006F94" stop-opacity="0" />
</radialGradient>
<radialGradient
id="paint2_radial_3016_409"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(7.77166 8.81012) rotate(-112.063) scale(6.22076 11.1709)"
>
<stop offset="0.259744" stop-color="#002A42" />
<stop offset="0.612265" stop-color="#004261" stop-opacity="0.688298" />
<stop offset="0.968041" stop-color="#006F94" stop-opacity="0" />
</radialGradient>
<radialGradient
id="paint3_radial_3016_409"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(8.87294 0.508276) rotate(124.447) scale(5.20428)"
>
<stop stop-color="#78EDFF" />
<stop offset="1" stop-color="#2CCFCA" stop-opacity="0" />
</radialGradient>
<linearGradient
id="paint4_linear_3016_409"
x1="7.51172"
y1="5.8"
x2="12.845"
y2="12.8"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#00E3DF" />
<stop offset="0.476427" stop-color="#00A2B8" />
<stop offset="0.945063" stop-color="#00637C" />
</linearGradient>
<radialGradient
id="paint5_radial_3016_409"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(8.22004 12.1333) rotate(-70.8012) scale(4.94148 8.90348)"
>
<stop stop-color="#003B5D" />
<stop offset="0.492035" stop-color="#004C6C" stop-opacity="0.72" />
<stop offset="0.968041" stop-color="#007A86" stop-opacity="0" />
</radialGradient>
<radialGradient
id="paint6_radial_3016_409"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(12.7946 5.22356) rotate(124.447) scale(4.33692)"
>
<stop stop-color="#78EDFF" />
<stop offset="1" stop-color="#2CCFCA" stop-opacity="0" />
</radialGradient>
<linearGradient
id="paint7_linear_3016_409"
x1="4.3535"
y1="9.54"
x2="7.7535"
y2="16.34"
gradientUnits="userSpaceOnUse"
>
<stop offset="0.0534989" stop-color="#75FFF6" />
<stop offset="0.51144" stop-color="#00C7D1" />
<stop offset="0.96002" stop-color="#0096AD" />
</linearGradient>
<linearGradient
id="paint8_linear_3016_409"
x1="10.1179"
y1="16.0003"
x2="8.30102"
y2="13.4503"
gradientUnits="userSpaceOnUse"
>
<stop offset="0.259744" stop-color="#0E5A5D" />
<stop offset="0.535716" stop-color="#126C6B" stop-opacity="0.688298" />
<stop offset="0.968041" stop-color="#1C948A" stop-opacity="0" />
</linearGradient>
<radialGradient
id="paint9_radial_3016_409"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(0.133545 7.60001) rotate(45) scale(9.05096)"
>
<stop offset="0.0625" stop-color="#00B6BD" />
<stop offset="0.890131" stop-color="#00495C" />
</radialGradient>
<radialGradient
id="paint10_radial_3016_409"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(3.33354 11.44) rotate(90) scale(4.48 5.1)"
>
<stop offset="0.566964" stop-color="#1E8581" stop-opacity="0" />
<stop offset="0.973806" stop-color="#1ECBE6" />
</radialGradient>
<clipPath id="clip0_3016_409">
<rect
width="14.4"
height="16"
fill="white"
transform="translate(0.133545)"
/>
</clipPath>
</defs>
</svg>
);
export default SharePointIcon;

View file

@ -1,7 +1,8 @@
"use client";
import { ArrowUpRight, Loader2, PlugZap, RefreshCw } from "lucide-react";
import { useSearchParams } from "next/navigation";
import { ArrowUpRight, Loader2, Minus, Plus } from "lucide-react";
import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation";
import { Suspense, useCallback, useEffect, useState } from "react";
import { useUpdateFlowSettingMutation } from "@/app/api/mutations/useUpdateFlowSettingMutation";
import {
@ -12,8 +13,8 @@ import {
import { useGetSettingsQuery } from "@/app/api/queries/useGetSettingsQuery";
import { ConfirmationDialog } from "@/components/confirmation-dialog";
import { LabelWrapper } from "@/components/label-wrapper";
import OpenAILogo from "@/components/logo/openai-logo";
import { ProtectedRoute } from "@/components/protected-route";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
@ -22,7 +23,6 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
@ -41,9 +41,14 @@ import {
UI_CONSTANTS,
} from "@/lib/constants";
import { useDebounce } from "@/lib/debounce";
import { ModelSelector } from "../onboarding/components/model-selector";
import { getFallbackModels, type ModelProvider } from "./helpers/model-helpers";
import { ModelSelectItems } from "./helpers/model-select-item";
import GoogleDriveIcon from "./icons/google-drive-icon";
import OneDriveIcon from "./icons/one-drive-icon";
import SharePointIcon from "./icons/share-point-icon";
const { MAX_SYSTEM_PROMPT_CHARS } = UI_CONSTANTS;
interface GoogleDriveFile {
@ -92,10 +97,38 @@ interface Connection {
last_sync?: string;
}
const DEFAULT_CONNECTORS: Connector[] = [
{
id: "google_drive",
name: "Google Drive",
description: "Google Drive is not configured.",
icon: <GoogleDriveIcon />,
status: "not_connected",
type: "google_drive",
},
{
id: "one_drive",
name: "OneDrive",
description: "OneDrive is not configured.",
icon: <OneDriveIcon />,
status: "not_connected",
type: "one_drive",
},
{
id: "amazon_s3",
name: "SharePoint",
description: "SharePoint is not configured.",
icon: <SharePointIcon />,
status: "not_connected",
type: "sharepoint",
},
];
function KnowledgeSourcesPage() {
const { isAuthenticated, isNoAuthMode } = useAuth();
const { addTask, tasks } = useTask();
const searchParams = useSearchParams();
const router = useRouter();
// Connectors state
const [connectors, setConnectors] = useState<Connector[]>([]);
@ -265,19 +298,15 @@ function KnowledgeSourcesPage() {
// Helper function to get connector icon
const getConnectorIcon = useCallback((iconName: string) => {
const iconMap: { [key: string]: React.ReactElement } = {
"google-drive": (
<div className="w-8 h-8 bg-blue-600 rounded flex items-center justify-center text-white font-bold leading-none shrink-0">
G
</div>
),
"google-drive": <GoogleDriveIcon />,
sharepoint: (
<div className="w-8 h-8 bg-blue-700 rounded flex items-center justify-center text-white font-bold leading-none shrink-0">
SP
</div>
),
onedrive: (
<div className="w-8 h-8 bg-blue-400 rounded flex items-center justify-center text-white font-bold leading-none shrink-0">
OD
<div className="w-8 h-8 bg-white border border-gray-300 rounded flex items-center justify-center">
<OneDriveIcon />
</div>
),
};
@ -398,93 +427,77 @@ function KnowledgeSourcesPage() {
}
};
const handleSync = async (connector: Connector) => {
if (!connector.connectionId) return;
// const handleSync = async (connector: Connector) => {
// if (!connector.connectionId) return;
setIsSyncing(connector.id);
setSyncResults((prev) => ({ ...prev, [connector.id]: null }));
// setIsSyncing(connector.id);
// setSyncResults(prev => ({ ...prev, [connector.id]: null }));
try {
const syncBody: {
connection_id: string;
max_files?: number;
selected_files?: string[];
} = {
connection_id: connector.connectionId,
max_files: syncAllFiles ? 0 : maxFiles || undefined,
};
// try {
// const syncBody: {
// connection_id: string;
// max_files?: number;
// selected_files?: string[];
// } = {
// connection_id: connector.connectionId,
// max_files: syncAllFiles ? 0 : maxFiles || undefined,
// };
// Note: File selection is now handled via the cloud connectors dialog
// // Note: File selection is now handled via the cloud connectors dialog
const response = await fetch(`/api/connectors/${connector.type}/sync`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(syncBody),
});
// const response = await fetch(`/api/connectors/${connector.type}/sync`, {
// method: "POST",
// headers: {
// "Content-Type": "application/json",
// },
// body: JSON.stringify(syncBody),
// });
const result = await response.json();
// const result = await response.json();
if (response.status === 201) {
const taskId = result.task_id;
if (taskId) {
addTask(taskId);
setSyncResults((prev) => ({
...prev,
[connector.id]: {
processed: 0,
total: result.total_files || 0,
},
}));
}
} else if (response.ok) {
setSyncResults((prev) => ({ ...prev, [connector.id]: result }));
// Note: Stats will auto-refresh via task completion watcher for async syncs
} else {
console.error("Sync failed:", result.error);
}
} catch (error) {
console.error("Sync error:", error);
} finally {
setIsSyncing(null);
}
};
// if (response.status === 201) {
// const taskId = result.task_id;
// if (taskId) {
// addTask(taskId);
// setSyncResults(prev => ({
// ...prev,
// [connector.id]: {
// processed: 0,
// total: result.total_files || 0,
// },
// }));
// }
// } else if (response.ok) {
// setSyncResults(prev => ({ ...prev, [connector.id]: result }));
// // Note: Stats will auto-refresh via task completion watcher for async syncs
// } else {
// console.error("Sync failed:", result.error);
// }
// } catch (error) {
// console.error("Sync error:", error);
// } finally {
// setIsSyncing(null);
// }
// };
const getStatusBadge = (status: Connector["status"]) => {
switch (status) {
case "connected":
return (
<Badge
variant="default"
className="bg-green-500/20 text-green-400 border-green-500/30"
>
Connected
</Badge>
);
return <div className="h-2 w-2 bg-green-500 rounded-full" />;
case "connecting":
return (
<Badge
variant="secondary"
className="bg-yellow-500/20 text-yellow-400 border-yellow-500/30"
>
Connecting...
</Badge>
);
return <div className="h-2 w-2 bg-yellow-500 rounded-full" />;
case "error":
return <Badge variant="destructive">Error</Badge>;
return <div className="h-2 w-2 bg-red-500 rounded-full" />;
default:
return (
<Badge
variant="outline"
className="bg-muted/20 text-muted-foreground border-muted whitespace-nowrap"
>
Not Connected
</Badge>
);
return <div className="h-2 w-2 bg-muted rounded-full" />;
}
};
const navigateToKnowledgePage = (connector: Connector) => {
const provider = connector.type.replace(/-/g, "_");
router.push(`/upload/${provider}`);
};
// Check connector status on mount and when returning from OAuth
useEffect(() => {
if (isAuthenticated) {
@ -609,166 +622,172 @@ function KnowledgeSourcesPage() {
{/* Connectors Section */}
<div className="space-y-6">
<div>
<h2 className="text-2xl font-semibold tracking-tight mb-2">
<h2 className="text-lg font-semibold tracking-tight mb-2">
Cloud Connectors
</h2>
</div>
{/* Conditional Sync Settings or No-Auth Message */}
{isNoAuthMode ? (
<Card className="border-yellow-500/50 bg-yellow-500/5">
<CardHeader>
<CardTitle className="text-lg text-yellow-600">
Cloud connectors are only available with auth mode enabled
</CardTitle>
<CardDescription className="text-sm">
Please provide the following environment variables and restart:
</CardDescription>
</CardHeader>
<CardContent>
<div className="bg-muted rounded-md p-4 font-mono text-sm">
<div className="text-muted-foreground mb-2">
# make here https://console.cloud.google.com/apis/credentials
{
isNoAuthMode ? (
<Card className="border-yellow-500/50 bg-yellow-500/5">
<CardHeader>
<CardTitle className="text-lg text-yellow-600">
Cloud connectors are only available with auth mode enabled
</CardTitle>
<CardDescription className="text-sm">
Please provide the following environment variables and
restart:
</CardDescription>
</CardHeader>
<CardContent>
<div className="bg-muted rounded-md p-4 font-mono text-sm">
<div className="text-muted-foreground mb-2">
# make here
https://console.cloud.google.com/apis/credentials
</div>
<div>GOOGLE_OAUTH_CLIENT_ID=</div>
<div>GOOGLE_OAUTH_CLIENT_SECRET=</div>
</div>
<div>GOOGLE_OAUTH_CLIENT_ID=</div>
<div>GOOGLE_OAUTH_CLIENT_SECRET=</div>
</div>
</CardContent>
</Card>
) : (
<div className="flex items-center justify-between py-4">
<div>
<h3 className="text-lg font-medium">Sync Settings</h3>
<p className="text-sm text-muted-foreground">
Configure how many files to sync when manually triggering a sync
</p>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center space-x-2">
<Checkbox
id="syncAllFiles"
checked={syncAllFiles}
onCheckedChange={(checked) => {
setSyncAllFiles(!!checked);
if (checked) {
setMaxFiles(0);
} else {
setMaxFiles(10);
}
}}
/>
<Label
htmlFor="syncAllFiles"
className="font-medium whitespace-nowrap"
>
Sync all files
</Label>
</div>
<Label
htmlFor="maxFiles"
className="font-medium whitespace-nowrap"
>
Max files per sync:
</Label>
<div className="relative">
<Input
id="maxFiles"
type="number"
value={syncAllFiles ? 0 : maxFiles}
onChange={(e) => setMaxFiles(parseInt(e.target.value) || 10)}
disabled={syncAllFiles}
className="w-16 min-w-16 max-w-16 flex-shrink-0 disabled:opacity-50 disabled:cursor-not-allowed"
min="1"
max="100"
title={
syncAllFiles
? "Disabled when 'Sync all files' is checked"
: "Leave blank or set to 0 for unlimited"
}
/>
</div>
</div>
</div>
)}
</CardContent>
</Card>
) : null
// <div className="flex items-center justify-between py-4">
// <div>
// <h3 className="text-lg font-medium">Sync Settings</h3>
// <p className="text-sm text-muted-foreground">
// Configure how many files to sync when manually triggering a sync
// </p>
// </div>
// <div className="flex items-center gap-4">
// <div className="flex items-center space-x-2">
// <Checkbox
// id="syncAllFiles"
// checked={syncAllFiles}
// onCheckedChange={checked => {
// setSyncAllFiles(!!checked);
// if (checked) {
// setMaxFiles(0);
// } else {
// setMaxFiles(10);
// }
// }}
// />
// <Label
// htmlFor="syncAllFiles"
// className="font-medium whitespace-nowrap"
// >
// Sync all files
// </Label>
// </div>
// <Label
// htmlFor="maxFiles"
// className="font-medium whitespace-nowrap"
// >
// Max files per sync:
// </Label>
// <div className="relative">
// <Input
// id="maxFiles"
// type="number"
// value={syncAllFiles ? 0 : maxFiles}
// onChange={e => setMaxFiles(parseInt(e.target.value) || 10)}
// disabled={syncAllFiles}
// className="w-16 min-w-16 max-w-16 flex-shrink-0 disabled:opacity-50 disabled:cursor-not-allowed"
// min="1"
// max="100"
// title={
// syncAllFiles
// ? "Disabled when 'Sync all files' is checked"
// : "Leave blank or set to 0 for unlimited"
// }
// />
// </div>
// </div>
// </div>
}
{/* Connectors Grid */}
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{connectors.map((connector) => (
<Card key={connector.id} className="relative flex flex-col">
<CardHeader>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
{connector.icon}
<div>
<CardTitle className="text-lg">
{DEFAULT_CONNECTORS.map((connector) => {
const actualConnector = connectors.find(
(c) => c.id === connector.id,
);
return (
<Card key={connector.id} className="relative flex flex-col">
<CardHeader>
<div className="flex flex-col items-start justify-between">
<div className="flex flex-col gap-3">
<div className="mb-1">
<div
className={`w-8 h-8 ${
actualConnector ? "bg-white" : "bg-muted grayscale"
} rounded flex items-center justify-center`}
>
{connector.icon}
</div>
</div>
<CardTitle className="flex flex-row items-center gap-2">
{connector.name}
{actualConnector &&
getStatusBadge(actualConnector.status)}
</CardTitle>
<CardDescription className="text-sm">
{connector.description}
<CardDescription className="text-[13px]">
{actualConnector?.description
? `${actualConnector.name} is configured.`
: connector.description}
</CardDescription>
</div>
</div>
{getStatusBadge(connector.status)}
</div>
</CardHeader>
<CardContent className="flex-1 flex flex-col justify-end space-y-4">
{connector.status === "connected" ? (
<div className="space-y-3">
<Button
onClick={() => handleSync(connector)}
disabled={isSyncing === connector.id}
className="w-full"
variant="outline"
>
{isSyncing === connector.id ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Syncing...
</>
) : (
<>
<RefreshCw className="mr-2 h-4 w-4" />
Sync Now
</>
)}
</Button>
</CardHeader>
<CardContent className="flex-1 flex flex-col justify-end space-y-4">
{actualConnector?.status === "connected" ? (
<div className="space-y-3">
<Button
onClick={() => navigateToKnowledgePage(connector)}
disabled={isSyncing === connector.id}
className="w-full cursor-pointer"
size="sm"
>
<Plus className="h-4 w-4" />
Add Knowledge
</Button>
{syncResults[connector.id] && (
<div className="text-xs text-muted-foreground bg-muted/50 p-2 rounded">
<div>
Processed: {syncResults[connector.id]?.processed || 0}
{syncResults[connector.id] && (
<div className="text-xs text-muted-foreground bg-muted/50 p-2 rounded">
<div>
Processed:{" "}
{syncResults[connector.id]?.processed || 0}
</div>
<div>
Added: {syncResults[connector.id]?.added || 0}
</div>
{syncResults[connector.id]?.errors && (
<div>
Errors: {syncResults[connector.id]?.errors}
</div>
)}
</div>
<div>
Added: {syncResults[connector.id]?.added || 0}
</div>
{syncResults[connector.id]?.errors && (
<div>Errors: {syncResults[connector.id]?.errors}</div>
)}
</div>
)}
</div>
) : (
<Button
onClick={() => handleConnect(connector)}
disabled={isConnecting === connector.id}
className="w-full"
>
{isConnecting === connector.id ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Connecting...
</>
) : (
<>
<PlugZap className="mr-2 h-4 w-4" />
Connect
</>
)}
</Button>
)}
</CardContent>
</Card>
))}
)}
</div>
) : (
<div className="text-[13px] text-muted-foreground">
<p>
See our{" "}
<Link
className="text-accent-pink-foreground"
href="https://github.com/langflow-ai/openrag/pull/96/files#diff-06889aa94ccf8dac64e70c8cc30a2ceed32cc3c0c2c14a6ff0336fe882a9c2ccR41"
>
Cloud Connectors installation guide
</Link>{" "}
for more detail.
</p>
</div>
)}
</CardContent>
</Card>
);
})}
</div>
</div>
{/* Agent Behavior Section */}
@ -847,63 +866,47 @@ function KnowledgeSourcesPage() {
<CardContent>
<div className="space-y-6">
<div className="space-y-2">
<LabelWrapper
helperText="Model used for chat"
id="model-select"
<LabelWrapper
label="Language model"
required
helperText="Model used for chat"
id="embedding-model"
required={true}
>
<Select
value={
settings.agent?.llm_model ||
modelsData?.language_models?.find((m) => m.default)
?.value ||
"gpt-4"
}
<ModelSelector
options={modelsData?.language_models || []}
noOptionsPlaceholder={modelsData ? "No language models detected." : "Loading models..."}
icon={<OpenAILogo className="w-4 h-4" />}
value={modelsData ? settings.agent?.llm_model || "" : ""}
onValueChange={handleModelChange}
>
<SelectTrigger id="model-select">
<SelectValue placeholder="Select a model" />
</SelectTrigger>
<SelectContent>
<ModelSelectItems
models={modelsData?.language_models}
fallbackModels={
getFallbackModels(currentProvider).language
}
provider={currentProvider}
/>
</SelectContent>
</Select>
/>
</LabelWrapper>
</div>
<div className="space-y-2">
<Label htmlFor="system-prompt" className="text-base font-medium">
Agent Instructions
</Label>
<Textarea
id="system-prompt"
placeholder="Enter your agent instructions here..."
value={systemPrompt}
onChange={(e) => setSystemPrompt(e.target.value)}
rows={6}
className={`resize-none ${
systemPrompt.length > MAX_SYSTEM_PROMPT_CHARS
? "border-red-500 focus:border-red-500"
: ""
}`}
/>
<div className="flex justify-start">
<span
className={`text-xs ${
<LabelWrapper label="Agent Instructions" id="system-prompt">
<Textarea
id="system-prompt"
placeholder="Enter your agent instructions here..."
value={systemPrompt}
onChange={(e) => setSystemPrompt(e.target.value)}
rows={6}
className={`resize-none ${
systemPrompt.length > MAX_SYSTEM_PROMPT_CHARS
? "text-red-500"
: "text-muted-foreground"
? "border-red-500 focus:border-red-500"
: ""
}`}
>
{systemPrompt.length}/{MAX_SYSTEM_PROMPT_CHARS} characters
</span>
</div>
/>
<div className="flex justify-start">
<span
className={`text-xs ${
systemPrompt.length > MAX_SYSTEM_PROMPT_CHARS
? "text-red-500"
: "text-muted-foreground"
}`}
>
{systemPrompt.length}/{MAX_SYSTEM_PROMPT_CHARS} characters
</span>
</div>
</LabelWrapper>
</div>
<div className="flex justify-end pt-2">
<Button
@ -935,7 +938,9 @@ function KnowledgeSourcesPage() {
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-lg mb-4">Knowledge</CardTitle>
<CardTitle className="text-lg mb-4">
Knowledge ingestion and retrieval
</CardTitle>
<CardDescription>
Quick knowledge settings. Edit in Langflow for full control.
</CardDescription>
@ -1046,47 +1051,94 @@ function KnowledgeSourcesPage() {
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="chunk-size" className="text-base font-medium">
Chunk size
</Label>
<div className="relative">
<Input
id="chunk-size"
type="number"
min="1"
value={chunkSize}
onChange={(e) => handleChunkSizeChange(e.target.value)}
className="w-full pr-20"
/>
<div className="absolute inset-y-0 right-0 flex items-center pr-8 pointer-events-none">
<span className="text-sm text-muted-foreground">
characters
</span>
<LabelWrapper id="chunk-size" label="Chunk size">
<div className="relative">
<Input
id="chunk-size"
type="number"
min="1"
value={chunkSize}
onChange={(e) => handleChunkSizeChange(e.target.value)}
className="w-full pr-20 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
/>
<div className="absolute inset-y-0 right-0 flex items-center">
<span className="text-sm text-placeholder-foreground mr-4 pointer-events-none">
characters
</span>
<div className="flex flex-col">
<Button
aria-label="Increase value"
className="h-5 rounded-l-none rounded-br-none border-input border-b-[0.5px] focus-visible:relative"
variant="outline"
size="iconSm"
onClick={() =>
handleChunkSizeChange((chunkSize + 1).toString())
}
>
<Plus className="text-muted-foreground" size={8} />
</Button>
<Button
aria-label="Decrease value"
className="h-5 rounded-l-none rounded-tr-none border-input border-t-[0.5px] focus-visible:relative"
variant="outline"
size="iconSm"
onClick={() =>
handleChunkSizeChange((chunkSize - 1).toString())
}
>
<Minus className="text-muted-foreground" size={8} />
</Button>
</div>
</div>
</div>
</div>
</LabelWrapper>
</div>
<div className="space-y-2">
<Label
htmlFor="chunk-overlap"
className="text-base font-medium"
>
Chunk overlap
</Label>
<div className="relative">
<Input
id="chunk-overlap"
type="number"
min="0"
value={chunkOverlap}
onChange={(e) => handleChunkOverlapChange(e.target.value)}
className="w-full pr-20"
/>
<div className="absolute inset-y-0 right-0 flex items-center pr-8 pointer-events-none">
<span className="text-sm text-muted-foreground">
characters
</span>
<LabelWrapper id="chunk-overlap" label="Chunk overlap">
<div className="relative">
<Input
id="chunk-overlap"
type="number"
min="0"
value={chunkOverlap}
onChange={(e) => handleChunkOverlapChange(e.target.value)}
className="w-full pr-20 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
/>
<div className="absolute inset-y-0 right-0 flex items-center">
<span className="text-sm text-placeholder-foreground mr-4 pointer-events-none">
characters
</span>
<div className="flex flex-col">
<Button
aria-label="Increase value"
className="h-5 rounded-l-none rounded-br-none border-input border-b-[0.5px] focus-visible:relative"
variant="outline"
size="iconSm"
onClick={() =>
handleChunkOverlapChange(
(chunkOverlap + 1).toString(),
)
}
>
<Plus className="text-muted-foreground" size={8} />
</Button>
<Button
aria-label="Decrease value"
className="h-5 rounded-l-none rounded-tr-none border-input border-t-[0.5px] focus-visible:relative"
variant="outline"
size="iconSm"
onClick={() =>
handleChunkOverlapChange(
(chunkOverlap - 1).toString(),
)
}
>
<Minus className="text-muted-foreground" size={8} />
</Button>
</div>
</div>
</div>
</div>
</LabelWrapper>
</div>
</div>
<div className="">

View file

@ -1,386 +1,378 @@
"use client";
import { useState, useEffect } from "react";
import { AlertCircle, ArrowLeft } from "lucide-react";
import { useParams, useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import { ArrowLeft, AlertCircle } from "lucide-react";
import { UnifiedCloudPicker, CloudFile } from "@/components/cloud-picker";
import { useEffect, useState } from "react";
import { type CloudFile, UnifiedCloudPicker } from "@/components/cloud-picker";
import type { IngestSettings } from "@/components/cloud-picker/types";
import { useTask } from "@/contexts/task-context";
import { Button } from "@/components/ui/button";
import { Toast } from "@/components/ui/toast";
import { useTask } from "@/contexts/task-context";
// CloudFile interface is now imported from the unified cloud picker
interface CloudConnector {
id: string;
name: string;
description: string;
status: "not_connected" | "connecting" | "connected" | "error";
type: string;
connectionId?: string;
clientId: string;
hasAccessToken: boolean;
accessTokenError?: string;
id: string;
name: string;
description: string;
status: "not_connected" | "connecting" | "connected" | "error";
type: string;
connectionId?: string;
clientId: string;
hasAccessToken: boolean;
accessTokenError?: string;
}
export default function UploadProviderPage() {
const params = useParams();
const router = useRouter();
const provider = params.provider as string;
const { addTask, tasks } = useTask();
const params = useParams();
const router = useRouter();
const provider = params.provider as string;
const { addTask, tasks } = useTask();
const [connector, setConnector] = useState<CloudConnector | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [accessToken, setAccessToken] = useState<string | null>(null);
const [selectedFiles, setSelectedFiles] = useState<CloudFile[]>([]);
const [isIngesting, setIsIngesting] = useState<boolean>(false);
const [currentSyncTaskId, setCurrentSyncTaskId] = useState<string | null>(
null
);
const [showSuccessToast, setShowSuccessToast] = useState(false);
const [ingestSettings, setIngestSettings] = useState<IngestSettings>({
chunkSize: 1000,
chunkOverlap: 200,
ocr: false,
pictureDescriptions: false,
embeddingModel: "text-embedding-3-small",
});
const [connector, setConnector] = useState<CloudConnector | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [accessToken, setAccessToken] = useState<string | null>(null);
const [selectedFiles, setSelectedFiles] = useState<CloudFile[]>([]);
const [isIngesting, setIsIngesting] = useState<boolean>(false);
const [currentSyncTaskId, setCurrentSyncTaskId] = useState<string | null>(
null,
);
const [ingestSettings, setIngestSettings] = useState<IngestSettings>({
chunkSize: 1000,
chunkOverlap: 200,
ocr: false,
pictureDescriptions: false,
embeddingModel: "text-embedding-3-small",
});
useEffect(() => {
const fetchConnectorInfo = async () => {
setIsLoading(true);
setError(null);
useEffect(() => {
const fetchConnectorInfo = async () => {
setIsLoading(true);
setError(null);
try {
// Fetch available connectors to validate the provider
const connectorsResponse = await fetch("/api/connectors");
if (!connectorsResponse.ok) {
throw new Error("Failed to load connectors");
}
try {
// Fetch available connectors to validate the provider
const connectorsResponse = await fetch("/api/connectors");
if (!connectorsResponse.ok) {
throw new Error("Failed to load connectors");
}
const connectorsResult = await connectorsResponse.json();
const providerInfo = connectorsResult.connectors[provider];
const connectorsResult = await connectorsResponse.json();
const providerInfo = connectorsResult.connectors[provider];
if (!providerInfo || !providerInfo.available) {
setError(
`Cloud provider "${provider}" is not available or configured.`
);
return;
}
if (!providerInfo || !providerInfo.available) {
setError(
`Cloud provider "${provider}" is not available or configured.`,
);
return;
}
// Check connector status
const statusResponse = await fetch(
`/api/connectors/${provider}/status`
);
if (!statusResponse.ok) {
throw new Error(`Failed to check ${provider} status`);
}
// Check connector status
const statusResponse = await fetch(
`/api/connectors/${provider}/status`,
);
if (!statusResponse.ok) {
throw new Error(`Failed to check ${provider} status`);
}
const statusData = await statusResponse.json();
const connections = statusData.connections || [];
const activeConnection = connections.find(
(conn: { is_active: boolean; connection_id: string }) =>
conn.is_active
);
const isConnected = activeConnection !== undefined;
const statusData = await statusResponse.json();
const connections = statusData.connections || [];
const activeConnection = connections.find(
(conn: { is_active: boolean; connection_id: string }) =>
conn.is_active,
);
const isConnected = activeConnection !== undefined;
let hasAccessToken = false;
let accessTokenError: string | undefined = undefined;
let hasAccessToken = false;
let accessTokenError: string | undefined;
// Try to get access token for connected connectors
if (isConnected && activeConnection) {
try {
const tokenResponse = await fetch(
`/api/connectors/${provider}/token?connection_id=${activeConnection.connection_id}`
);
if (tokenResponse.ok) {
const tokenData = await tokenResponse.json();
if (tokenData.access_token) {
hasAccessToken = true;
setAccessToken(tokenData.access_token);
}
} else {
const errorData = await tokenResponse
.json()
.catch(() => ({ error: "Token unavailable" }));
accessTokenError = errorData.error || "Access token unavailable";
}
} catch {
accessTokenError = "Failed to fetch access token";
}
}
// Try to get access token for connected connectors
if (isConnected && activeConnection) {
try {
const tokenResponse = await fetch(
`/api/connectors/${provider}/token?connection_id=${activeConnection.connection_id}`,
);
if (tokenResponse.ok) {
const tokenData = await tokenResponse.json();
if (tokenData.access_token) {
hasAccessToken = true;
setAccessToken(tokenData.access_token);
}
} else {
const errorData = await tokenResponse
.json()
.catch(() => ({ error: "Token unavailable" }));
accessTokenError = errorData.error || "Access token unavailable";
}
} catch {
accessTokenError = "Failed to fetch access token";
}
}
setConnector({
id: provider,
name: providerInfo.name,
description: providerInfo.description,
status: isConnected ? "connected" : "not_connected",
type: provider,
connectionId: activeConnection?.connection_id,
clientId: activeConnection?.client_id,
hasAccessToken,
accessTokenError,
});
} catch (error) {
console.error("Failed to load connector info:", error);
setError(
error instanceof Error
? error.message
: "Failed to load connector information"
);
} finally {
setIsLoading(false);
}
};
setConnector({
id: provider,
name: providerInfo.name,
description: providerInfo.description,
status: isConnected ? "connected" : "not_connected",
type: provider,
connectionId: activeConnection?.connection_id,
clientId: activeConnection?.client_id,
hasAccessToken,
accessTokenError,
});
} catch (error) {
console.error("Failed to load connector info:", error);
setError(
error instanceof Error
? error.message
: "Failed to load connector information",
);
} finally {
setIsLoading(false);
}
};
if (provider) {
fetchConnectorInfo();
}
}, [provider]);
if (provider) {
fetchConnectorInfo();
}
}, [provider]);
// Watch for sync task completion and redirect
useEffect(() => {
if (!currentSyncTaskId) return;
// Watch for sync task completion and redirect
useEffect(() => {
if (!currentSyncTaskId) return;
const currentTask = tasks.find(task => task.task_id === currentSyncTaskId);
const currentTask = tasks.find(
(task) => task.task_id === currentSyncTaskId,
);
if (currentTask && currentTask.status === "completed") {
// Task completed successfully, show toast and redirect
setIsIngesting(false);
setShowSuccessToast(true);
setTimeout(() => {
router.push("/knowledge");
}, 2000); // 2 second delay to let user see toast
} else if (currentTask && currentTask.status === "failed") {
// Task failed, clear the tracking but don't redirect
setIsIngesting(false);
setCurrentSyncTaskId(null);
}
}, [tasks, currentSyncTaskId, router]);
if (currentTask && currentTask.status === "completed") {
// Task completed successfully, show toast and redirect
setIsIngesting(false);
setTimeout(() => {
router.push("/knowledge");
}, 2000); // 2 second delay to let user see toast
} else if (currentTask && currentTask.status === "failed") {
// Task failed, clear the tracking but don't redirect
setIsIngesting(false);
setCurrentSyncTaskId(null);
}
}, [tasks, currentSyncTaskId, router]);
const handleFileSelected = (files: CloudFile[]) => {
setSelectedFiles(files);
console.log(`Selected ${files.length} files from ${provider}:`, files);
// You can add additional handling here like triggering sync, etc.
};
const handleFileSelected = (files: CloudFile[]) => {
setSelectedFiles(files);
console.log(`Selected ${files.length} files from ${provider}:`, files);
// You can add additional handling here like triggering sync, etc.
};
const handleSync = async (connector: CloudConnector) => {
if (!connector.connectionId || selectedFiles.length === 0) return;
const handleSync = async (connector: CloudConnector) => {
if (!connector.connectionId || selectedFiles.length === 0) return;
setIsIngesting(true);
setIsIngesting(true);
try {
const syncBody: {
connection_id: string;
max_files?: number;
selected_files?: string[];
settings?: IngestSettings;
} = {
connection_id: connector.connectionId,
selected_files: selectedFiles.map(file => file.id),
settings: ingestSettings,
};
try {
const syncBody: {
connection_id: string;
max_files?: number;
selected_files?: string[];
settings?: IngestSettings;
} = {
connection_id: connector.connectionId,
selected_files: selectedFiles.map((file) => file.id),
settings: ingestSettings,
};
const response = await fetch(`/api/connectors/${connector.type}/sync`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(syncBody),
});
const response = await fetch(`/api/connectors/${connector.type}/sync`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(syncBody),
});
const result = await response.json();
const result = await response.json();
if (response.status === 201) {
const taskIds = result.task_ids;
if (taskIds && taskIds.length > 0) {
const taskId = taskIds[0]; // Use the first task ID
addTask(taskId);
setCurrentSyncTaskId(taskId);
}
} else {
console.error("Sync failed:", result.error);
}
} catch (error) {
console.error("Sync error:", error);
setIsIngesting(false);
}
};
if (response.status === 201) {
const taskIds = result.task_ids;
if (taskIds && taskIds.length > 0) {
const taskId = taskIds[0]; // Use the first task ID
addTask(taskId);
setCurrentSyncTaskId(taskId);
}
} else {
console.error("Sync failed:", result.error);
}
} catch (error) {
console.error("Sync error:", error);
setIsIngesting(false);
}
};
const getProviderDisplayName = () => {
const nameMap: { [key: string]: string } = {
google_drive: "Google Drive",
onedrive: "OneDrive",
sharepoint: "SharePoint",
};
return nameMap[provider] || provider;
};
const getProviderDisplayName = () => {
const nameMap: { [key: string]: string } = {
google_drive: "Google Drive",
onedrive: "OneDrive",
sharepoint: "SharePoint",
};
return nameMap[provider] || provider;
};
if (isLoading) {
return (
<div className="container mx-auto p-6">
<div className="flex items-center justify-center py-12">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
<p>Loading {getProviderDisplayName()} connector...</p>
</div>
</div>
</div>
);
}
if (isLoading) {
return (
<div className="container mx-auto p-6">
<div className="flex items-center justify-center py-12">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
<p>Loading {getProviderDisplayName()} connector...</p>
</div>
</div>
</div>
);
}
if (error || !connector) {
return (
<div className="container mx-auto p-6">
<div className="mb-6">
<Button
variant="ghost"
onClick={() => router.back()}
className="mb-4"
>
<ArrowLeft className="h-4 w-4 mr-2" />
Back
</Button>
</div>
if (error || !connector) {
return (
<div className="container mx-auto p-6">
<div className="mb-6">
<Button
variant="ghost"
onClick={() => router.back()}
className="mb-4"
>
<ArrowLeft className="h-4 w-4 mr-2" />
Back
</Button>
</div>
<div className="flex items-center justify-center py-12">
<div className="text-center max-w-md">
<AlertCircle className="h-12 w-12 text-red-500 mx-auto mb-4" />
<h2 className="text-xl font-semibold mb-2">
Provider Not Available
</h2>
<p className="text-muted-foreground mb-4">{error}</p>
<Button onClick={() => router.push("/settings")}>
Configure Connectors
</Button>
</div>
</div>
</div>
);
}
<div className="flex items-center justify-center py-12">
<div className="text-center max-w-md">
<AlertCircle className="h-12 w-12 text-red-500 mx-auto mb-4" />
<h2 className="text-xl font-semibold mb-2">
Provider Not Available
</h2>
<p className="text-muted-foreground mb-4">{error}</p>
<Button onClick={() => router.push("/settings")}>
Configure Connectors
</Button>
</div>
</div>
</div>
);
}
if (connector.status !== "connected") {
return (
<div className="container mx-auto p-6">
<div className="mb-6">
<Button
variant="ghost"
onClick={() => router.back()}
className="mb-4"
>
<ArrowLeft className="h-4 w-4 mr-2" />
Back
</Button>
</div>
if (connector.status !== "connected") {
return (
<div className="container mx-auto p-6">
<div className="mb-6">
<Button
variant="ghost"
onClick={() => router.back()}
className="mb-4"
>
<ArrowLeft className="h-4 w-4 mr-2" />
Back
</Button>
</div>
<div className="flex items-center justify-center py-12">
<div className="text-center max-w-md">
<AlertCircle className="h-12 w-12 text-yellow-500 mx-auto mb-4" />
<h2 className="text-xl font-semibold mb-2">
{connector.name} Not Connected
</h2>
<p className="text-muted-foreground mb-4">
You need to connect your {connector.name} account before you can
select files.
</p>
<Button onClick={() => router.push("/settings")}>
Connect {connector.name}
</Button>
</div>
</div>
</div>
);
}
<div className="flex items-center justify-center py-12">
<div className="text-center max-w-md">
<AlertCircle className="h-12 w-12 text-yellow-500 mx-auto mb-4" />
<h2 className="text-xl font-semibold mb-2">
{connector.name} Not Connected
</h2>
<p className="text-muted-foreground mb-4">
You need to connect your {connector.name} account before you can
select files.
</p>
<Button onClick={() => router.push("/settings")}>
Connect {connector.name}
</Button>
</div>
</div>
</div>
);
}
if (!connector.hasAccessToken) {
return (
<div className="container mx-auto p-6">
<div className="mb-6">
<Button
variant="ghost"
onClick={() => router.back()}
className="mb-4"
>
<ArrowLeft className="h-4 w-4 mr-2" />
Back
</Button>
</div>
if (!connector.hasAccessToken) {
return (
<div className="container mx-auto p-6">
<div className="mb-6">
<Button
variant="ghost"
onClick={() => router.back()}
className="mb-4"
>
<ArrowLeft className="h-4 w-4 mr-2" />
Back
</Button>
</div>
<div className="flex items-center justify-center py-12">
<div className="text-center max-w-md">
<AlertCircle className="h-12 w-12 text-red-500 mx-auto mb-4" />
<h2 className="text-xl font-semibold mb-2">
Access Token Required
</h2>
<p className="text-muted-foreground mb-4">
{connector.accessTokenError ||
`Unable to get access token for ${connector.name}. Try reconnecting your account.`}
</p>
<Button onClick={() => router.push("/settings")}>
Reconnect {connector.name}
</Button>
</div>
</div>
</div>
);
}
<div className="flex items-center justify-center py-12">
<div className="text-center max-w-md">
<AlertCircle className="h-12 w-12 text-red-500 mx-auto mb-4" />
<h2 className="text-xl font-semibold mb-2">
Access Token Required
</h2>
<p className="text-muted-foreground mb-4">
{connector.accessTokenError ||
`Unable to get access token for ${connector.name}. Try reconnecting your account.`}
</p>
<Button onClick={() => router.push("/settings")}>
Reconnect {connector.name}
</Button>
</div>
</div>
</div>
);
}
return (
<div className="container mx-auto max-w-3xl p-6">
<div className="mb-6 flex gap-2 items-center">
<Button variant="ghost" onClick={() => router.back()}>
<ArrowLeft className="h-4 w-4 scale-125" />
</Button>
<h2 className="text-2xl font-bold">
Add from {getProviderDisplayName()}
</h2>
</div>
return (
<div className="container mx-auto max-w-3xl p-6">
<div className="mb-6 flex gap-2 items-center">
<Button variant="ghost" onClick={() => router.back()}>
<ArrowLeft className="h-4 w-4 scale-125" />
</Button>
<h2 className="text-2xl font-bold">
Add from {getProviderDisplayName()}
</h2>
</div>
<div className="max-w-3xl mx-auto">
<UnifiedCloudPicker
provider={
connector.type as "google_drive" | "onedrive" | "sharepoint"
}
onFileSelected={handleFileSelected}
selectedFiles={selectedFiles}
isAuthenticated={true}
accessToken={accessToken || undefined}
clientId={connector.clientId}
onSettingsChange={setIngestSettings}
/>
</div>
<div className="max-w-3xl mx-auto">
<UnifiedCloudPicker
provider={
connector.type as "google_drive" | "onedrive" | "sharepoint"
}
onFileSelected={handleFileSelected}
selectedFiles={selectedFiles}
isAuthenticated={true}
accessToken={accessToken || undefined}
clientId={connector.clientId}
onSettingsChange={setIngestSettings}
/>
</div>
<div className="max-w-3xl mx-auto mt-6">
<div className="flex justify-between gap-3 mb-4">
<Button
variant="ghost"
className=" border bg-transparent border-border rounded-lg text-secondary-foreground"
onClick={() => router.back()}
>
Back
</Button>
<Button
variant="secondary"
onClick={() => handleSync(connector)}
disabled={selectedFiles.length === 0 || isIngesting}
>
{isIngesting ? (
<>Ingesting {selectedFiles.length} Files...</>
) : (
<>Start ingest</>
)}
</Button>
</div>
</div>
{/* Success toast notification */}
<Toast
message="Ingested successfully!."
show={showSuccessToast}
onHide={() => setShowSuccessToast(false)}
duration={20000}
/>
</div>
);
<div className="max-w-3xl mx-auto mt-6">
<div className="flex justify-between gap-3 mb-4">
<Button
variant="ghost"
className=" border bg-transparent border-border rounded-lg text-secondary-foreground"
onClick={() => router.back()}
>
Back
</Button>
<Button
variant="secondary"
onClick={() => handleSync(connector)}
disabled={selectedFiles.length === 0 || isIngesting}
>
{isIngesting ? (
<>Ingesting {selectedFiles.length} Files...</>
) : (
<>Start ingest</>
)}
</Button>
</div>
</div>
</div>
);
}

View file

@ -2,7 +2,10 @@
import { Bell, Loader2 } from "lucide-react";
import { usePathname } from "next/navigation";
import { useGetConversationsQuery, type ChatConversation } from "@/app/api/queries/useGetConversationsQuery";
import {
useGetConversationsQuery,
type ChatConversation,
} from "@/app/api/queries/useGetConversationsQuery";
import { useGetSettingsQuery } from "@/app/api/queries/useGetSettingsQuery";
import { KnowledgeFilterPanel } from "@/components/knowledge-filter-panel";
import Logo from "@/components/logo/logo";
@ -16,6 +19,7 @@ import { useKnowledgeFilter } from "@/contexts/knowledge-filter-context";
// import { GitHubStarButton } from "@/components/github-star-button"
// import { DiscordLink } from "@/components/discord-link"
import { useTask } from "@/contexts/task-context";
import { cn } from "@/lib/utils";
export function LayoutWrapper({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
@ -48,12 +52,16 @@ export function LayoutWrapper({ children }: { children: React.ReactNode }) {
const authPaths = ["/login", "/auth/callback", "/onboarding"];
const isAuthPage = authPaths.includes(pathname);
// List of paths with smaller max-width
const smallWidthPaths = ["/settings", "/settings/connector/new"];
const isSmallWidthPath = smallWidthPaths.includes(pathname);
// Calculate active tasks for the bell icon
const activeTasks = tasks.filter(
(task) =>
task.status === "pending" ||
task.status === "running" ||
task.status === "processing",
task.status === "processing"
);
// Show loading state when backend isn't ready
@ -139,7 +147,14 @@ export function LayoutWrapper({ children }: { children: React.ReactNode }) {
"md:pr-0" // Neither open: 24px
}`}
>
<div className="container py-6 lg:py-8 px-4 lg:px-6">{children}</div>
<div
className={cn(
"py-6 lg:py-8 px-4 lg:px-6",
isSmallWidthPath ? "max-w-[850px]" : "container"
)}
>
{children}
</div>
</main>
<TaskNotificationMenu />
<KnowledgeFilterPanel />

View file

@ -197,7 +197,7 @@ export function TaskProvider({ children }: { children: React.ReactNode }) {
newTask.status === "completed"
) {
// Task just completed - show success toast
toast.success("Task completed successfully!", {
toast.success("Task completed successfully", {
description: `Task ${newTask.task_id} has finished processing.`,
action: {
label: "View",

View file

@ -4,7 +4,6 @@ import tailwindcssTypography from "@tailwindcss/typography";
import { fontFamily } from "tailwindcss/defaultTheme";
import plugin from "tailwindcss/plugin";
import tailwindcssAnimate from "tailwindcss-animate";
import tailwindcssLineClamp from "@tailwindcss/line-clamp";
const config = {
darkMode: ["class"],
@ -167,7 +166,6 @@ const config = {
},
plugins: [
tailwindcssAnimate,
tailwindcssLineClamp,
tailwindcssForms({
strategy: "class",
}),

View file

@ -2,7 +2,12 @@
from starlette.requests import Request
from config.settings import DISABLE_INGEST_WITH_LANGFLOW
from config.settings import (
DISABLE_INGEST_WITH_LANGFLOW,
clients,
INDEX_NAME,
INDEX_BODY,
)
from utils.logging_config import get_logger
logger = get_logger(__name__)
@ -12,19 +17,19 @@ class ConnectorRouter:
"""
Router that automatically chooses between LangflowConnectorService and ConnectorService
based on the DISABLE_INGEST_WITH_LANGFLOW configuration.
- If DISABLE_INGEST_WITH_LANGFLOW is False (default): uses LangflowConnectorService
- If DISABLE_INGEST_WITH_LANGFLOW is True: uses traditional ConnectorService
"""
def __init__(self, langflow_connector_service, openrag_connector_service):
self.langflow_connector_service = langflow_connector_service
self.openrag_connector_service = openrag_connector_service
logger.debug(
"ConnectorRouter initialized",
disable_langflow_ingest=DISABLE_INGEST_WITH_LANGFLOW
disable_langflow_ingest=DISABLE_INGEST_WITH_LANGFLOW,
)
def get_active_service(self):
"""Get the currently active connector service based on configuration."""
if DISABLE_INGEST_WITH_LANGFLOW:
@ -33,28 +38,32 @@ class ConnectorRouter:
else:
logger.debug("Using Langflow connector service")
return self.langflow_connector_service
# Proxy all connector service methods to the active service
async def initialize(self):
"""Initialize the active connector service."""
# Initialize OpenSearch index if using traditional OpenRAG connector service
return await self.get_active_service().initialize()
@property
def connection_manager(self):
"""Get the connection manager from the active service."""
return self.get_active_service().connection_manager
async def get_connector(self, connection_id: str):
"""Get a connector instance from the active service."""
return await self.get_active_service().get_connector(connection_id)
async def sync_specific_files(self, connection_id: str, user_id: str, file_list: list, jwt_token: str = None):
async def sync_specific_files(
self, connection_id: str, user_id: str, file_list: list, jwt_token: str = None
):
"""Sync specific files using the active service."""
return await self.get_active_service().sync_specific_files(
connection_id, user_id, file_list, jwt_token
)
def __getattr__(self, name):
"""
Proxy any other method calls to the active service.
@ -64,4 +73,6 @@ class ConnectorRouter:
if hasattr(active_service, name):
return getattr(active_service, name)
else:
raise AttributeError(f"'{type(active_service).__name__}' object has no attribute '{name}'")
raise AttributeError(
f"'{type(active_service).__name__}' object has no attribute '{name}'"
)

View file

@ -4,6 +4,7 @@ from starlette.responses import JSONResponse
from utils.container_utils import transform_localhost_url
from utils.logging_config import get_logger
from config.settings import (
DISABLE_INGEST_WITH_LANGFLOW,
LANGFLOW_URL,
LANGFLOW_CHAT_FLOW_ID,
LANGFLOW_INGEST_FLOW_ID,
@ -451,7 +452,7 @@ async def onboarding(request, flows_service):
config_updated = True
# Update knowledge settings
if "embedding_model" in body:
if "embedding_model" in body and not DISABLE_INGEST_WITH_LANGFLOW:
if (
not isinstance(body["embedding_model"], str)
or not body["embedding_model"].strip()
@ -601,11 +602,16 @@ async def onboarding(request, flows_service):
# Import here to avoid circular imports
from main import init_index
logger.info("Initializing OpenSearch index after onboarding configuration")
logger.info(
"Initializing OpenSearch index after onboarding configuration"
)
await init_index()
logger.info("OpenSearch index initialization completed successfully")
except Exception as e:
logger.error("Failed to initialize OpenSearch index after onboarding", error=str(e))
logger.error(
"Failed to initialize OpenSearch index after onboarding",
error=str(e),
)
# Don't fail the entire onboarding process if index creation fails
# The application can still work, but document operations may fail

View file

@ -73,7 +73,7 @@ class ConfigManager:
Args:
config_file: Path to configuration file. Defaults to 'config.yaml' in project root.
"""
self.config_file = Path(config_file) if config_file else Path("config.yaml")
self.config_file = Path(config_file) if config_file else Path("config/config.yaml")
self._config: Optional[OpenRAGConfig] = None
def load_config(self) -> OpenRAGConfig:

View file

@ -73,7 +73,7 @@ class GoogleDriveConnector(BaseConnector):
# Connector metadata
CONNECTOR_NAME = "Google Drive"
CONNECTOR_DESCRIPTION = "Connect your Google Drive to automatically sync documents"
CONNECTOR_DESCRIPTION = "Add knowledge from Google Drive"
CONNECTOR_ICON = "google-drive"
# Supported alias keys coming from various frontends / pickers

View file

@ -19,7 +19,7 @@ class OneDriveConnector(BaseConnector):
# Connector metadata
CONNECTOR_NAME = "OneDrive"
CONNECTOR_DESCRIPTION = "Connect to OneDrive (personal) to sync documents and files"
CONNECTOR_DESCRIPTION = "Add knowledge from OneDrive"
CONNECTOR_ICON = "onedrive"
def __init__(self, config: Dict[str, Any]):

View file

@ -20,7 +20,7 @@ class SharePointConnector(BaseConnector):
# Connector metadata
CONNECTOR_NAME = "SharePoint"
CONNECTOR_DESCRIPTION = "Connect to SharePoint to sync documents and files"
CONNECTOR_DESCRIPTION = "Add knowledge from SharePoint"
CONNECTOR_ICON = "sharepoint"
def __init__(self, config: Dict[str, Any]):

View file

@ -53,6 +53,7 @@ from auth_middleware import optional_auth, require_auth
from config.settings import (
DISABLE_INGEST_WITH_LANGFLOW,
EMBED_MODEL,
INDEX_BODY,
INDEX_NAME,
SESSION_SECRET,
clients,
@ -82,6 +83,7 @@ logger.info(
cuda_version=torch.version.cuda,
)
async def wait_for_opensearch():
"""Wait for OpenSearch to be ready with retries"""
max_retries = 30
@ -128,6 +130,34 @@ async def configure_alerting_security():
# Don't fail startup if alerting config fails
async def _ensure_opensearch_index(self):
"""Ensure OpenSearch index exists when using traditional connector service."""
try:
# Check if index already exists
if await clients.opensearch.indices.exists(index=INDEX_NAME):
logger.debug("OpenSearch index already exists", index_name=INDEX_NAME)
return
# Create the index with hard-coded INDEX_BODY (uses OpenAI embedding dimensions)
await clients.opensearch.indices.create(index=INDEX_NAME, body=INDEX_BODY)
logger.info(
"Created OpenSearch index for traditional connector service",
index_name=INDEX_NAME,
vector_dimensions=INDEX_BODY["mappings"]["properties"]["chunk_embedding"][
"dimension"
],
)
except Exception as e:
logger.error(
"Failed to initialize OpenSearch index for traditional connector service",
error=str(e),
index_name=INDEX_NAME,
)
# Don't raise the exception to avoid breaking the initialization
# The service can still function, document operations might fail later
async def init_index():
"""Initialize OpenSearch index and security roles"""
await wait_for_opensearch()
@ -141,10 +171,20 @@ async def init_index():
# Create documents index
if not await clients.opensearch.indices.exists(index=INDEX_NAME):
await clients.opensearch.indices.create(index=INDEX_NAME, body=dynamic_index_body)
logger.info("Created OpenSearch index", index_name=INDEX_NAME, embedding_model=embedding_model)
await clients.opensearch.indices.create(
index=INDEX_NAME, body=dynamic_index_body
)
logger.info(
"Created OpenSearch index",
index_name=INDEX_NAME,
embedding_model=embedding_model,
)
else:
logger.info("Index already exists, skipping creation", index_name=INDEX_NAME, embedding_model=embedding_model)
logger.info(
"Index already exists, skipping creation",
index_name=INDEX_NAME,
embedding_model=embedding_model,
)
# Create knowledge filters index
knowledge_filter_index_name = "knowledge_filters"
@ -406,6 +446,9 @@ async def startup_tasks(services):
# Index will be created after onboarding when we know the embedding model
await wait_for_opensearch()
if DISABLE_INGEST_WITH_LANGFLOW:
await _ensure_opensearch_index()
# Configure alerting security
await configure_alerting_security()
@ -1079,14 +1122,6 @@ async def create_app():
return app
async def startup():
"""Application startup tasks"""
await init_index()
# Get services from app state if needed for initialization
# services = app.state.services
# await services['connector_service'].initialize()
def cleanup():
"""Cleanup on application shutdown"""
# Cleanup process pools only (webhooks handled by Starlette shutdown)

View file

@ -110,14 +110,13 @@ class ChatService:
filter_expression["score_threshold"] = score_threshold
# Pass the complete filter expression as a single header to Langflow (only if we have something to send)
if filter_expression:
logger.info(
"Sending OpenRAG query filter to Langflow",
filter_expression=filter_expression,
)
extra_headers["X-LANGFLOW-GLOBAL-VAR-OPENRAG-QUERY-FILTER"] = json.dumps(
filter_expression
)
logger.info(
"Sending OpenRAG query filter to Langflow",
filter_expression=filter_expression,
)
extra_headers["X-LANGFLOW-GLOBAL-VAR-OPENRAG-QUERY-FILTER"] = json.dumps(
filter_expression
)
logger.info(f"[LF] Extra headers {extra_headers}")
# Ensure the Langflow client exists; try lazy init if needed
langflow_client = await clients.ensure_langflow_client()

View file

@ -1,4 +1,5 @@
from config.settings import (
DISABLE_INGEST_WITH_LANGFLOW,
NUDGES_FLOW_ID,
LANGFLOW_URL,
LANGFLOW_CHAT_FLOW_ID,
@ -69,17 +70,17 @@ class FlowsService:
# Scan all JSON files in the flows directory
try:
for filename in os.listdir(flows_dir):
if not filename.endswith('.json'):
if not filename.endswith(".json"):
continue
file_path = os.path.join(flows_dir, filename)
try:
with open(file_path, 'r') as f:
with open(file_path, "r") as f:
flow_data = json.load(f)
# Check if this file contains the flow we're looking for
if flow_data.get('id') == flow_id:
if flow_data.get("id") == flow_id:
# Cache the result
self._flow_file_cache[flow_id] = file_path
logger.info(f"Found flow {flow_id} in file: {filename}")
@ -95,6 +96,7 @@ class FlowsService:
logger.warning(f"Flow with ID {flow_id} not found in flows directory")
return None
async def reset_langflow_flow(self, flow_type: str):
"""Reset a Langflow flow by uploading the corresponding JSON file
@ -131,7 +133,9 @@ class FlowsService:
try:
with open(flow_path, "r") as f:
flow_data = json.load(f)
logger.info(f"Successfully loaded flow data for {flow_type} from {os.path.basename(flow_path)}")
logger.info(
f"Successfully loaded flow data for {flow_type} from {os.path.basename(flow_path)}"
)
except json.JSONDecodeError as e:
raise ValueError(f"Invalid JSON in flow file {flow_path}: {e}")
except FileNotFoundError:
@ -157,43 +161,62 @@ class FlowsService:
# Check if configuration has been edited (onboarding completed)
if config.edited:
logger.info(f"Updating {flow_type} flow with current configuration settings")
logger.info(
f"Updating {flow_type} flow with current configuration settings"
)
provider = config.provider.model_provider.lower()
# Step 1: Assign model provider (replace components) if not OpenAI
if provider != "openai":
logger.info(f"Assigning {provider} components to {flow_type} flow")
logger.info(
f"Assigning {provider} components to {flow_type} flow"
)
provider_result = await self.assign_model_provider(provider)
if not provider_result.get("success"):
logger.warning(f"Failed to assign {provider} components: {provider_result.get('error', 'Unknown error')}")
logger.warning(
f"Failed to assign {provider} components: {provider_result.get('error', 'Unknown error')}"
)
# Continue anyway, maybe just value updates will work
# Step 2: Update model values for the specific flow being reset
single_flow_config = [{
"name": flow_type,
"flow_id": flow_id,
}]
single_flow_config = [
{
"name": flow_type,
"flow_id": flow_id,
}
]
logger.info(f"Updating {flow_type} flow model values")
update_result = await self.change_langflow_model_value(
provider=provider,
embedding_model=config.knowledge.embedding_model,
llm_model=config.agent.llm_model,
endpoint=config.provider.endpoint if config.provider.endpoint else None,
flow_configs=single_flow_config
endpoint=config.provider.endpoint
if config.provider.endpoint
else None,
flow_configs=single_flow_config,
)
if update_result.get("success"):
logger.info(f"Successfully updated {flow_type} flow with current configuration")
logger.info(
f"Successfully updated {flow_type} flow with current configuration"
)
else:
logger.warning(f"Failed to update {flow_type} flow with current configuration: {update_result.get('error', 'Unknown error')}")
logger.warning(
f"Failed to update {flow_type} flow with current configuration: {update_result.get('error', 'Unknown error')}"
)
else:
logger.info(f"Configuration not yet edited (onboarding not completed), skipping model updates for {flow_type} flow")
logger.info(
f"Configuration not yet edited (onboarding not completed), skipping model updates for {flow_type} flow"
)
except Exception as e:
logger.error(f"Error updating {flow_type} flow with current configuration", error=str(e))
logger.error(
f"Error updating {flow_type} flow with current configuration",
error=str(e),
)
# Don't fail the entire reset operation if configuration update fails
return {
@ -239,7 +262,9 @@ class FlowsService:
try:
# Load component templates based on provider
llm_template, embedding_template, llm_text_template = self._load_component_templates(provider)
llm_template, embedding_template, llm_text_template = (
self._load_component_templates(provider)
)
logger.info(f"Assigning {provider} components")
@ -354,7 +379,9 @@ class FlowsService:
logger.info(f"Loaded component templates for {provider}")
return llm_template, embedding_template, llm_text_template
async def _update_flow_components(self, config, llm_template, embedding_template, llm_text_template):
async def _update_flow_components(
self, config, llm_template, embedding_template, llm_text_template
):
"""Update components in a specific flow"""
flow_name = config["name"]
flow_id = config["flow_id"]
@ -379,20 +406,21 @@ class FlowsService:
components_updated = []
# Replace embedding component
embedding_node, _ = self._find_node_in_flow(flow_data, display_name=old_embedding_name)
if embedding_node:
# Preserve position
original_position = embedding_node.get("position", {})
if not DISABLE_INGEST_WITH_LANGFLOW:
embedding_node, _ = self._find_node_in_flow(flow_data, display_name=old_embedding_name)
if embedding_node:
# Preserve position
original_position = embedding_node.get("position", {})
# Replace with new template
new_embedding_node = embedding_template.copy()
new_embedding_node["position"] = original_position
# Replace with new template
new_embedding_node = embedding_template.copy()
new_embedding_node["position"] = original_position
# Replace in flow
self._replace_node_in_flow(flow_data, old_embedding_name, new_embedding_node)
components_updated.append(
f"embedding: {old_embedding_name} -> {new_embedding_id}"
)
# Replace in flow
self._replace_node_in_flow(flow_data, old_embedding_name, new_embedding_node)
components_updated.append(
f"embedding: {old_embedding_name} -> {new_embedding_id}"
)
# Replace LLM component (if exists in this flow)
if old_llm_name:
@ -438,14 +466,15 @@ class FlowsService:
flow_json_str = json.dumps(flow_data)
# Replace embedding ID references
flow_json_str = re.sub(
re.escape(old_embedding_id), new_embedding_id, flow_json_str
)
flow_json_str = re.sub(
re.escape(old_embedding_id.split("-")[0]),
new_embedding_id.split("-")[0],
flow_json_str,
)
if not DISABLE_INGEST_WITH_LANGFLOW:
flow_json_str = re.sub(
re.escape(old_embedding_id), new_embedding_id, flow_json_str
)
flow_json_str = re.sub(
re.escape(old_embedding_id.split("-")[0]),
new_embedding_id.split("-")[0],
flow_json_str,
)
# Replace LLM ID references (if applicable)
if old_llm_id:
@ -527,19 +556,21 @@ class FlowsService:
raise ValueError("flow_id is required")
# Get the current flow data from Langflow
response = await clients.langflow_request(
"GET", f"/api/v1/flows/{flow_id}"
)
response = await clients.langflow_request("GET", f"/api/v1/flows/{flow_id}")
if response.status_code != 200:
raise Exception(f"Failed to get flow: HTTP {response.status_code} - {response.text}")
raise Exception(
f"Failed to get flow: HTTP {response.status_code} - {response.text}"
)
flow_data = response.json()
# Find the target component by display name first, then by ID as fallback
target_node, target_node_index = None, None
if node_display_name:
target_node, target_node_index = self._find_node_in_flow(flow_data, display_name=node_display_name)
target_node, target_node_index = self._find_node_in_flow(
flow_data, display_name=node_display_name
)
if target_node is None:
identifier = node_display_name
@ -561,7 +592,9 @@ class FlowsService:
)
if patch_response.status_code != 200:
raise Exception(f"Failed to update flow: HTTP {patch_response.status_code} - {patch_response.text}")
raise Exception(
f"Failed to update flow: HTTP {patch_response.status_code} - {patch_response.text}"
)
async def update_chat_flow_model(self, model_name: str, provider: str):
"""Helper function to update the model in the chat flow"""
@ -598,15 +631,23 @@ class FlowsService:
"""Helper function to update chunk size in the ingest flow"""
if not LANGFLOW_INGEST_FLOW_ID:
raise ValueError("LANGFLOW_INGEST_FLOW_ID is not configured")
await self._update_flow_field(LANGFLOW_INGEST_FLOW_ID, "chunk_size", chunk_size,
node_display_name="Split Text")
await self._update_flow_field(
LANGFLOW_INGEST_FLOW_ID,
"chunk_size",
chunk_size,
node_display_name="Split Text",
)
async def update_ingest_flow_chunk_overlap(self, chunk_overlap: int):
"""Helper function to update chunk overlap in the ingest flow"""
if not LANGFLOW_INGEST_FLOW_ID:
raise ValueError("LANGFLOW_INGEST_FLOW_ID is not configured")
await self._update_flow_field(LANGFLOW_INGEST_FLOW_ID, "chunk_overlap", chunk_overlap,
node_display_name="Split Text")
await self._update_flow_field(
LANGFLOW_INGEST_FLOW_ID,
"chunk_overlap",
chunk_overlap,
node_display_name="Split Text",
)
async def update_ingest_flow_embedding_model(self, embedding_model: str, provider: str):
"""Helper function to update embedding model in the ingest flow"""
@ -629,7 +670,12 @@ class FlowsService:
return False
async def change_langflow_model_value(
self, provider: str, embedding_model: str, llm_model: str, endpoint: str = None, flow_configs: list = None
self,
provider: str,
embedding_model: str,
llm_model: str,
endpoint: str = None,
flow_configs: list = None,
):
"""
Change dropdown values for provider-specific components across flows
@ -753,26 +799,25 @@ class FlowsService:
flow_id = config["flow_id"]
# Get flow data from Langflow API instead of file
response = await clients.langflow_request(
"GET", f"/api/v1/flows/{flow_id}"
)
response = await clients.langflow_request("GET", f"/api/v1/flows/{flow_id}")
if response.status_code != 200:
raise Exception(
f"Failed to get flow from Langflow: HTTP {response.status_code} - {response.text}"
)
flow_data = response.json()
updates_made = []
# Update embedding component
embedding_node, _ = self._find_node_in_flow(flow_data, display_name=target_embedding_name)
if embedding_node:
if self._update_component_fields(
embedding_node, provider, embedding_model, endpoint
):
updates_made.append(f"embedding model: {embedding_model}")
if not DISABLE_INGEST_WITH_LANGFLOW:
embedding_node, _ = self._find_node_in_flow(flow_data, display_name=target_embedding_name)
if embedding_node:
if self._update_component_fields(
embedding_node, provider, embedding_model, endpoint
):
updates_made.append(f"embedding model: {embedding_model}")
# Update LLM component (if exists in this flow)
if target_llm_name:

View file

@ -21,6 +21,27 @@ class ModelsService:
"jina-embeddings-v2-base-en",
]
OPENAI_TOOL_CALLING_MODELS = [
"gpt-5",
"gpt-5-mini",
"gpt-5-nano",
"gpt-4o-mini",
"gpt-4o",
"gpt-4.1",
"gpt-4.1-mini",
"gpt-4.1-nano",
"gpt-4-turbo",
"gpt-4-turbo-preview",
"gpt-4",
"gpt-3.5-turbo",
"o1",
"o3-mini",
"o3",
"o3-pro",
"o4-mini",
"o4-mini-high",
]
def __init__(self):
self.session_manager = None
@ -49,12 +70,12 @@ class ModelsService:
model_id = model.get("id", "")
# Language models (GPT models)
if any(prefix in model_id for prefix in ["gpt-4", "gpt-3.5"]):
if model_id in self.OPENAI_TOOL_CALLING_MODELS:
language_models.append(
{
"value": model_id,
"label": model_id,
"default": model_id == "gpt-4o-mini",
"default": model_id == "gpt-5",
}
)