diff --git a/.gitignore b/.gitignore index eb53104..a1e287a 100644 --- a/.gitignore +++ b/.gitignore @@ -44,5 +44,8 @@ hs_err_pid* # Docker docker-compose.override.yml +# Audit profiles (internal tooling) +profiles/ + # E2E tests — Maven build outputs & Chrome user data e2e/**/target/ diff --git a/CHANGELOG.md b/CHANGELOG.md index abdc198..961bcb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to Docling Studio will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/). -## [0.4.0] - Unreleased +## [0.4.0] - 2026-04-13 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fc91d31..e10aeaa 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -96,15 +96,43 @@ npx prettier --write src/ # auto-format ## Running Tests ```bash -# Backend (199 tests) +# Backend (377 tests) cd document-parser pytest tests/ -v -# Frontend (129 tests) +# Frontend (156 tests) cd frontend npm run test:run ``` +### E2E API (Karate) + +```bash +# Generate test PDFs + start stack +python e2e/generate-test-data.py +docker compose up -d --wait + +# Run all API tests +mvn test -f e2e/api/pom.xml + +# Or by tag: @smoke, @regression, @e2e +mvn test -f e2e/api/pom.xml -Dkarate.options="--tags @smoke" +``` + +### E2E UI (Karate UI) + +```bash +# Generate test PDFs + start stack (if not already running) +python e2e/generate-test-data.py +docker compose up -d --wait + +# Run critical UI tests (CI scope) +mvn test -f e2e/ui/pom.xml -Dkarate.options="--tags @critical" + +# Run all UI tests (local scope) +mvn test -f e2e/ui/pom.xml -Dkarate.options="--tags @ui" +``` + All tests must pass before submitting a PR. ## Submitting Changes diff --git a/README.md b/README.md index 801094e..eff9564 100644 --- a/README.md +++ b/README.md @@ -31,9 +31,13 @@ Upload a PDF, configure the extraction pipeline, and visualize the results — t - **Configurable Docling pipeline** — OCR, table extraction, code/formula enrichment, picture classification & description, image generation - **Bounding box visualization** — color-coded element overlay directly on the PDF - **Per-page results** — right panel syncs with the current PDF page +- **Chunking** — split extracted content into semantic chunks (hierarchical, hybrid, or page-based) with configurable token limits and inline editing +- **Ingestion pipeline** — Docling → chunking → embedding → OpenSearch vector indexing (one-click from Studio) - **Markdown & HTML export** of extracted content -- **Document management** — upload, list, delete +- **Document management** — upload, list, delete, search, filter by indexing status - **Analysis history** — re-visit and open past analyses +- **Upload limits** — configurable max file size and max page count per document +- **Rate limiting** — configurable requests per minute per IP - **Dark / Light theme** and **FR / EN** localization @@ -74,7 +78,7 @@ document-parser/ ├── services/ # Use case orchestration │ ├── document_service.py # Upload, delete, preview │ └── analysis_service.py # Async Docling processing -└── tests/ # 199 tests (pytest) +└── tests/ # 377 tests (pytest) ``` ### Frontend structure (feature-based) @@ -98,14 +102,24 @@ frontend/src/ ## Quick Start -Docling Studio ships two Docker image variants: +One command, nothing else to install: + +```bash +docker run -p 3000:3000 ghcr.io/scub-france/docling-studio:latest-local +``` + +Open [http://localhost:3000](http://localhost:3000), upload a PDF, and get results. That's it. + +> **Note:** The first analysis takes longer as Docling downloads its ML models (~400 MB). Subsequent runs are fast. + +### Image variants | Variant | Image tag | Size | Description | |---------|-----------|------|-------------| +| **local** | `latest-local` | ~1.9 GB | Full — runs Docling in-process, CPU-only | | **remote** | `latest-remote` | ~270 MB | Lightweight — delegates to an external [Docling Serve](https://github.com/DS4SD/docling-serve) instance | -| **local** | `latest-local` | ~1.9 GB | Full — runs Docling in-process, CPU-only (downloads ML models on first run) | -### Docker — remote mode (fastest) +For remote mode: ```bash docker run -p 3000:3000 \ @@ -113,27 +127,17 @@ docker run -p 3000:3000 \ ghcr.io/scub-france/docling-studio:latest-remote ``` -### Docker — local mode (self-contained) - -```bash -docker run -p 3000:3000 ghcr.io/scub-france/docling-studio:latest-local -``` - -> **Note:** The first analysis takes longer as Docling downloads its ML models (~400 MB). Subsequent runs are fast. - -Open [http://localhost:3000](http://localhost:3000) - -### Docker Compose (for development) +### Docker Compose ```bash git clone https://github.com/scub-france/Docling-Studio.git cd Docling-Studio -# Local mode (default) +# Simple mode (backend + frontend only) docker compose up --build -# Remote mode -CONVERSION_MODE=remote DOCLING_SERVE_URL=http://your-docling-serve:5001 docker compose up --build +# With ingestion pipeline (OpenSearch + embeddings) +docker compose --profile ingestion -f docker-compose.yml -f docker-compose.ingestion.yml up --build ``` ### Local Development @@ -162,12 +166,12 @@ npm run dev ### Running Tests ```bash -# Backend (199 tests) +# Backend (377 tests) cd document-parser pip install pytest pytest-asyncio httpx pytest tests/ -v -# Frontend (129 tests) +# Frontend (156 tests) cd frontend npm run test:run ``` @@ -202,6 +206,43 @@ All configuration is done via environment variables. See [`.env.example`](.env.e | `UPLOAD_DIR` | `./uploads` | File storage directory | | `DB_PATH` | `./data/docling_studio.db` | SQLite database path | | `CONVERSION_TIMEOUT` | `600` | Max seconds for a single Docling conversion | +| `BATCH_PAGE_SIZE` | `10` | Pages per batch (`0` = process all at once) | +| `MAX_FILE_SIZE_MB` | `50` | Maximum upload file size in MB (`0` = unlimited) | +| `MAX_PAGE_COUNT` | `0` | Maximum number of pages per document (`0` = unlimited) | +| `RATE_LIMIT_RPM` | `100` | Max requests per minute per IP (`0` = disabled) | + +## Upload Limits + +Docling Studio enforces configurable limits on uploaded documents to protect the server against oversized files and long-running analyses: + +- **`MAX_FILE_SIZE_MB`** (default `50`) — rejects uploads exceeding this size. Validated at two levels: early `Content-Length` check and streaming byte count. +- **`MAX_PAGE_COUNT`** (default `0` = unlimited) — rejects documents with more pages than allowed. Useful on shared instances or Hugging Face Spaces to cap processing time. + +Both limits are exposed in the `/api/health` endpoint so the frontend can display them to the user before upload. Set either to `0` to disable the corresponding check. + +## Ingestion Pipeline (opt-in) + +Docling Studio can optionally index extracted chunks into [OpenSearch](https://opensearch.org/) for vector and full-text search. This requires two additional services (OpenSearch + embedding) and is **disabled by default**. + +To enable ingestion with Docker Compose: + +```bash +docker compose --profile ingestion \ + -f docker-compose.yml -f docker-compose.ingestion.yml \ + up --build +``` + +When ingestion is enabled, the UI shows: +- An **Ingest** button in Studio to push chunks to OpenSearch +- An **OpenSearch** connection status badge in the sidebar +- **Indexed / Not indexed** filters on the Documents page +- A **Search** page for full-text and vector search across indexed documents + +| Variable | Default | Description | +|----------|---------|-------------| +| `OPENSEARCH_URL` | — | OpenSearch endpoint (empty = ingestion disabled) | +| `EMBEDDING_URL` | — | Embedding service endpoint (empty = ingestion disabled) | +| `EMBEDDING_DIMENSION` | `384` | Vector dimension (must match embedding model) | ## CI / Release diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index fc08ad2..9255c3b 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -74,7 +74,7 @@ services: DOCLING_SERVE_API_KEY: ${DOCLING_SERVE_API_KEY:-} RATE_LIMIT_RPM: ${RATE_LIMIT_RPM:-100} MAX_FILE_SIZE_MB: ${MAX_FILE_SIZE_MB:-50} - BATCH_PAGE_SIZE: ${BATCH_PAGE_SIZE:-0} + BATCH_PAGE_SIZE: ${BATCH_PAGE_SIZE:-10} OPENSEARCH_URL: http://opensearch:9200 EMBEDDING_URL: http://embedding:8001 command: ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] diff --git a/docker-compose.ingestion.yml b/docker-compose.ingestion.yml new file mode 100644 index 0000000..ed15dc1 --- /dev/null +++ b/docker-compose.ingestion.yml @@ -0,0 +1,19 @@ +# Override to enable the ingestion pipeline (OpenSearch + embeddings). +# +# Usage: +# docker compose --profile ingestion -f docker-compose.yml -f docker-compose.ingestion.yml up --build +# +# This wires the backend to the OpenSearch and embedding services started +# by the "ingestion" profile and ensures they are healthy before the +# backend starts. + +services: + document-parser: + environment: + OPENSEARCH_URL: http://opensearch:9200 + EMBEDDING_URL: http://embedding:8001 + depends_on: + opensearch: + condition: service_healthy + embedding: + condition: service_healthy diff --git a/docker-compose.yml b/docker-compose.yml index e965fc5..918bfb9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,7 @@ services: # --- OpenSearch (single-node, security disabled) --- opensearch: + profiles: ["ingestion"] image: opensearchproject/opensearch:2 environment: discovery.type: single-node @@ -16,6 +17,7 @@ services: # --- Embedding service (sentence-transformers) --- embedding: + profiles: ["ingestion"] build: context: ./embedding-service environment: @@ -48,14 +50,9 @@ services: DOCLING_SERVE_API_KEY: ${DOCLING_SERVE_API_KEY:-} RATE_LIMIT_RPM: ${RATE_LIMIT_RPM:-100} MAX_FILE_SIZE_MB: ${MAX_FILE_SIZE_MB:-50} - BATCH_PAGE_SIZE: ${BATCH_PAGE_SIZE:-0} - OPENSEARCH_URL: http://opensearch:9200 - EMBEDDING_URL: http://embedding:8001 - depends_on: - opensearch: - condition: service_healthy - embedding: - condition: service_healthy + BATCH_PAGE_SIZE: ${BATCH_PAGE_SIZE:-10} + OPENSEARCH_URL: ${OPENSEARCH_URL:-} + EMBEDDING_URL: ${EMBEDDING_URL:-} deploy: resources: limits: diff --git a/docs/getting-started.md b/docs/getting-started.md index b2ee2b8..c187e72 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1,15 +1,28 @@ # Getting Started -Docling Studio ships two Docker image variants: +## Quick Start -| Variant | Image tag | Size | Description | -|---------|-----------|------|-------------| -| **remote** | `latest-remote` | ~270 MB | Lightweight — delegates to an external [Docling Serve](https://github.com/DS4SD/docling-serve) instance | -| **local** | `latest-local` | ~1.9 GB | Full — runs Docling in-process, CPU-only (downloads ML models on first run) | +One command, nothing else to install: + +```bash +docker run -p 3000:3000 ghcr.io/scub-france/docling-studio:latest-local +``` + +Open [http://localhost:3000](http://localhost:3000), upload a PDF, and get results. That's it. + +!!! note + The first analysis takes longer as Docling downloads its ML models (~400 MB). Subsequent runs are fast. { width="600" } -## Docker — remote mode (fastest) +## Image Variants + +| Variant | Image tag | Size | Description | +|---------|-----------|------|-------------| +| **local** | `latest-local` | ~1.9 GB | Full — runs Docling in-process, CPU-only | +| **remote** | `latest-remote` | ~270 MB | Lightweight — delegates to an external [Docling Serve](https://github.com/DS4SD/docling-serve) instance | + +For remote mode: ```bash docker run -p 3000:3000 \ @@ -17,27 +30,19 @@ docker run -p 3000:3000 \ ghcr.io/scub-france/docling-studio:latest-remote ``` -## Docker — local mode (self-contained) - -```bash -docker run -p 3000:3000 ghcr.io/scub-france/docling-studio:latest-local -``` - -> **Note:** The first analysis takes longer as Docling downloads its ML models (~400 MB). Subsequent runs are fast. - -Open [http://localhost:3000](http://localhost:3000). - -## Docker Compose (recommended for development) +## Docker Compose ```bash git clone https://github.com/scub-france/Docling-Studio.git cd Docling-Studio -# Local mode (default) +# Simple mode (backend + frontend only) docker compose up --build -# Remote mode -CONVERSION_MODE=remote DOCLING_SERVE_URL=http://your-docling-serve:5001 docker compose up --build +# With ingestion pipeline (OpenSearch + embeddings) +docker compose --profile ingestion \ + -f docker-compose.yml -f docker-compose.ingestion.yml \ + up --build ``` ## Local Development @@ -136,10 +141,48 @@ All configuration is done via environment variables: | `UPLOAD_DIR` | `./uploads` | File storage directory | | `DB_PATH` | `./data/docling_studio.db` | SQLite database path | | `CONVERSION_TIMEOUT` | `600` | Max seconds per Docling conversion | +| `BATCH_PAGE_SIZE` | `10` | Pages per batch (`0` = process all at once) | | `MAX_CONCURRENT_ANALYSES` | `3` | Maximum parallel analysis jobs | | `DEPLOYMENT_MODE` | `self-hosted` | `self-hosted` or `huggingface` (shows disclaimer banner) | +| `MAX_FILE_SIZE_MB` | `50` | Maximum upload file size in MB (`0` = unlimited) | +| `MAX_PAGE_COUNT` | `0` | Maximum number of pages per document (`0` = unlimited) | +| `RATE_LIMIT_RPM` | `100` | Max requests per minute per IP (`0` = disabled) | | `APP_VERSION` | `dev` | Application version (set automatically by CI/Docker) | +## Upload Limits + +Docling Studio enforces configurable limits on uploaded documents to protect the server against oversized files and long-running analyses: + +- **`MAX_FILE_SIZE_MB`** (default `50`) — rejects uploads exceeding this size. Validated at two levels: early `Content-Length` check and streaming byte count. +- **`MAX_PAGE_COUNT`** (default `0` = unlimited) — rejects documents with more pages than allowed. Useful on shared instances or Hugging Face Spaces to cap processing time. + +Both limits are exposed in the `/api/health` endpoint so the frontend can display them to the user before upload. Set either to `0` to disable the corresponding check. + +## Ingestion Pipeline (opt-in) + +Docling Studio can optionally index extracted chunks into [OpenSearch](https://opensearch.org/) for vector and full-text search. This requires two additional services (OpenSearch + embedding) and is **disabled by default**. + +To enable ingestion with Docker Compose: + +```bash +docker compose --profile ingestion \ + -f docker-compose.yml -f docker-compose.ingestion.yml \ + up --build +``` + +When ingestion is enabled, the UI shows: + +- An **Ingest** button in Studio to push chunks to OpenSearch +- An **OpenSearch** connection status badge in the sidebar +- **Indexed / Not indexed** filters on the Documents page +- A **Search** page for full-text and vector search across indexed documents + +| Variable | Default | Description | +|----------|---------|-------------| +| `OPENSEARCH_URL` | — | OpenSearch endpoint (empty = ingestion disabled) | +| `EMBEDDING_URL` | — | Embedding service endpoint (empty = ingestion disabled) | +| `EMBEDDING_DIMENSION` | `384` | Vector dimension (must match embedding model) | + ## System Requirements | | Remote image | Local image | diff --git a/docs/index.md b/docs/index.md index b476021..2323b46 100644 --- a/docs/index.md +++ b/docs/index.md @@ -18,7 +18,8 @@ Upload a PDF, configure the extraction pipeline, and visualize the results — t - **Document management** — upload, list, delete - **Analysis history** — re-visit and open past analyses - **Feature flags** — capabilities adapt to the conversion engine (local vs remote) -- **Rate limiting** — 60 requests per minute per IP to protect the backend +- **Upload limits** — configurable max file size (`MAX_FILE_SIZE_MB`) and max page count (`MAX_PAGE_COUNT`) per document +- **Rate limiting** — configurable requests per minute per IP (`RATE_LIMIT_RPM`) - **Deployment modes** — self-hosted (default) or HuggingFace Spaces (with disclaimer banner) - **Health endpoint** — `/api/health` reports engine type, deployment mode, and database status - **Dark / Light theme** and **FR / EN** localization diff --git a/document-parser/api/schemas.py b/document-parser/api/schemas.py index 1f283e3..2b4a1b1 100644 --- a/document-parser/api/schemas.py +++ b/document-parser/api/schemas.py @@ -34,6 +34,7 @@ class HealthResponse(_CamelModel): database: str max_page_count: int | None = None max_file_size_mb: int | None = None + ingestion_available: bool = False class DocumentResponse(_CamelModel): diff --git a/document-parser/infra/settings.py b/document-parser/infra/settings.py index f93c299..50e465d 100644 --- a/document-parser/infra/settings.py +++ b/document-parser/infra/settings.py @@ -94,7 +94,7 @@ class Settings: max_file_size=int(os.environ.get("MAX_FILE_SIZE", "0")), max_file_size_mb=int(os.environ.get("MAX_FILE_SIZE_MB", "50")), rate_limit_rpm=int(os.environ.get("RATE_LIMIT_RPM", "100")), - batch_page_size=int(os.environ.get("BATCH_PAGE_SIZE", "0")), + batch_page_size=int(os.environ.get("BATCH_PAGE_SIZE", "10")), opensearch_url=os.environ.get("OPENSEARCH_URL", ""), embedding_url=os.environ.get("EMBEDDING_URL", ""), embedding_dimension=int(os.environ.get("EMBEDDING_DIMENSION", "384")), diff --git a/document-parser/main.py b/document-parser/main.py index a316b34..0f6a4a4 100644 --- a/document-parser/main.py +++ b/document-parser/main.py @@ -138,7 +138,11 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]: document_repo, analysis_repo = _build_repos() app.state.analysis_service = _build_analysis_service(document_repo, analysis_repo) app.state.document_service = _build_document_service(document_repo, analysis_repo) - app.state.ingestion_service = _build_ingestion_service() + ingestion_service = _build_ingestion_service() + app.state.ingestion_service = ingestion_service + if ingestion_service is not None: + app.include_router(ingestion_router) + logger.info("Ingestion router mounted") logger.info("Docling Studio backend ready (engine=%s)", settings.conversion_engine) yield @@ -165,7 +169,6 @@ if settings.rate_limit_rpm > 0: app.include_router(documents_router) app.include_router(analyses_router) -app.include_router(ingestion_router) @app.get("/api/health", response_model=HealthResponse) @@ -188,4 +191,5 @@ async def health() -> HealthResponse: database=db_status, max_page_count=settings.max_page_count if settings.max_page_count > 0 else None, max_file_size_mb=settings.max_file_size_mb if settings.max_file_size_mb > 0 else None, + ingestion_available=getattr(app.state, "ingestion_service", None) is not None, ) diff --git a/document-parser/tests/test_api_endpoints.py b/document-parser/tests/test_api_endpoints.py index 97c154e..2be220e 100644 --- a/document-parser/tests/test_api_endpoints.py +++ b/document-parser/tests/test_api_endpoints.py @@ -51,6 +51,23 @@ class TestHealthEndpoint: assert "maxFileSizeMb" in data assert data["maxFileSizeMb"] == 50 + def test_health_exposes_ingestion_available_false(self, client): + original = getattr(app.state, "ingestion_service", None) + app.state.ingestion_service = None + resp = client.get("/api/health") + app.state.ingestion_service = original + data = resp.json() + assert "ingestionAvailable" in data + assert data["ingestionAvailable"] is False + + def test_health_exposes_ingestion_available_true(self, client): + original = getattr(app.state, "ingestion_service", None) + app.state.ingestion_service = MagicMock() + resp = client.get("/api/health") + app.state.ingestion_service = original + data = resp.json() + assert data["ingestionAvailable"] is True + class TestDocumentEndpoints: def test_list_documents(self, client, mock_document_service): diff --git a/document-parser/tests/test_ingestion_api.py b/document-parser/tests/test_ingestion_api.py index 0c330c4..a872d79 100644 --- a/document-parser/tests/test_ingestion_api.py +++ b/document-parser/tests/test_ingestion_api.py @@ -104,7 +104,18 @@ class TestIngestionStatus: class TestIngestionDisabled: - def test_returns_503_when_disabled(self) -> None: + def test_router_not_mounted_returns_404(self) -> None: + """When ingestion service is None, router should not be mounted → 404.""" + app = FastAPI() + # Do NOT include ingestion router — simulates main.py conditional mount + app.state.ingestion_service = None + app.state.analysis_service = AsyncMock() + tc = TestClient(app) + resp = tc.post("/api/ingestion/job-1") + assert resp.status_code == 404 + + def test_status_still_returns_503_when_router_mounted_but_service_none(self) -> None: + """If router is mounted but service is None, endpoints return 503.""" app = FastAPI() app.include_router(router) app.state.ingestion_service = None diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 469c71b..b6e3d75 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "docling-studio", - "version": "0.3.0", + "version": "0.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "docling-studio", - "version": "0.3.0", + "version": "0.3.1", "dependencies": { "dompurify": "^3.3.3", "marked": "^17.0.4", diff --git a/frontend/package.json b/frontend/package.json index 5c7dcff..3c7c581 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "docling-studio", - "version": "0.3.1", + "version": "0.4.0", "private": true, "type": "module", "scripts": { diff --git a/frontend/src/features/chunking/ui/ChunkPanel.vue b/frontend/src/features/chunking/ui/ChunkPanel.vue index 093452b..a8328de 100644 --- a/frontend/src/features/chunking/ui/ChunkPanel.vue +++ b/frontend/src/features/chunking/ui/ChunkPanel.vue @@ -202,7 +202,10 @@ -
{{ chunks.length ? t('chunking.noChunksOnPage') : t('chunking.noChunks') }}
diff --git a/frontend/src/features/feature-flags/store.test.ts b/frontend/src/features/feature-flags/store.test.ts index ee70e31..ab3c581 100644 --- a/frontend/src/features/feature-flags/store.test.ts +++ b/frontend/src/features/feature-flags/store.test.ts @@ -82,6 +82,38 @@ describe('useFeatureFlagStore', () => { expect(store.maxFileSizeMb).toBe(0) }) + it('enables ingestion when ingestionAvailable is true', async () => { + mockApiFetch.mockResolvedValue({ + status: 'ok', + engine: 'local', + ingestionAvailable: true, + }) + const store = useFeatureFlagStore() + await store.load() + expect(store.ingestionAvailable).toBe(true) + expect(store.isEnabled('ingestion')).toBe(true) + }) + + it('disables ingestion when ingestionAvailable is false', async () => { + mockApiFetch.mockResolvedValue({ + status: 'ok', + engine: 'local', + ingestionAvailable: false, + }) + const store = useFeatureFlagStore() + await store.load() + expect(store.ingestionAvailable).toBe(false) + expect(store.isEnabled('ingestion')).toBe(false) + }) + + it('defaults ingestionAvailable to false when missing', async () => { + mockApiFetch.mockResolvedValue({ status: 'ok', engine: 'local' }) + const store = useFeatureFlagStore() + await store.load() + expect(store.ingestionAvailable).toBe(false) + expect(store.isEnabled('ingestion')).toBe(false) + }) + it('handles health endpoint failure gracefully', async () => { mockApiFetch.mockRejectedValue(new Error('Network error')) const store = useFeatureFlagStore() diff --git a/frontend/src/features/feature-flags/store.ts b/frontend/src/features/feature-flags/store.ts index 0cc6eb7..0014a23 100644 --- a/frontend/src/features/feature-flags/store.ts +++ b/frontend/src/features/feature-flags/store.ts @@ -13,9 +13,10 @@ interface HealthResponse { deploymentMode?: DeploymentMode maxPageCount?: number maxFileSizeMb?: number + ingestionAvailable?: boolean } -export type FeatureFlag = 'chunking' | 'disclaimer' +export type FeatureFlag = 'chunking' | 'disclaimer' | 'ingestion' interface FeatureFlagDef { description: string @@ -25,6 +26,7 @@ interface FeatureFlagDef { interface FeatureFlagContext { engine: ConversionEngine | null deploymentMode: DeploymentMode | null + ingestionAvailable: boolean } const featureRegistry: Record