From bd232bdef312ebaadaffe4ee1d92adfecda67776 Mon Sep 17 00:00:00 2001 From: Pier-Jean Malandrino Date: Fri, 10 Apr 2026 21:21:32 +0200 Subject: [PATCH] feat(frontend): ingestion button in Studio for one-click indexing Closes #76 --- CHANGELOG.md | 6 ++++ frontend/src/pages/StudioPage.vue | 48 +++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2031ddf..abdc198 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this - OpenSearch adapter (`OpenSearchStore`): kNN vector search, full-text search, bulk indexing, document CRUD - Embedding microservice (`embedding-service/`): sentence-transformers REST API with batch processing and Dockerfile - `EmbeddingService` port and `EmbeddingClient` HTTP adapter for remote embedding generation +- Orchestrated ingestion pipeline: Docling → chunking → embedding → OpenSearch indexing (idempotent) +- Ingestion REST API: `POST /api/ingestion/{jobId}`, `DELETE /api/ingestion/{docId}`, `GET /api/ingestion/status` +- Production docker-compose with OpenSearch and embedding service +- E2E Karate test for full ingestion workflow (PDF → chunks in OpenSearch) +- My Documents screen: search, filter (all/indexed/not indexed), sort (name/date), ingestion status badges +- Ingest button in Studio: one-click ingestion from completed analysis with progress feedback ### Fixed diff --git a/frontend/src/pages/StudioPage.vue b/frontend/src/pages/StudioPage.vue index e96769b..5ef26f9 100644 --- a/frontend/src/pages/StudioPage.vue +++ b/frontend/src/pages/StudioPage.vue @@ -94,6 +94,23 @@ {{ analysisStore.running ? t('studio.analyzing') : t('studio.run') }} + @@ -459,6 +476,7 @@ import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount, reactive } import { useRoute, useRouter } from 'vue-router' import { useDocumentStore } from '../features/document/store' import { useAnalysisStore } from '../features/analysis/store' +import { useIngestionStore } from '../features/ingestion/store' import { DocumentUpload, DocumentList } from '../features/document/index' import { ResultTabs } from '../features/analysis/index' import BboxOverlay from '../features/analysis/ui/BboxOverlay.vue' @@ -472,6 +490,7 @@ const route = useRoute() const router = useRouter() const documentStore = useDocumentStore() const analysisStore = useAnalysisStore() +const ingestionStore = useIngestionStore() const { t } = useI18n() const chunkingEnabled = useFeatureFlag('chunking') @@ -528,6 +547,14 @@ const pipelineOptions = reactive({ images_scale: 1.0, }) +const canIngest = computed(() => { + return ( + ingestionStore.available && + analysisStore.currentAnalysis?.status === 'COMPLETED' && + analysisStore.currentAnalysis?.chunksJson != null + ) +}) + const hasAnalysisResults = computed(() => { return ( analysisStore.currentAnalysis?.status === 'COMPLETED' && analysisStore.currentPages?.length > 0 @@ -564,6 +591,11 @@ async function runAnalysis() { await analysisStore.run(documentStore.selectedId, { ...pipelineOptions }) } +async function runIngestion() { + if (!analysisStore.currentAnalysis?.id) return + await ingestionStore.ingest(analysisStore.currentAnalysis.id) +} + function addMore() { documentStore.selectedId = null } @@ -598,6 +630,7 @@ watch( onMounted(async () => { await documentStore.load() analysisStore.load() + ingestionStore.checkAvailability() // Restore analysis from history via query param const analysisId = route.query.analysisId @@ -811,6 +844,21 @@ onBeforeUnmount(() => { cursor: not-allowed; } +.topbar-btn.ingest { + background: var(--success); + border-color: var(--success); + color: white; +} + +.topbar-btn.ingest:hover:not(:disabled) { + filter: brightness(1.1); +} + +.topbar-btn.ingest:disabled { + opacity: 0.6; + cursor: not-allowed; +} + .topbar-btn .btn-icon { width: 16px; height: 16px;