feat(ui): add Maintain step with graph visualization

Move the graph view from a Verify-tab (where it sat post-analysis, off
the main flow) to a dedicated Maintain step after Ingest, so the graph
result is visible at the natural end of the Configure → Verify →
Prepare → Ingest → Maintain pipeline.

- StudioPage: new 'maintain' mode toggle + panel rendering GraphView
- ResultTabs: remove obsolete graph tab
- i18n: add studio.maintain (fr + en)
- GraphView: fix init order — flip loading off and await nextTick before
  renderGraph so the canvas <div> is mounted when cytoscape reads its
  container ref (previous code bailed silently on null ref)
This commit is contained in:
Pier-Jean Malandrino 2026-04-20 10:13:08 +02:00
parent 45cecc1115
commit 03e5dfac17
4 changed files with 38 additions and 15 deletions

View file

@ -33,7 +33,7 @@
</template>
<script setup lang="ts">
import { onMounted, onBeforeUnmount, ref, watch } from 'vue'
import { onMounted, onBeforeUnmount, ref, watch, nextTick } from 'vue'
import { useI18n } from '../../../shared/i18n'
import { fetchDocumentGraph, type GraphPayload } from '../graphApi'
@ -90,11 +90,12 @@ async function load(): Promise<void> {
payload.value = await fetchDocumentGraph(props.docId)
if (!payload.value.nodes.length) {
empty.value = true
} else {
// Wait for template to render the canvas before initializing Cytoscape.
await new Promise((r) => requestAnimationFrame(r))
await renderGraph()
return
}
// Flip loading off so the canvas <div> mounts, then wait a tick before init.
loading.value = false
await nextTick()
await renderGraph()
} catch (e) {
error.value = (e as Error).message || 'Failed to load graph'
console.error('Failed to load graph', e)

View file

@ -106,12 +106,6 @@
<!-- IMAGES -->
<ImageGallery v-else-if="activeTab === 'images'" :pages="currentPageAsArray" />
<!-- GRAPH -->
<GraphView
v-else-if="activeTab === 'graph'"
:doc-id="store.currentAnalysis?.documentId ?? null"
/>
</div>
</div>
<div v-else-if="store.currentAnalysis?.status === 'RUNNING'" class="result-placeholder">
@ -199,7 +193,6 @@ import { ref, computed, reactive } from 'vue'
import { useAnalysisStore } from '../store'
import MarkdownViewer from './MarkdownViewer.vue'
import ImageGallery from './ImageGallery.vue'
import GraphView from './GraphView.vue'
import { useI18n } from '../../../shared/i18n'
import type { PageElement } from '../../../shared/types'
@ -230,7 +223,6 @@ const tabs = computed(() => [
{ id: 'elements', label: t('results.elements') },
{ id: 'markdown', label: t('results.markdown') },
{ id: 'images', label: t('results.images') },
{ id: 'graph', label: t('results.graph') },
])
const totalPages = computed(() => store.currentPages.length)

View file

@ -84,6 +84,27 @@
</svg>
{{ t('studio.ingest') }}
</button>
<button
v-if="chunkingEnabled && ingestionEnabled && ingestionStore.available"
class="toggle-btn"
data-e2e="toggle-btn maintain-btn"
:class="{ active: mode === 'maintain' }"
@click="mode = 'maintain'"
:disabled="!analysisStore.currentAnalysis"
>
<svg class="toggle-icon" viewBox="0 0 20 20" fill="currentColor">
<path
d="M10 3.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM6 10a4 4 0 118 0 4 4 0 01-8 0zm4-2a2 2 0 100 4 2 2 0 000-4z"
/>
<path
d="M10 1v2M10 17v2M1 10h2M17 10h2M3.5 3.5l1.4 1.4M15.1 15.1l1.4 1.4M3.5 16.5l1.4-1.4M15.1 4.9l1.4-1.4"
stroke="currentColor"
stroke-width="1.5"
fill="none"
/>
</svg>
{{ t('studio.maintain') }}
</button>
</div>
</div>
<div class="topbar-actions">
@ -475,6 +496,11 @@
:chunk-count="analysisStore.currentChunks?.length ?? 0"
/>
</div>
<!-- MAINTAIN MODE -->
<div v-if="mode === 'maintain'" class="maintain-panel">
<GraphView :doc-id="analysisStore.currentAnalysis?.documentId ?? null" />
</div>
</div>
</div>
</div>
@ -489,6 +515,7 @@ 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'
import GraphView from '../features/analysis/ui/GraphView.vue'
import { ChunkPanel } from '../features/chunking'
import { IngestPanel } from '../features/ingestion'
import { useFeatureFlag } from '../features/feature-flags'
@ -1404,10 +1431,11 @@ onBeforeUnmount(() => {
padding-top: 16px;
}
/* Verify / Prepare / Ingest panels */
/* Verify / Prepare / Ingest / Maintain panels */
.verify-panel,
.prepare-panel,
.ingest-panel-wrapper {
.ingest-panel-wrapper,
.maintain-panel {
height: 100%;
overflow: hidden;
display: flex;

View file

@ -114,6 +114,7 @@ const messages: Messages = {
// Chunking
'studio.prepare': 'Préparer',
'studio.ingest': 'Ingérer',
'studio.maintain': 'Maintenir',
'chunking.settings': 'Chunking',
'chunking.chunkerType': 'Type de chunker',
'chunking.maxTokens': 'Tokens max',
@ -287,6 +288,7 @@ const messages: Messages = {
'studio.prepare': 'Prepare',
'studio.ingest': 'Ingest',
'studio.maintain': 'Maintain',
'chunking.settings': 'Chunking',
'chunking.chunkerType': 'Chunker type',
'chunking.maxTokens': 'Max tokens',