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:
parent
45cecc1115
commit
03e5dfac17
4 changed files with 38 additions and 15 deletions
|
|
@ -33,7 +33,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 { useI18n } from '../../../shared/i18n'
|
||||||
import { fetchDocumentGraph, type GraphPayload } from '../graphApi'
|
import { fetchDocumentGraph, type GraphPayload } from '../graphApi'
|
||||||
|
|
||||||
|
|
@ -90,11 +90,12 @@ async function load(): Promise<void> {
|
||||||
payload.value = await fetchDocumentGraph(props.docId)
|
payload.value = await fetchDocumentGraph(props.docId)
|
||||||
if (!payload.value.nodes.length) {
|
if (!payload.value.nodes.length) {
|
||||||
empty.value = true
|
empty.value = true
|
||||||
} else {
|
return
|
||||||
// Wait for template to render the canvas before initializing Cytoscape.
|
|
||||||
await new Promise((r) => requestAnimationFrame(r))
|
|
||||||
await renderGraph()
|
|
||||||
}
|
}
|
||||||
|
// Flip loading off so the canvas <div> mounts, then wait a tick before init.
|
||||||
|
loading.value = false
|
||||||
|
await nextTick()
|
||||||
|
await renderGraph()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = (e as Error).message || 'Failed to load graph'
|
error.value = (e as Error).message || 'Failed to load graph'
|
||||||
console.error('Failed to load graph', e)
|
console.error('Failed to load graph', e)
|
||||||
|
|
|
||||||
|
|
@ -106,12 +106,6 @@
|
||||||
|
|
||||||
<!-- IMAGES -->
|
<!-- IMAGES -->
|
||||||
<ImageGallery v-else-if="activeTab === 'images'" :pages="currentPageAsArray" />
|
<ImageGallery v-else-if="activeTab === 'images'" :pages="currentPageAsArray" />
|
||||||
|
|
||||||
<!-- GRAPH -->
|
|
||||||
<GraphView
|
|
||||||
v-else-if="activeTab === 'graph'"
|
|
||||||
:doc-id="store.currentAnalysis?.documentId ?? null"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="store.currentAnalysis?.status === 'RUNNING'" class="result-placeholder">
|
<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 { useAnalysisStore } from '../store'
|
||||||
import MarkdownViewer from './MarkdownViewer.vue'
|
import MarkdownViewer from './MarkdownViewer.vue'
|
||||||
import ImageGallery from './ImageGallery.vue'
|
import ImageGallery from './ImageGallery.vue'
|
||||||
import GraphView from './GraphView.vue'
|
|
||||||
import { useI18n } from '../../../shared/i18n'
|
import { useI18n } from '../../../shared/i18n'
|
||||||
import type { PageElement } from '../../../shared/types'
|
import type { PageElement } from '../../../shared/types'
|
||||||
|
|
||||||
|
|
@ -230,7 +223,6 @@ const tabs = computed(() => [
|
||||||
{ id: 'elements', label: t('results.elements') },
|
{ id: 'elements', label: t('results.elements') },
|
||||||
{ id: 'markdown', label: t('results.markdown') },
|
{ id: 'markdown', label: t('results.markdown') },
|
||||||
{ id: 'images', label: t('results.images') },
|
{ id: 'images', label: t('results.images') },
|
||||||
{ id: 'graph', label: t('results.graph') },
|
|
||||||
])
|
])
|
||||||
|
|
||||||
const totalPages = computed(() => store.currentPages.length)
|
const totalPages = computed(() => store.currentPages.length)
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,27 @@
|
||||||
</svg>
|
</svg>
|
||||||
{{ t('studio.ingest') }}
|
{{ t('studio.ingest') }}
|
||||||
</button>
|
</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>
|
</div>
|
||||||
<div class="topbar-actions">
|
<div class="topbar-actions">
|
||||||
|
|
@ -475,6 +496,11 @@
|
||||||
:chunk-count="analysisStore.currentChunks?.length ?? 0"
|
:chunk-count="analysisStore.currentChunks?.length ?? 0"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- MAINTAIN MODE -->
|
||||||
|
<div v-if="mode === 'maintain'" class="maintain-panel">
|
||||||
|
<GraphView :doc-id="analysisStore.currentAnalysis?.documentId ?? null" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -489,6 +515,7 @@ import { useIngestionStore } from '../features/ingestion/store'
|
||||||
import { DocumentUpload, DocumentList } from '../features/document/index'
|
import { DocumentUpload, DocumentList } from '../features/document/index'
|
||||||
import { ResultTabs } from '../features/analysis/index'
|
import { ResultTabs } from '../features/analysis/index'
|
||||||
import BboxOverlay from '../features/analysis/ui/BboxOverlay.vue'
|
import BboxOverlay from '../features/analysis/ui/BboxOverlay.vue'
|
||||||
|
import GraphView from '../features/analysis/ui/GraphView.vue'
|
||||||
import { ChunkPanel } from '../features/chunking'
|
import { ChunkPanel } from '../features/chunking'
|
||||||
import { IngestPanel } from '../features/ingestion'
|
import { IngestPanel } from '../features/ingestion'
|
||||||
import { useFeatureFlag } from '../features/feature-flags'
|
import { useFeatureFlag } from '../features/feature-flags'
|
||||||
|
|
@ -1404,10 +1431,11 @@ onBeforeUnmount(() => {
|
||||||
padding-top: 16px;
|
padding-top: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Verify / Prepare / Ingest panels */
|
/* Verify / Prepare / Ingest / Maintain panels */
|
||||||
.verify-panel,
|
.verify-panel,
|
||||||
.prepare-panel,
|
.prepare-panel,
|
||||||
.ingest-panel-wrapper {
|
.ingest-panel-wrapper,
|
||||||
|
.maintain-panel {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,7 @@ const messages: Messages = {
|
||||||
// Chunking
|
// Chunking
|
||||||
'studio.prepare': 'Préparer',
|
'studio.prepare': 'Préparer',
|
||||||
'studio.ingest': 'Ingérer',
|
'studio.ingest': 'Ingérer',
|
||||||
|
'studio.maintain': 'Maintenir',
|
||||||
'chunking.settings': 'Chunking',
|
'chunking.settings': 'Chunking',
|
||||||
'chunking.chunkerType': 'Type de chunker',
|
'chunking.chunkerType': 'Type de chunker',
|
||||||
'chunking.maxTokens': 'Tokens max',
|
'chunking.maxTokens': 'Tokens max',
|
||||||
|
|
@ -287,6 +288,7 @@ const messages: Messages = {
|
||||||
|
|
||||||
'studio.prepare': 'Prepare',
|
'studio.prepare': 'Prepare',
|
||||||
'studio.ingest': 'Ingest',
|
'studio.ingest': 'Ingest',
|
||||||
|
'studio.maintain': 'Maintain',
|
||||||
'chunking.settings': 'Chunking',
|
'chunking.settings': 'Chunking',
|
||||||
'chunking.chunkerType': 'Chunker type',
|
'chunking.chunkerType': 'Chunker type',
|
||||||
'chunking.maxTokens': 'Max tokens',
|
'chunking.maxTokens': 'Max tokens',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue