Adapted from docs/developer/FrontendArchitecture.md in the Wikantik repository; current as of release 2.4.56.
The wikantik-frontend module implements Wikantik's user interface as a React
19 single-page application. It is built with Vite as part of the standard
Maven build, bundled into the WAR, and served at the web root — there is no
separate hosting, no CORS configuration, and no legacy JSP templates alongside
it.
Fully implemented. The SPA covers reading, inline editing, search, change history, similar-pages discovery, attachment management, comments/mentions, dark mode, the admin panel (users, content, security, knowledge, API keys, KG policy, retrieval quality, page ownership, audit), and the graph viewers.
| Concern | Choice |
|---|---|
| Framework | React 19 (react, react-dom) |
| Routing | React Router v7 (react-router-dom ^7.18) |
| Build | Vite 8 (Rolldown) (@vitejs/plugin-react ^6) |
| Editor | CodeMirror 6 (@uiw/react-codemirror + @codemirror/lang-markdown) |
| Article rendering | Server-rendered HTML (contentHtml) decorated client-side; react-markdown + remark-gfm, remark-math, rehype-katex for the editor preview (see Rendering Pipeline) |
| Math and code highlighting | katex and lowlight (highlight.js "common" grammars), each code-split and loaded only for pages that need it |
| Graph visualisation | cytoscape + cytoscape-cose-bilkent via react-cytoscapejs |
| Testing | Vitest + Testing Library + happy-dom |
| Styling | Hand-written CSS with a token-based design system (no framework) |
| State | React state + hooks; no external store library |
The build runs automatically during mvn package: wikantik-war/pom.xml
uses exec-maven-plugin to run npm ci --no-audit --no-fund --ignore-scripts
and then npm run build in wikantik-frontend (Node and npm must already be
installed), and the resulting dist/ is copied into the WAR.
The SPA uses client-side routing for all interactive views. Deep links are
handled by SpaRoutingFilter (com.wikantik.rest.SpaRoutingFilter in wikantik-rest), which forwards recognised
SPA paths to /index.html so React Router can take over.
SpaRoutingFilter matches two categories of path:
/wiki/, /edit/, /diff/, /admin//admin, /admin/insights (redundant with the /admin/
prefix, listed explicitly in SPA_EXACT anyway), /search, /page-graph,
/knowledge-graph, /preferences, /reset-password, /login,
/me/mentions, /change-passwordThe full route table from main.jsx:
| Route | Component | Notes |
|---|---|---|
/ | <Navigate to="/wiki/Main" replace /> | No home page — always redirects |
/wiki/:name | PageView | Article reader (eager-loaded) |
/wiki | <Navigate to="/wiki/Main" replace /> | Bare /wiki redirect |
/edit/:name | PageEditor | Wiki page editor |
/diff/:name | DiffViewer | Side-by-side version diff |
/search | SearchResultsPage | Full-text + faceted search |
/page-graph | PageGraphView | Page Graph (wikilink edges) viewer |
/knowledge-graph | KnowledgeGraphView | KG entity/edge viewer |
/preferences | UserPreferencesPage | Personal settings |
/me/mentions | MentionsPage | @-mention inbox |
/reset-password | ResetPasswordPage | Password reset flow |
/login | LoginPage | Login (dual-registered in web.xml + SPA_EXACT) |
/change-password | ChangePasswordPage | Forced password change flow |
/admin (index) | OverviewDashboard | Admin overview |
/admin/users | AdminUsersPage | User management |
/admin/content | AdminContentPage | Content & index tools |
/admin/security | AdminSecurityPage | Groups + policy grants |
/admin/knowledge-graph | AdminKnowledgePage | KG curation |
/admin/apikeys | AdminApiKeysPage | API key management |
/admin/retrieval-quality | AdminRetrievalQualityPage | Retrieval experiment harness |
/admin/drift | AdminDriftPage | Vocabulary/SHACL drift burn-down dashboard |
/admin/insights | InsightsPanel | Search-visibility / content-intelligence panel |
/admin/kg-policy | AdminKgPolicyPage | KG inclusion policy rules |
/admin/kg-policy/explain | AdminKgPolicyExplain | Per-page policy explanation |
/admin/kg-policy/pending | AdminKgPolicyPending | Pages with pending review |
/admin/kg-policy/bootstrap | AdminKgPolicyBootstrap | Bulk bootstrap tooling |
/admin/page-ownership | AdminPageOwnershipPage | Page-owner assignment |
/admin/audit | AdminAuditPage | Tamper-evident audit log; rows are clickable and open a record-detail modal |
/admin/connectors | AdminConnectorsPage | Connector list + detail (settings/authorization/runs/pages tabs) |
/admin/connectors/new | AddConnectorWizard | Guided add-connector wizard |
/admin/connectors/:id | ConnectorDetailPage | Single connector detail |
All /admin/* routes are nested under AdminLayout (lazy-loaded), which sends anyone
without the Admin role back to the wiki. The data behind them is protected
server-side: AdminAuthFilter requires AllPermission for every /admin/* API
call (a browser navigation that asks for HTML gets the SPA shell, which carries no
admin data).
Graph viewer routes get a full-bleed app-content-full layout; editor and
admin routes use app-content-wide; wiki article routes use the narrow reading
column.
The REST API lives under /api/ and admin endpoints under /admin/. Two MCP
servers (/wikantik-admin-mcp and /knowledge-mcp) plus the OpenAPI tool
server at /tools/* are serviced by separate backend modules and are not part
of the SPA. Servlet counts are in Wikantik Architecture; MCP tool
counts are owned by the admin MCP documentation.
wikantik-frontend/
├── package.json
├── vite.config.js
├── index.html
├── public/
└── src/
├── main.jsx Entry point + route table
├── App.jsx Layout shell (Outlet + sidebar context-swap)
├── api/
│ └── client.js Fetch wrapper for /api/*; dispatches wikantik:version-mismatch
├── components/
│ ├── PageView.jsx Article rendering (server HTML + client DOM passes)
│ ├── PageEditor.jsx Inline editor (CodeMirror 6)
│ ├── Sidebar.jsx Cluster-grouped navigation (reader)
│ ├── QuickOverlay.jsx Quick switcher (Mod-K / Mod-O) and command palette (Mod-P)
│ ├── MetadataPanel.jsx Frontmatter chips
│ ├── ChangeNotesPanel.jsx History + diffs
│ ├── SimilarPagesPanel.jsx
│ ├── AttachmentPanel.jsx
│ ├── BacklinksPanel.jsx "Referenced by" backlinks (GET /api/backlinks/{name})
│ ├── CommentsDrawer.jsx Threaded comments + @-mention composer
│ ├── DiffViewer.jsx
│ ├── MentionsPage.jsx @-mention inbox (/me/mentions)
│ ├── PersonalZone.jsx Personal zone widget
│ ├── LoginPage.jsx / LoginForm.jsx / SsoLoginButton.jsx
│ ├── UserPreferencesPage.jsx / ResetPasswordPage.jsx
│ ├── TableOfContents.jsx / Breadcrumbs.jsx
│ ├── admin/ Admin panel pages + forms (see below)
│ ├── pagegraph/ Page Graph viewer (lazy-loaded)
│ ├── kgraph/ Knowledge Graph viewer (lazy-loaded)
│ └── ui/ Shared primitive components (see below)
├── commands/ Command registry shared by the overlay, slash menu, toolbar and key bindings
├── navigation/ NavigationGuardProvider: confirm-before-leaving dialog while a guard is registered
├── newpage/ NewPageProvider: the New page dialog and its command
├── hooks/ custom hooks (see below)
├── utils/ utility modules (see below)
└── styles/
├── globals.css Reset, CSS variables, theme tokens
├── article.css
└── admin.css
There are two rendering paths, and the reader does not use react-markdown.
Reader (PageView). The server renders the page (Flexmark MarkdownRenderer)
and GET /api/pages/{name} returns the result as contentHtml; a /wiki/{name}
navigation also embeds the same data in the served index.html
(window.__WIKANTIK_PAGE__), so the client can seed its state without a render-critical
fetch. PageView injects that HTML and then runs idempotent client-side DOM passes over it:
renderMath — typesets the server-emitted .math-inline / .math-display
elements with KaTeX. KaTeX (CSS and fonts bundled from the npm package, not a CDN)
is fetched only for a page that contains math.highlightCodeBlocks — syntax-highlights language-* code blocks with lowlight,
loaded lazily and only when the HTML contains such a block.addCopyButtons and addHeadingAnchors — copy buttons on code blocks and
hover-reveal anchor links on h2/h3 headings.commentHighlight) — marks the text ranges that
comment threads are attached to.Editor preview, search snippets and KG chunk views. These render markdown in the
browser with react-markdown. The editor preview (EditorPreviewMarkdown, fed by
PageEditor) uses this plugin chain:
remark-gfm — tables, strikethrough, task listsremark-math plus the custom remarkInlineMath — $…$ and $$…$$, with
Pandoc's inline-math ruleremarkCallouts — Obsidian-style callouts, mirroring the server's CalloutExtensionremarkWikiMarkup — [{…}]() plugin and directive markup that remark would
otherwise leave as literal textremarkWikiLinks and remarkMissingLinks — native [[wikilinks]] and ![[embeds]]
(mirroring the server's native-links extension), and links to pages that do not existremarkAttachments — rewrites attachment links so they resolve relative to the
current pagerehype-katex — typesets mathrehypeHighlightCode (lowlight) — code highlightingrehypeSourceLine — stamps each element with data-line so a click in the preview
jumps the editor caret to that source lineSee docs/user/MathematicalNotation.md for the full math
syntax reference.
Wikantik ships two distinct, lazy-loaded graph viewers in the SPA. Don't conflate them — they reflect two separate subsystems (see Page Graph vs Knowledge Graph for the long form):
/page-graph — Page Graph viewerReal wikilink edges between pages. Nodes are pages; edges are wikilinks
parsed from page bodies, with co-resident structural information
(canonical_id, cluster) layered in. Hubs (nodes above a configurable
degree threshold) are highlighted; anomalies (restricted pages, orphans)
can be toggled into view. Components live under
wikantik-frontend/src/components/pagegraph/:
| Component | Role |
|---|---|
PageGraphView.jsx | Top-level container — fetches snapshot, owns state |
GraphCanvas.jsx | Cytoscape canvas with CoSE-Bilkent layout |
GraphToolbar.jsx | Edge-type filters, anomaly toggle, refresh |
GraphZoomSlider.jsx | Manual zoom control (semantic zoom) |
GraphLegend.jsx | Hub threshold and edge-type legend |
GraphDetailsDrawer.jsx | Selected-node details + incident edges |
FilterPanel.jsx | Per-cluster / tag filtering |
GraphErrorBoundary.jsx / GraphErrorState.jsx | Failure UI |
graph-data.js | Converts the REST snapshot into Cytoscape elements |
graph-style.js | Cytoscape stylesheet |
Snapshot comes from PageGraphSnapshotResource in wikantik-rest,
backed by the Page Graph subsystem (com.wikantik.pagegraph.*). A
?focus=PageName query parameter auto-centres and selects a node on
load — useful for linking to the graph from an article.
/knowledge-graph — Knowledge Graph viewerReader-facing visualisation of the LLM-extracted entity graph. Nodes are
typed entities; edges are co-mention or typed relations between them.
Includes tier filter, node-type colours, provenance/status badges, and a
large-graph warning gate that prompts before rendering big subgraphs.
Components under wikantik-frontend/src/components/kgraph/:
| Component | Role |
|---|---|
KnowledgeGraphView.jsx | Top-level container |
KgGraphToolbar.jsx | Tier filter, node-type filter, refresh |
KgGraphLegend.jsx | Node-type colour key |
KgGraphDetailsDrawer.jsx | Selected-node provenance + incident edges |
KgErrorState.jsx | Failure UI |
kg-graph-data.js | Converts the REST snapshot into Cytoscape elements |
kg-graph-style.js | Cytoscape stylesheet |
Snapshot comes from KnowledgeGraphResource in wikantik-rest, backed
by DefaultKnowledgeGraphService in wikantik-knowledge.
The CSS design system is intentionally minimal and framework-free. Tokens live
in src/styles/globals.css and drive light and dark themes. The key tokens
from the :root block (light theme; spacing, transition, and radius tokens
omitted for brevity):
:root {
/* Typography */
--font-display: 'Playfair Display', Georgia, 'Times New Roman', serif;
--font-body: 'Source Serif 4', Georgia, serif;
--font-ui: 'DM Sans', -apple-system, BlinkMacSystemFont, sans-serif;
--font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
/* Light theme */
--bg: #FEFCF8;
--bg-elevated: #FFFFFF;
--bg-sidebar: #F7F4EE;
--text: #2D2D2D;
--text-secondary: #6B6560;
--text-muted: #9B9590;
--accent: #C45D3E; /* terracotta */
--accent-hover: #A8442A;
--sage: #7A8B6F;
--sage-light: #E8EDE4;
--border: #E8E4DC;
--border-strong: #D0C8BC;
--code-bg: #F3F0EA;
/* Semantic state */
--success: #2a8d2a;
--danger: #b13a3a;
--warning: #b8860b;
/* Layout */
--sidebar-width: 280px;
--content-max-width: 1470px;
--content-max-width-wide: 1600px;
--toc-width: 200px;
}
[data-theme="dark"] {
--bg: #1A1A1E;
--bg-elevated: #222226;
--bg-sidebar: #18181C;
--text: #E8E4DC;
--text-secondary: #A8A09A;
--accent: #E07050; /* lighter terracotta for dark bg */
--accent-hover: #F08060;
/* … */
}
body uses var(--font-ui) (DM Sans). Long-form article text uses
var(--font-body) (Source Serif 4) applied in article.css. The
--content-max-width of 1470px is for the main reading/article layout; admin
and wide contexts use --content-max-width-wide: 1600px.
CommentsDrawer renders threaded comment threads anchored to page text
selections. It mounts inside PageView alongside BacklinksPanel. The
@-mention composer is powered by useMentionPicker + MentionPicker.
Unread mention counts are tracked by useUnreadMentions. The personal
mention inbox lives at /me/mentions (MentionsPage).
For the full feature spec see docs/user/CommentsAndMentions.md.
PersonalZone (/components/PersonalZone.jsx) surfaces the current user's
recently-edited pages, drafts, and other personal signals. Related hooks:
useMyPages, useRecentlyViewed, useDraft, useDrafts.
For the full feature spec see docs/user/PersonalZone.md.
App.jsx listens for the custom DOM event wikantik:version-mismatch,
dispatched by api/client.js when the server's X-Build-Version response
header (set by BuildVersionFilter in wikantik-rest) differs from the
embedded __BUILD_VERSION__ constant (stamped at Vite build time via
vite.config.js's buildVersionPlugin). When a mismatch is detected the app
renders a non-blocking "A new version is available." toast with a Reload button.
Dismissing the toast is remembered for the browser session, per server version
(sessionStorage), so a later deploy shows it again. Separately, the stale-asset
recovery script in index.html sets a __wikantik_reload retry flag, which
main.jsx clears once the app has rendered.
Accessed at /admin; its /admin/* API calls are protected server-side by
AdminAuthFilter (requires AllPermission). AdminSidebar replaces the reader Sidebar via a context
swap in App.jsx whenever location.pathname.startsWith('/admin').
Links are grouped into four sections:
People & Access
/admin/users)/admin/security)/admin/apikeys) — see API KeysContent
/admin/content)/admin/page-ownership) — see Page Ownership/admin/connectors)Knowledge & Search
/admin/knowledge-graph) — shown only when the knowledgeGraph capability is on/admin/kg-policy) — see KG Inclusion Policy/admin/retrieval-quality) — see Running the Retrieval Quality Harness/admin/drift) — vocabulary/SHACL drift burn-down dashboard/admin/insights)Observability
/admin/audit) — see Audit LogPlus an Overview index link at /admin (exact match).
AdminContentPage uses a tab bar defined as:
Dashboard | Orphaned Pages | Broken Links | Versions | Chunk Inspector | Index Status
ChunkInspectorTab) inspect embedding chunks per page.IndexStatusTab) search index health and rebuild trigger.AdminKnowledgePage uses a tab bar defined as:
Proposals | Extraction | Node Explorer | Edge Explorer |
Content Embeddings | Hub Proposals | Hub Discovery | LLM Activity
ProposalReviewQueue) — review pending KG node/edge proposals; approve or reject individually.ExtractionTab) — run or cancel the LLM entity extractor; watch live progress.GraphExplorer) — browse/search all KG nodes by type or status; inspect properties; project frontmatter.EdgeExplorer) — browse/search all KG edges by relationship type; inspect provenance.ContentEmbeddingsTab) — inspect the shared Ollama mention-centroid index; bulk-backfill missing frontmatter.HubProposalsTab) — generate/manage hub membership proposals; approve/reject individually, in bulk, or by percentile threshold.HubDiscoveryTab) — cluster-based hub discovery; accept/dismiss proposal cards; manage existing hub memberships.LlmActivityTab) — live in-memory view of recent/in-flight LLM calls (extraction, proposal judging, embeddings); last ~1 hour only, cleared on restart.components/ui/)A set of reusable primitives used across both the reader and admin panel:
| Component | Purpose |
|---|---|
Badge.jsx | Inline status/label badge |
Card.jsx | Content card container |
Chip.jsx | Removable filter chip |
Combobox.jsx | Searchable select with typeahead |
ConfirmDialog.jsx | Confirmation modal |
EmptyState.jsx | Zero-results placeholder with icon |
Icon.jsx | Icon wrapper |
Modal.jsx | Accessible modal dialog |
Select.jsx | Styled native-select wrapper |
Skeleton.jsx | Loading skeleton placeholder |
Spinner.jsx | Loading spinner |
Tabs.jsx | Tab bar |
TagInput.jsx | Freeform multi-tag input |
ToastProvider.jsx | Toast context + renderer (used by useToast) |
src/hooks/)35 hook modules (counted 2026-10-05; most have a .test file). The main groups:
useAuth.jsx (login state, current user via AuthProvider context), useCapabilities.jsx (server-advertised feature flags, e.g. knowledgeGraph)useDarkMode.js (toggle + localStorage persistence)useApi.js (fetch + auth header plumbing), useAttachments.js, useMyPages.jsuseEditorDrop.js (drag-and-drop into editor), useDraft.js / useDrafts.js (auto-save draft management), useFrontmatterValidation.js (debounced live frontmatter validation)useMentionPicker.js (debounced @-mention autocomplete with caret tracking), useUnreadMentions.jsuseGlobalHotkeys.js (Mod-K / Mod-O open the quick overlay on pages, Mod-P on commands, Mod-Alt-N today's daily note), useScrollSpy.js (ToC active heading), useScrollLock.js (modal body lock), useFocusTrap.js (modal focus management), useDocumentTitle.js, usePageTrail.js (per-tab reader breadcrumb trail)useRecentSearches.js, useRecentlyViewed.jsuseToast.js (consumes ToastProvider context)src/utils/)57 utility modules, five of them under utils/livePreview/ (counted 2026-10-13; most have a .test file). The main groups:
remarkAttachments.js (custom remark plugin for attachment links), math.js (lazy KaTeX renderer for the server-emitted math elements), rehypeSourceLine.js, headingAnchors.js, headings.js, codeCopy.js, highlight.js, markdownFormat.jspageUrl.js, slugUtils.js, frontmatterUtils.js, readingTime.jscommentAnchor.js, commentHighlight.js, caretCoords.jssearchFacets.jswikiLinkComplete.js, scrollSync.js (editor/preview scroll sync), draftKeys.jsattachmentNameValidator.jsdatetime.jsThe SPA uses session cookies set by the standard Wikantik login flow. All
/api/* calls include the session cookie; ACLs and policy grants are
enforced server-side by RestServletBase.checkPagePermission(). There is no
token or JWT layer — the SPA is a thin client of the same session the wiki
uses server-side.
Google SSO is available at the login page via SsoLoginButton when the
server is configured with wikantik.sso.* properties.
For fast frontend iteration without a full Maven rebuild:
cd wikantik-frontend
npm install
npm run dev # Vite dev server at http://localhost:5173/
The Vite dev server proxies three path prefixes to http://localhost:8080
(hard-coded in vite.config.js — there is no VITE_API_BASE environment
variable):
| Proxy path | Target |
|---|---|
/api | http://localhost:8080 |
/attach | http://localhost:8080 |
/admin | http://localhost:8080 |
A running backend instance on port 8080 is required for most functionality.
Unit tests:
npm test # vitest run (single pass)
npm run test:watch # vitest watch mode
Note: Vitest concurrency can cause false failures in some hook tests — if a test file fails in a full run, re-run it in isolation before chasing the failure.
The production build runs as part of mvn package. To build the frontend
directly:
cd wikantik-frontend
npm run build # outputs to dist/
The vite.config.js buildVersionPlugin stamps a build-version.txt asset
and a <meta name="build-version"> tag into index.html at build time;
BuildVersionFilter (in wikantik-rest) reads this file and echoes the
version in X-Build-Version response headers so the update toast can detect
stale bundles.
The wikantik-war module's pom.xml copies dist/ into the WAR during the
packaging phase — no manual copy step is required.
Cluster Hub: Wikantik Platform Development Hub: Architecture, MCP Protocols, and Agent Briefings (wikantik-development)