docling-studio/frontend/src/features/feature-flags/store.ts
Pier-Jean Malandrino 8103460e9c feat(reasoning): reasoning-trace viewer v1 with SQLite-backed graph
Adds the `docling-agent` reasoning-trace viewer as a Studio tunnel, per
`docs/design/reasoning-trace.md`. Users pick an analyzed document, import
a RAGResult JSON, and the iterations are overlaid on the document graph.

Graph source is decoupled from Neo4j: a new pure builder
(`infra/docling_graph.build_graph_payload`) reads `document_json` from
SQLite and emits the same Cytoscape-shaped payload that `fetch_graph`
returns from Neo4j. Neo4j stays exclusive to the Maintain ingestion
pipeline. Shared DoclingDocument helpers live in `infra/docling_tree.py`
so TreeWriter and the builder can't drift on label taxonomy or tree walks.

Also removes the Cytoscape minimap (cytoscape-navigator) from GraphView:
second render instance hurt perf on large documents for no UX win.

Backend
- new `GET /api/documents/:id/reasoning-graph` (SQLite-only)
- new `infra/docling_tree.py`, `infra/docling_graph.py`
- `analysis_repo.find_latest_completed_by_document`
- tests: `test_docling_graph.py` (builder), `test_graph_api.py` (endpoint)

Frontend
- `features/reasoning/` — store, overlay, types, panel, import dialog,
  workspace, doc picker
- new `ReasoningPage` + `/reasoning` and `/reasoning/:docId` routes
- `GraphView` gains a `fetcher` prop so reasoning can inject the
  SQLite-backed fetcher while Maintain keeps using the Neo4j one
- drops minimap (nav container, dep, CSS)
- legend filters + section parenting extracted for reuse
- i18n base strings (FR + EN)
2026-04-29 14:00:00 +02:00

107 lines
3.2 KiB
TypeScript

import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { apiFetch } from '../../shared/api/http'
import { appMaxFileSizeMb, appMaxPageCount } from '../../shared/appConfig'
type ConversionEngine = 'local' | 'remote'
type DeploymentMode = 'self-hosted' | 'huggingface'
interface HealthResponse {
status: string
version?: string
engine: ConversionEngine
deploymentMode?: DeploymentMode
maxPageCount?: number
maxFileSizeMb?: number
ingestionAvailable?: boolean
}
export type FeatureFlag = 'chunking' | 'disclaimer' | 'ingestion' | 'reasoning'
interface FeatureFlagDef {
description: string
isEnabled: (ctx: FeatureFlagContext) => boolean
}
interface FeatureFlagContext {
engine: ConversionEngine | null
deploymentMode: DeploymentMode | null
ingestionAvailable: boolean
}
const featureRegistry: Record<FeatureFlag, FeatureFlagDef> = {
chunking: {
description: 'Document chunking for RAG preparation',
isEnabled: (ctx) => ctx.engine !== null,
},
disclaimer: {
description: 'Show shared-instance disclaimer banner',
isEnabled: (ctx) => ctx.deploymentMode === 'huggingface',
},
ingestion: {
description: 'OpenSearch ingestion pipeline (embedding + vector indexing)',
isEnabled: (ctx) => ctx.ingestionAvailable,
},
reasoning: {
// SQLite-backed (builds the graph from `document_json` on the fly), so no
// server-side gating needed. Kept as a flag so a future deployment can
// still kill-switch the UI if it wants to.
description: 'Reasoning trace tunnel (docling-agent RAGResult viewer)',
isEnabled: () => true,
},
}
export const useFeatureFlagStore = defineStore('feature-flags', () => {
const engine = ref<ConversionEngine | null>(null)
const deploymentMode = ref<DeploymentMode | null>(null)
const maxPageCount = ref<number>(0)
const maxFileSizeMb = ref<number>(0)
const ingestionAvailable = ref(false)
const appVersion = ref<string>(__APP_VERSION__)
const loaded = ref(false)
const error = ref<string | null>(null)
const context = computed<FeatureFlagContext>(() => ({
engine: engine.value,
deploymentMode: deploymentMode.value,
ingestionAvailable: ingestionAvailable.value,
}))
function isEnabled(flag: FeatureFlag): boolean {
if (!loaded.value) return false
const def = featureRegistry[flag]
return def.isEnabled(context.value)
}
async function load(): Promise<void> {
try {
const data = await apiFetch<HealthResponse>('/api/health')
engine.value = data.engine
deploymentMode.value = data.deploymentMode ?? 'self-hosted'
maxPageCount.value = data.maxPageCount ?? 0
maxFileSizeMb.value = data.maxFileSizeMb ?? 0
ingestionAvailable.value = data.ingestionAvailable ?? false
appMaxFileSizeMb.value = maxFileSizeMb.value
appMaxPageCount.value = maxPageCount.value
if (data.version) appVersion.value = data.version
loaded.value = true
error.value = null
} catch (e) {
error.value = e instanceof Error ? e.message : 'Failed to load feature flags'
loaded.value = true
}
}
return {
engine,
deploymentMode,
maxPageCount,
maxFileSizeMb,
ingestionAvailable,
appVersion,
loaded,
error,
isEnabled,
load,
}
})