diff --git a/frontend/src/app/router/index.ts b/frontend/src/app/router/index.ts
index adb9397..d8745be 100644
--- a/frontend/src/app/router/index.ts
+++ b/frontend/src/app/router/index.ts
@@ -1,59 +1,8 @@
import { createRouter, createWebHistory } from 'vue-router'
-import type { RouteRecordRaw } from 'vue-router'
-const routes: RouteRecordRaw[] = [
- {
- path: '/',
- name: 'home',
- component: () => import('../../pages/HomePage.vue'),
- },
- {
- path: '/studio',
- name: 'studio',
- component: () => import('../../pages/StudioPage.vue'),
- },
- {
- path: '/history',
- name: 'history',
- component: () => import('../../pages/HistoryPage.vue'),
- },
- {
- path: '/documents',
- name: 'documents',
- component: () => import('../../pages/DocumentsPage.vue'),
- },
- {
- path: '/search',
- name: 'search',
- component: () => import('../../pages/SearchPage.vue'),
- },
- {
- // Reasoning-trace tunnel. Route is always registered; the page shows
- // an empty state when the `reasoning` feature flag is off (same pattern
- // as /search does for ingestion).
- path: '/reasoning',
- name: 'reasoning',
- component: () => import('../../pages/ReasoningPage.vue'),
- },
- {
- // Deep-link into a specific document's reasoning workspace, e.g. shared
- // by Peter to a teammate.
- path: '/reasoning/:docId',
- name: 'reasoning-doc',
- component: () => import('../../pages/ReasoningPage.vue'),
- props: true,
- },
- {
- path: '/settings',
- name: 'settings',
- component: () => import('../../pages/SettingsPage.vue'),
- },
- {
- path: '/:pathMatch(.*)*',
- name: 'not-found',
- redirect: '/',
- },
-]
+import { routes } from './routes'
+
+export { routes }
export const router = createRouter({
history: createWebHistory(),
diff --git a/frontend/src/app/router/router.test.ts b/frontend/src/app/router/router.test.ts
new file mode 100644
index 0000000..b43713b
--- /dev/null
+++ b/frontend/src/app/router/router.test.ts
@@ -0,0 +1,81 @@
+import { createMemoryHistory, createRouter } from 'vue-router'
+import { describe, expect, it } from 'vitest'
+
+import { routes } from './routes'
+import { ROUTES } from '../../shared/routing/names'
+
+/**
+ * Router test — uses `createMemoryHistory` so we don't need `window`
+ * (vitest defaults to the node environment for performance). The
+ * production router builds the same `routes` table on top of
+ * `createWebHistory` in `index.ts`.
+ */
+const buildRouter = () => createRouter({ history: createMemoryHistory(), routes })
+
+describe('router', () => {
+ it('resolves every 0.6.0 doc-centric route to a component', () => {
+ const router = buildRouter()
+ const cases: Array<{ path: string; name: string }> = [
+ { path: '/docs', name: ROUTES.DOCS_LIBRARY },
+ { path: '/docs/new', name: ROUTES.DOCS_NEW },
+ { path: '/docs/abc', name: ROUTES.DOC_WORKSPACE },
+ { path: '/index', name: ROUTES.STORES_LIST },
+ { path: '/index/foo', name: ROUTES.STORE_DETAIL },
+ { path: '/index/foo/query', name: ROUTES.STORE_QUERY },
+ { path: '/runs', name: ROUTES.RUNS },
+ { path: '/runs/run-42', name: ROUTES.RUN_DETAIL },
+ ]
+ for (const c of cases) {
+ const resolved = router.resolve(c.path)
+ expect(resolved.name, `route ${c.path}`).toBe(c.name)
+ expect(resolved.matched.length, `route ${c.path} has a component`).toBeGreaterThan(0)
+ }
+ })
+
+ it('keeps legacy routes functional', () => {
+ const router = buildRouter()
+ expect(router.resolve('/').name).toBe(ROUTES.HOME)
+ expect(router.resolve('/studio').name).toBe(ROUTES.STUDIO)
+ expect(router.resolve('/documents').name).toBe(ROUTES.DOCUMENTS)
+ expect(router.resolve('/history').name).toBe(ROUTES.HISTORY)
+ expect(router.resolve('/search').name).toBe(ROUTES.SEARCH)
+ expect(router.resolve('/reasoning').name).toBe(ROUTES.REASONING)
+ expect(router.resolve('/reasoning/abc').name).toBe(ROUTES.REASONING_DOC)
+ expect(router.resolve('/settings').name).toBe(ROUTES.SETTINGS)
+ })
+
+ it('passes id and parsed mode to the doc workspace as props', () => {
+ const router = buildRouter()
+ const route = router.resolve({
+ name: ROUTES.DOC_WORKSPACE,
+ params: { id: 'abc' },
+ query: { mode: 'chunks' },
+ })
+ const propsFn = route.matched[0]?.props as
+ | { default?: (r: typeof route) => unknown }
+ | undefined
+ const computed = (propsFn?.default ?? (() => null))(route) as { id: string; mode: string }
+ expect(computed.id).toBe('abc')
+ expect(computed.mode).toBe('chunks')
+ })
+
+ it('falls back to ask when mode is unknown', () => {
+ const router = buildRouter()
+ const route = router.resolve({
+ name: ROUTES.DOC_WORKSPACE,
+ params: { id: 'abc' },
+ query: { mode: 'garbage' },
+ })
+ const propsFn = route.matched[0]?.props as
+ | { default?: (r: typeof route) => unknown }
+ | undefined
+ const computed = (propsFn?.default ?? (() => null))(route) as { mode: string }
+ expect(computed.mode).toBe('ask')
+ })
+
+ it('redirects unknown paths to /', () => {
+ const router = buildRouter()
+ const resolved = router.resolve('/nope/this/does/not/exist')
+ expect(resolved.matched[0]?.redirect).toBeDefined()
+ })
+})
diff --git a/frontend/src/app/router/routes.ts b/frontend/src/app/router/routes.ts
new file mode 100644
index 0000000..dfe2da2
--- /dev/null
+++ b/frontend/src/app/router/routes.ts
@@ -0,0 +1,125 @@
+import type { RouteLocationNormalized, RouteRecordRaw } from 'vue-router'
+
+import { parseMode } from '../../shared/routing/modes'
+import { ROUTES } from '../../shared/routing/names'
+
+/**
+ * Route table used by the production router and by tests.
+ *
+ * Lives in its own file so importing the router definition doesn't
+ * trigger `createWebHistory()` (which needs `window` and breaks the
+ * default node-environment vitest tests). Tests build a router with
+ * `createMemoryHistory()` over this same table.
+ */
+export const routes: RouteRecordRaw[] = [
+ // ---------------------------------------------------------------------------
+ // Legacy routes — kept functional during the 0.6.0 transition.
+ // ---------------------------------------------------------------------------
+ {
+ path: '/',
+ name: ROUTES.HOME,
+ component: () => import('../../pages/HomePage.vue'),
+ },
+ {
+ path: '/studio',
+ name: ROUTES.STUDIO,
+ component: () => import('../../pages/StudioPage.vue'),
+ },
+ {
+ path: '/history',
+ name: ROUTES.HISTORY,
+ component: () => import('../../pages/HistoryPage.vue'),
+ },
+ {
+ path: '/documents',
+ name: ROUTES.DOCUMENTS,
+ component: () => import('../../pages/DocumentsPage.vue'),
+ },
+ {
+ path: '/search',
+ name: ROUTES.SEARCH,
+ component: () => import('../../pages/SearchPage.vue'),
+ },
+ {
+ // Reasoning-trace tunnel. Route is always registered; the page shows
+ // an empty state when the `reasoning` feature flag is off (same pattern
+ // as /search does for ingestion).
+ path: '/reasoning',
+ name: ROUTES.REASONING,
+ component: () => import('../../pages/ReasoningPage.vue'),
+ },
+ {
+ // Deep-link into a specific document's reasoning workspace, e.g. shared
+ // by Peter to a teammate.
+ path: '/reasoning/:docId',
+ name: ROUTES.REASONING_DOC,
+ component: () => import('../../pages/ReasoningPage.vue'),
+ props: true,
+ },
+ {
+ path: '/settings',
+ name: ROUTES.SETTINGS,
+ component: () => import('../../pages/SettingsPage.vue'),
+ },
+
+ // ---------------------------------------------------------------------------
+ // 0.6.0 — Document-centric routes (#207). Placeholder pages until E3/E4/E5
+ // implement them; the legacy routes above keep working in parallel.
+ // ---------------------------------------------------------------------------
+ {
+ path: '/docs',
+ name: ROUTES.DOCS_LIBRARY,
+ component: () => import('../../pages/DocsLibraryPage.vue'),
+ },
+ {
+ path: '/docs/new',
+ name: ROUTES.DOCS_NEW,
+ component: () => import('../../pages/DocsNewPage.vue'),
+ },
+ {
+ path: '/docs/:id',
+ name: ROUTES.DOC_WORKSPACE,
+ component: () => import('../../pages/DocWorkspacePage.vue'),
+ props: (route: RouteLocationNormalized) => ({
+ id: String(route.params.id),
+ mode: parseMode(route.query.mode),
+ }),
+ },
+ {
+ path: '/index',
+ name: ROUTES.STORES_LIST,
+ component: () => import('../../pages/StoresListPage.vue'),
+ },
+ {
+ path: '/index/:store',
+ name: ROUTES.STORE_DETAIL,
+ component: () => import('../../pages/StoreDetailPage.vue'),
+ props: true,
+ },
+ {
+ path: '/index/:store/query',
+ name: ROUTES.STORE_QUERY,
+ component: () => import('../../pages/StoreQueryPage.vue'),
+ props: true,
+ },
+ {
+ path: '/runs',
+ name: ROUTES.RUNS,
+ component: () => import('../../pages/RunsPage.vue'),
+ },
+ {
+ path: '/runs/:id',
+ name: ROUTES.RUN_DETAIL,
+ component: () => import('../../pages/RunDetailPage.vue'),
+ props: true,
+ },
+
+ // ---------------------------------------------------------------------------
+ // 404 — must come last.
+ // ---------------------------------------------------------------------------
+ {
+ path: '/:pathMatch(.*)*',
+ name: ROUTES.NOT_FOUND,
+ redirect: '/',
+ },
+]
diff --git a/frontend/src/pages/DocWorkspacePage.vue b/frontend/src/pages/DocWorkspacePage.vue
new file mode 100644
index 0000000..37ea58f
--- /dev/null
+++ b/frontend/src/pages/DocWorkspacePage.vue
@@ -0,0 +1,27 @@
+
+
+
+
+
diff --git a/frontend/src/pages/DocsLibraryPage.vue b/frontend/src/pages/DocsLibraryPage.vue
new file mode 100644
index 0000000..9449be2
--- /dev/null
+++ b/frontend/src/pages/DocsLibraryPage.vue
@@ -0,0 +1,14 @@
+
+
+
+
+
diff --git a/frontend/src/pages/DocsNewPage.vue b/frontend/src/pages/DocsNewPage.vue
new file mode 100644
index 0000000..46df169
--- /dev/null
+++ b/frontend/src/pages/DocsNewPage.vue
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/frontend/src/pages/RunDetailPage.vue b/frontend/src/pages/RunDetailPage.vue
new file mode 100644
index 0000000..8243632
--- /dev/null
+++ b/frontend/src/pages/RunDetailPage.vue
@@ -0,0 +1,20 @@
+
+
+
+
+
diff --git a/frontend/src/pages/RunsPage.vue b/frontend/src/pages/RunsPage.vue
new file mode 100644
index 0000000..6a65c9f
--- /dev/null
+++ b/frontend/src/pages/RunsPage.vue
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/frontend/src/pages/StoreDetailPage.vue b/frontend/src/pages/StoreDetailPage.vue
new file mode 100644
index 0000000..c8cdae2
--- /dev/null
+++ b/frontend/src/pages/StoreDetailPage.vue
@@ -0,0 +1,20 @@
+
+
+
+
+
diff --git a/frontend/src/pages/StoreQueryPage.vue b/frontend/src/pages/StoreQueryPage.vue
new file mode 100644
index 0000000..dcfa6ad
--- /dev/null
+++ b/frontend/src/pages/StoreQueryPage.vue
@@ -0,0 +1,20 @@
+
+
+
+
+
diff --git a/frontend/src/pages/StoresListPage.vue b/frontend/src/pages/StoresListPage.vue
new file mode 100644
index 0000000..f846106
--- /dev/null
+++ b/frontend/src/pages/StoresListPage.vue
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/frontend/src/shared/i18n.ts b/frontend/src/shared/i18n.ts
index a7d6e6e..3b74247 100644
--- a/frontend/src/shared/i18n.ts
+++ b/frontend/src/shared/i18n.ts
@@ -19,6 +19,26 @@ const messages: Messages = {
// Top bar
'topbar.newAnalysis': 'Nouvelle analyse',
+ // Coming-soon placeholders (0.6.0 doc-centric routes — #207)
+ 'comingSoon.title': 'Bientôt disponible',
+ 'comingSoon.subtitle.docsLibrary':
+ "La bibliothèque de documents arrive avec la 0.6.0. Vous y verrez l'état du cycle de vie de chaque document, ses stores et ses dernières mises à jour.",
+ 'comingSoon.subtitle.docsNew':
+ "L'import multi-fichiers (drop d'un dossier ou sélection multiple) arrive avec la 0.6.0.",
+ 'comingSoon.subtitle.docWorkspace':
+ "L'espace de travail document (Inspect / Chunks / Ask) arrive avec la 0.6.0.",
+ 'comingSoon.subtitle.stores': 'La liste des stores arrive avec la 0.6.0.',
+ 'comingSoon.subtitle.storeDetail':
+ 'La vue détaillée du store (documents présents, état par store) arrive avec la 0.6.0.',
+ 'comingSoon.subtitle.storeQuery': 'Le playground de requête RAG arrive avec la 0.6.0.',
+ 'comingSoon.subtitle.runs': "L'historique des runs (audit / debug) arrive avec la 0.6.0.",
+ 'comingSoon.subtitle.runDetail': "Le détail d'un run arrive avec la 0.6.0.",
+ 'comingSoon.hint.docWorkspace': 'doc {id} · mode {mode}',
+ 'comingSoon.hint.storeDetail': 'store {store}',
+ 'comingSoon.hint.storeQuery': 'store {store}',
+ 'comingSoon.hint.runDetail': 'run {id}',
+ 'comingSoon.backHome': "Retour à l'accueil",
+
// Home
'home.title': 'Docling Studio',
'home.subtitle':
@@ -287,6 +307,26 @@ const messages: Messages = {
'topbar.newAnalysis': 'New analysis',
+ // Coming-soon placeholders (0.6.0 doc-centric routes — #207)
+ 'comingSoon.title': 'Coming soon',
+ 'comingSoon.subtitle.docsLibrary':
+ 'The document library lands with 0.6.0. It will show every document with its lifecycle state, the stores it lives in, and when it was last updated.',
+ 'comingSoon.subtitle.docsNew':
+ 'Multi-file import (drop a folder or pick multiple files) lands with 0.6.0.',
+ 'comingSoon.subtitle.docWorkspace':
+ 'The doc workspace (Inspect / Chunks / Ask) lands with 0.6.0.',
+ 'comingSoon.subtitle.stores': 'The stores list lands with 0.6.0.',
+ 'comingSoon.subtitle.storeDetail':
+ 'The store detail view (docs present, per-store state) lands with 0.6.0.',
+ 'comingSoon.subtitle.storeQuery': 'The RAG query playground lands with 0.6.0.',
+ 'comingSoon.subtitle.runs': 'The runs history (audit / debug) lands with 0.6.0.',
+ 'comingSoon.subtitle.runDetail': 'Run detail lands with 0.6.0.',
+ 'comingSoon.hint.docWorkspace': 'doc {id} · mode {mode}',
+ 'comingSoon.hint.storeDetail': 'store {store}',
+ 'comingSoon.hint.storeQuery': 'store {store}',
+ 'comingSoon.hint.runDetail': 'run {id}',
+ 'comingSoon.backHome': 'Back to home',
+
'home.title': 'Docling Studio',
'home.subtitle':
'Analyze, explore and validate the structure of your PDF documents with Docling.',
diff --git a/frontend/src/shared/routing/modes.test.ts b/frontend/src/shared/routing/modes.test.ts
new file mode 100644
index 0000000..7de79fa
--- /dev/null
+++ b/frontend/src/shared/routing/modes.test.ts
@@ -0,0 +1,35 @@
+import { describe, expect, it } from 'vitest'
+
+import { ALL_MODES, DEFAULT_MODE, isDocMode, parseMode } from './modes'
+
+describe('isDocMode', () => {
+ it.each(['ask', 'inspect', 'chunks'])('accepts %s', (value) => {
+ expect(isDocMode(value)).toBe(true)
+ })
+
+ it.each([undefined, null, '', 'foo', 42, {}, []])('rejects %s', (value) => {
+ expect(isDocMode(value)).toBe(false)
+ })
+})
+
+describe('parseMode', () => {
+ it('returns the default for missing or unknown values', () => {
+ expect(parseMode(undefined)).toBe(DEFAULT_MODE)
+ expect(parseMode(null)).toBe(DEFAULT_MODE)
+ expect(parseMode('garbage')).toBe(DEFAULT_MODE)
+ expect(parseMode(['chunks'])).toBe(DEFAULT_MODE) // arrays not accepted
+ })
+
+ it.each(['ask', 'inspect', 'chunks'] as const)('respects %s', (mode) => {
+ expect(parseMode(mode)).toBe(mode)
+ })
+})
+
+describe('ALL_MODES', () => {
+ it('lists every mode exactly once', () => {
+ expect(new Set(ALL_MODES).size).toBe(ALL_MODES.length)
+ expect(ALL_MODES).toContain('ask')
+ expect(ALL_MODES).toContain('inspect')
+ expect(ALL_MODES).toContain('chunks')
+ })
+})
diff --git a/frontend/src/shared/routing/modes.ts b/frontend/src/shared/routing/modes.ts
new file mode 100644
index 0000000..3cccc8e
--- /dev/null
+++ b/frontend/src/shared/routing/modes.ts
@@ -0,0 +1,24 @@
+/**
+ * Doc workspace mode parsing.
+ *
+ * The doc workspace at `/docs/:id` exposes three modes via the `?mode=`
+ * query param. Anything missing or unknown resolves to the default,
+ * `ask`, so a malformed URL never produces a broken page.
+ *
+ * #210 layers feature-flag-aware redirection on top: if the requested
+ * mode is disabled for the current tenant, the router replaces it with
+ * the first enabled mode (priority `ask` > `chunks` > `inspect`).
+ */
+
+export type DocMode = 'ask' | 'inspect' | 'chunks'
+
+export const DEFAULT_MODE: DocMode = 'ask'
+export const ALL_MODES: readonly DocMode[] = ['ask', 'inspect', 'chunks'] as const
+
+export function isDocMode(value: unknown): value is DocMode {
+ return value === 'ask' || value === 'inspect' || value === 'chunks'
+}
+
+export function parseMode(raw: unknown): DocMode {
+ return isDocMode(raw) ? raw : DEFAULT_MODE
+}
diff --git a/frontend/src/shared/routing/names.ts b/frontend/src/shared/routing/names.ts
new file mode 100644
index 0000000..4c2e89b
--- /dev/null
+++ b/frontend/src/shared/routing/names.ts
@@ -0,0 +1,35 @@
+/**
+ * Canonical route name constants — typed to keep callers honest.
+ *
+ * Use:
+ *
+ * router.push({ name: ROUTES.DOC_WORKSPACE, params: { id } })
+ *
+ * instead of stringly-typed names. Adding a route requires touching
+ * exactly two places: the router definition and this file.
+ */
+
+export const ROUTES = {
+ // Existing legacy routes — kept until E3/E4/E5 replace them.
+ HOME: 'home',
+ STUDIO: 'studio',
+ HISTORY: 'history',
+ DOCUMENTS: 'documents',
+ SEARCH: 'search',
+ REASONING: 'reasoning',
+ REASONING_DOC: 'reasoning-doc',
+ SETTINGS: 'settings',
+ NOT_FOUND: 'not-found',
+
+ // 0.6.0 — Document-centric routes (#207).
+ DOCS_LIBRARY: 'docs-library',
+ DOCS_NEW: 'docs-new',
+ DOC_WORKSPACE: 'doc-workspace',
+ STORES_LIST: 'stores-list',
+ STORE_DETAIL: 'store-detail',
+ STORE_QUERY: 'store-query',
+ RUNS: 'runs',
+ RUN_DETAIL: 'run-detail',
+} as const
+
+export type RouteName = (typeof ROUTES)[keyof typeof ROUTES]
diff --git a/frontend/src/shared/ui/ComingSoonShell.vue b/frontend/src/shared/ui/ComingSoonShell.vue
new file mode 100644
index 0000000..89ed35b
--- /dev/null
+++ b/frontend/src/shared/ui/ComingSoonShell.vue
@@ -0,0 +1,101 @@
+
+
+
+
0.6.0
+
{{ title }}
+
{{ subtitle }}
+
{{ hint }}
+
+ {{ t('comingSoon.backHome') }}
+
+
+
+
+
+
+
+