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:
parent
cab3e53fe4
commit
e44fbb134e
5 changed files with 209 additions and 37 deletions
143
frontend/src/features/chunks/ui/StaleStoresStrip.vue
Normal file
143
frontend/src/features/chunks/ui/StaleStoresStrip.vue
Normal 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>
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue