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)
107 lines
3.2 KiB
TypeScript
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,
|
|
}
|
|
})
|