feat(#225,#224): vocabulary rename index→ingest + stale stores strip

- #225: rename all push/index/pousser/indexé labels to ingest/ingérer/ingéré
  across status tooltips, docs/chunks push modals, and legacy ingestion panel
- #224: add DocStoreLink type + storeLinks on Document; StaleStoresStrip
  component shows per-store state badges with one-click re-ingest buttons
This commit is contained in:
Pier-Jean Malandrino 2026-04-30 10:04:01 +02:00
parent cab3e53fe4
commit e44fbb134e
5 changed files with 209 additions and 37 deletions

View file

@ -0,0 +1,143 @@
<template>
<div v-if="storeLinks.length > 0" class="stale-strip" data-e2e="stale-strip">
<div class="stale-strip__stores">
<div v-for="link in storeLinks" :key="link.store" class="store-row">
<span class="store-name">{{ link.store }}</span>
<StatusBadge :state="link.state" compact />
<span v-if="link.pushedAt" class="store-date">
{{ t('chunks.stale.pushedAt', { date: formatRelativeTime(link.pushedAt, locale) }) }}
</span>
<button
v-if="link.state === 'Stale'"
class="btn-reingest"
:disabled="reingestingStores.has(link.store)"
@click="reingest(link.store)"
>
{{ t('chunks.stale.reingest') }}
</button>
</div>
</div>
<button
v-if="staleCount > 1"
class="btn-reingest-all"
:disabled="reingestingStores.size > 0"
@click="reingestAll"
>
{{ t('chunks.stale.reingestAll') }}
</button>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import type { DocStoreLink } from '../../../shared/types'
import { pushChunksToStore } from '../api'
import StatusBadge from '../../document/ui/StatusBadge.vue'
import { useI18n } from '../../../shared/i18n'
import { formatRelativeTime } from '../../../shared/format'
import { appLocale } from '../../../shared/appConfig'
const props = defineProps<{
docId: string
storeLinks: DocStoreLink[]
}>()
const { t } = useI18n()
const locale = appLocale
const reingestingStores = ref<Set<string>>(new Set())
const staleCount = computed(() => props.storeLinks.filter((l) => l.state === 'Stale').length)
async function reingest(store: string): Promise<void> {
const next = new Set(reingestingStores.value)
next.add(store)
reingestingStores.value = next
try {
await pushChunksToStore(props.docId, store)
} catch {
// silent user can retry
} finally {
const after = new Set(reingestingStores.value)
after.delete(store)
reingestingStores.value = after
}
}
async function reingestAll(): Promise<void> {
const stale = props.storeLinks.filter((l) => l.state === 'Stale').map((l) => l.store)
await Promise.all(stale.map(reingest))
}
</script>
<style scoped>
.stale-strip {
display: flex;
align-items: center;
gap: 12px;
padding: 6px 16px;
background: var(--bg-surface);
border-bottom: 1px solid var(--border);
flex-wrap: wrap;
flex-shrink: 0;
}
.stale-strip__stores {
display: flex;
flex-wrap: wrap;
gap: 8px;
flex: 1;
}
.store-row {
display: flex;
align-items: center;
gap: 6px;
padding: 2px 8px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
font-size: 12px;
}
.store-name {
font-weight: 500;
color: var(--text-secondary);
}
.store-date {
color: var(--text-muted);
font-size: 11px;
}
.btn-reingest,
.btn-reingest-all {
padding: 2px 8px;
font-size: 11px;
font-weight: 500;
background: var(--warning-bg, rgba(234, 179, 8, 0.1));
color: var(--warning, #ca8a04);
border: 1px solid var(--warning, #ca8a04);
border-radius: var(--radius-sm);
cursor: pointer;
transition: opacity var(--transition);
white-space: nowrap;
}
.btn-reingest:disabled,
.btn-reingest-all:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-reingest:hover:not(:disabled),
.btn-reingest-all:hover:not(:disabled) {
opacity: 0.8;
}
.btn-reingest-all {
flex-shrink: 0;
padding: 4px 10px;
font-size: 12px;
}
</style>

View file

@ -25,6 +25,11 @@
<!-- Chunks editor -->
<div class="editor-pane">
<StaleStoresStrip
v-if="storeLinks && storeLinks.length > 0"
:doc-id="docId"
:store-links="storeLinks"
/>
<ChunksEditor
:doc-id="docId"
:available-stores="availableStores"
@ -36,15 +41,17 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import type { DocTreeNode } from '../shared/types'
import type { DocTreeNode, DocStoreLink } from '../shared/types'
import { fetchDocumentTree } from '../features/document/api'
import DocTreeRail from '../features/document/ui/DocTreeRail.vue'
import ChunksEditor from '../features/chunks/ui/ChunksEditor.vue'
import StaleStoresStrip from '../features/chunks/ui/StaleStoresStrip.vue'
import { useI18n } from '../shared/i18n'
const props = defineProps<{
docId: string
availableStores: string[]
storeLinks?: DocStoreLink[]
}>()
const { t } = useI18n()

View file

@ -41,6 +41,7 @@
v-if="activeMode === 'chunks'"
:doc-id="id"
:available-stores="doc.stores ?? []"
:store-links="doc.storeLinks"
/>
<DocInspectTab v-else-if="activeMode === 'inspect'" :doc-id="id" />
<DocAskTab v-else-if="activeMode === 'ask'" :doc-id="id" />

View file

@ -46,9 +46,9 @@ const messages: Messages = {
'status.Failed': 'Échoué',
'status.tooltip.Uploaded': 'Fichier reçu, en attente de parsing.',
'status.tooltip.Parsed': 'Document parsé avec succès.',
'status.tooltip.Chunked': 'Chunks générés, prêts à être poussés vers un store.',
'status.tooltip.Ingested': 'Indexé dans au moins un store.',
'status.tooltip.Stale': 'Chunks modifiés depuis le dernier push — re-ingestion requise.',
'status.tooltip.Chunked': 'Chunks générés, prêts à être ingérés dans un store.',
'status.tooltip.Ingested': 'Ingéré dans au moins un store.',
'status.tooltip.Stale': 'Chunks modifiés depuis la dernière ingestion — ré-ingestion requise.',
'status.tooltip.Failed': 'Une étape du pipeline a échoué. Réessayez.',
// Document library (#211, #212, #213)
@ -66,14 +66,14 @@ const messages: Messages = {
'docs.filterClear': 'Effacer les filtres',
'docs.selected': '{n} sélectionné(s)',
'docs.bulkRechunk': 'Re-chunker',
'docs.bulkPush': 'Pousser vers un store…',
'docs.bulkPush': 'Ingérer dans un store…',
'docs.bulkDelete': 'Supprimer',
'docs.bulkCancel': 'Annuler la sélection',
'docs.deleteConfirm': 'Supprimer {n} document(s) ? Cette action est irréversible.',
'docs.pushTitle': 'Pousser vers un store',
'docs.pushTitle': 'Ingérer dans un store',
'docs.pushLabel': 'Store cible',
'docs.pushPlaceholder': 'Nom du store…',
'docs.pushSubmit': 'Pousser',
'docs.pushSubmit': 'Ingérer',
'docs.pushCancel': 'Annuler',
'docs.jobDispatched': 'Job lancé ({jobId})',
@ -316,33 +316,33 @@ const messages: Messages = {
'Le chunking n\u2019est pas disponible pour cette analyse. Les documents volumineux trait\u00e9s par batch ne g\u00e9n\u00e8rent pas la structure interne n\u00e9cessaire au d\u00e9coupage. Coming soon !',
// Search (legacy nav label moved to the top with the other legacy keys)
'search.hint': 'Saisissez un terme pour rechercher dans les chunks indexés.',
'search.hint': 'Saisissez un terme pour rechercher dans les chunks ingérés.',
// Ingestion / My Documents
'ingestion.ingest': 'Ingérer',
'ingestion.document': 'Document',
'ingestion.chunkCount': 'Chunks prêts',
'ingestion.successMessage': 'Indexation terminée avec succès !',
'ingestion.successMessage': 'Ingestion terminée avec succès !',
'ingestion.ingesting': 'Ingestion...',
'ingestion.reindex': 'Ré-indexer',
'ingestion.indexed': 'Indexé',
'ingestion.notIndexed': 'Non indexé',
'ingestion.chunksIndexed': '{n} chunks indexés',
'ingestion.reindex': 'Ré-ingérer',
'ingestion.indexed': 'Ingéré',
'ingestion.notIndexed': 'Non ingéré',
'ingestion.chunksIndexed': '{n} chunks ingérés',
'ingestion.openInStudio': 'Ouvrir dans le Studio',
'ingestion.deleteIndex': "Supprimer de l'index",
'ingestion.deleteIndex': 'Retirer du store',
'ingestion.deleteConfirm':
'Retirer ce document de l\u2019index ? Les chunks seront supprimés mais le document source restera.',
'ingestion.unavailable': 'Ingestion non disponible',
'ingestion.filterAll': 'Tous',
'ingestion.filterIndexed': 'Indexés',
'ingestion.filterNotIndexed': 'Non indexés',
'ingestion.filterIndexed': 'Ingérés',
'ingestion.filterNotIndexed': 'Non ingérés',
'ingestion.sortName': 'Nom',
'ingestion.sortDate': 'Date',
'ingestion.search': 'Rechercher...',
'ingestion.searchChunks': 'Rechercher dans les chunks…',
'ingestion.noResults': 'Aucun résultat pour « {q} ».',
'ingestion.stepEmbedding': 'Embedding…',
'ingestion.stepIndexing': 'Indexation…',
'ingestion.stepIndexing': 'Ingestion…',
'ingestion.stepDone': 'Terminé',
'ingestion.opensearchConnected': 'OpenSearch connecté',
'ingestion.opensearchDisconnected': 'OpenSearch déconnecté',
@ -396,13 +396,19 @@ const messages: Messages = {
'chunks.diffAdded': 'ajout\u00e9(s)',
'chunks.diffModified': 'modifi\u00e9(s)',
'chunks.diffRemoved': 'supprim\u00e9(s)',
'chunks.pushTitle': 'Pousser vers un store',
'chunks.pushTitle': 'Ing\u00e9rer dans un store',
'chunks.pushStore': 'Store cible',
'chunks.pushPlaceholder': 'Nom du store\u2026',
'chunks.pushSummary': '{embeds} chunks \u00b7 ~{tokens} tokens',
'chunks.pushConfirm': 'Pousser',
'chunks.pushConfirm': 'Ing\u00e9rer',
'chunks.pushCancel': 'Annuler',
'chunks.pushedJob': 'Job lanc\u00e9 : {jobId}',
// Stale stores strip (#224)
'chunks.stale.pushedAt': 'ing\u00e9r\u00e9 le {date}',
'chunks.stale.reingest': 'R\u00e9-ing\u00e9rer',
'chunks.stale.reingestAll': 'R\u00e9-ing\u00e9rer tous les obsol\u00e8tes',
'chunks.stale.jobDispatched': 'Job lanc\u00e9 : {jobId}',
'chunks.selected': '{n} s\u00e9lectionn\u00e9(s)',
'chunks.bulkDrop': 'Supprimer la s\u00e9lection',
'chunks.bulkMerge': 'Fusionner la s\u00e9lection',
@ -450,9 +456,9 @@ const messages: Messages = {
'status.Failed': 'Failed',
'status.tooltip.Uploaded': 'File received, awaiting parsing.',
'status.tooltip.Parsed': 'Document parsed successfully.',
'status.tooltip.Chunked': 'Chunks generated, ready to push to a store.',
'status.tooltip.Ingested': 'Indexed in at least one store.',
'status.tooltip.Stale': 'Chunks modified since last push — re-ingestion required.',
'status.tooltip.Chunked': 'Chunks generated, ready to ingest into a store.',
'status.tooltip.Ingested': 'Ingested into at least one store.',
'status.tooltip.Stale': 'Chunks modified since last ingestion — re-ingestion required.',
'status.tooltip.Failed': 'A pipeline step failed. Retry to recover.',
// Document library (#211, #212, #213)
@ -470,14 +476,14 @@ const messages: Messages = {
'docs.filterClear': 'Clear filters',
'docs.selected': '{n} selected',
'docs.bulkRechunk': 'Re-chunk',
'docs.bulkPush': 'Push to store…',
'docs.bulkPush': 'Ingest into store…',
'docs.bulkDelete': 'Delete',
'docs.bulkCancel': 'Cancel selection',
'docs.deleteConfirm': 'Delete {n} document(s)? This action cannot be undone.',
'docs.pushTitle': 'Push to store',
'docs.pushTitle': 'Ingest into store',
'docs.pushLabel': 'Target store',
'docs.pushPlaceholder': 'Store name…',
'docs.pushSubmit': 'Push',
'docs.pushSubmit': 'Ingest',
'docs.pushCancel': 'Cancel',
'docs.jobDispatched': 'Job dispatched ({jobId})',
@ -710,32 +716,32 @@ const messages: Messages = {
'chunking.batchNotice':
'Chunking is not available for this analysis. Large documents processed in batch mode do not generate the internal structure required for chunking. Coming soon!',
'search.hint': 'Enter a term to search through indexed chunks.',
'search.hint': 'Enter a term to search through ingested chunks.',
'ingestion.ingest': 'Ingest',
'ingestion.document': 'Document',
'ingestion.chunkCount': 'Chunks ready',
'ingestion.successMessage': 'Indexing completed successfully!',
'ingestion.successMessage': 'Ingestion completed successfully!',
'ingestion.ingesting': 'Ingesting...',
'ingestion.reindex': 'Re-index',
'ingestion.indexed': 'Indexed',
'ingestion.notIndexed': 'Not indexed',
'ingestion.chunksIndexed': '{n} chunks indexed',
'ingestion.reindex': 'Re-ingest',
'ingestion.indexed': 'Ingested',
'ingestion.notIndexed': 'Not ingested',
'ingestion.chunksIndexed': '{n} chunks ingested',
'ingestion.openInStudio': 'Open in Studio',
'ingestion.deleteIndex': 'Remove from index',
'ingestion.deleteIndex': 'Remove from store',
'ingestion.deleteConfirm':
'Remove this document from the index? Chunks will be deleted but the source document will remain.',
'ingestion.unavailable': 'Ingestion unavailable',
'ingestion.filterAll': 'All',
'ingestion.filterIndexed': 'Indexed',
'ingestion.filterNotIndexed': 'Not indexed',
'ingestion.filterIndexed': 'Ingested',
'ingestion.filterNotIndexed': 'Not ingested',
'ingestion.sortName': 'Name',
'ingestion.sortDate': 'Date',
'ingestion.search': 'Search...',
'ingestion.searchChunks': 'Search indexed chunks…',
'ingestion.noResults': 'No results for "{q}".',
'ingestion.stepEmbedding': 'Embedding…',
'ingestion.stepIndexing': 'Indexing…',
'ingestion.stepIndexing': 'Ingesting…',
'ingestion.stepDone': 'Done',
'ingestion.opensearchConnected': 'OpenSearch connected',
'ingestion.opensearchDisconnected': 'OpenSearch unreachable',
@ -786,13 +792,19 @@ const messages: Messages = {
'chunks.diffAdded': 'added',
'chunks.diffModified': 'modified',
'chunks.diffRemoved': 'removed',
'chunks.pushTitle': 'Push to store',
'chunks.pushTitle': 'Ingest into store',
'chunks.pushStore': 'Target store',
'chunks.pushPlaceholder': 'Store name\u2026',
'chunks.pushSummary': '{embeds} chunks \u00b7 ~{tokens} tokens',
'chunks.pushConfirm': 'Push',
'chunks.pushConfirm': 'Ingest',
'chunks.pushCancel': 'Cancel',
'chunks.pushedJob': 'Job dispatched: {jobId}',
// Stale stores strip (#224)
'chunks.stale.pushedAt': 'ingested on {date}',
'chunks.stale.reingest': 'Re-ingest',
'chunks.stale.reingestAll': 'Re-ingest all stale',
'chunks.stale.jobDispatched': 'Job dispatched: {jobId}',
'chunks.selected': '{n} selected',
'chunks.bulkDrop': 'Drop selected',
'chunks.bulkMerge': 'Merge selected',

View file

@ -17,6 +17,13 @@ export type DocumentLifecycleState =
| 'Stale'
| 'Failed'
/** Per-store ingestion state for a document (#224). */
export interface DocStoreLink {
store: string
state: DocumentLifecycleState
pushedAt: string | null
}
export interface Document {
id: string
filename: string
@ -30,6 +37,8 @@ export interface Document {
lifecycleStateAt: string | null
/** Stores this document has been pushed to (added in E1 #203). */
stores?: string[]
/** Per-store state detail (#224). Present when backend returns storeLinks. */
storeLinks?: DocStoreLink[]
}
export interface PipelineOptions {