Graph: the Edges around a Block
The Edges of the Block index say which Block points at which. The graph is their neighbourhood in one answer: the Blocks within a few hops of a start, and the Edges between them. The library answers it (GET /api/editor/graph); the add-on @skaile/forge-editor-graph draws it.
The route
| Route | GET /api/editor/graph?documentId=&blockId=&depth= (API.graph, port index) |
| Start | documentId alone: every Block of the Document that has an Edge. With blockId: that one Block (kept even without an Edge). |
depth | Hops from the start Blocks, 1 to 3, default 2 |
| Answer | GraphData: { nodes: { address, title, type }[], edges: BlockEdge[], truncated: boolean } |
| Client | EditorBackend.index.graph({ documentId, blockId?, depth?, signal? }), optional: a backend without it leaves it out |
The walk follows Edges in both directions (what a Block points at and what points at it). It never follows tag Edges (a tag is no Block) or Edges to a Block that does not exist, and it never leaves the Documents the user may read: a Block in another Document is a node only when the user may read that Document. Every Edge in edges has both its Blocks in nodes.
The answer holds at most 500 Blocks. When the walk meets more, it keeps the first ones in the order it found them (start Blocks first) and sets truncated. Edges are answered once each, in the order they were met.
| Status | When |
|---|---|
400 | no documentId, or a depth outside 1 to 3 |
401 | no user |
403 | the user may not read the Document |
404 | blockId names no Block of the Document |
501 | the host has no index port |
The walk itself is walkGraph(seeds, source, { depth, maxNodes?, keepIsolated? }) in @skaile/forge-editor/core, over a GraphSource (edges(address, direction) and node(address)). The route reads a GraphSource from the index port; the memory backend (createMemoryBackend) reads one from its projected rows (graphSourceOfRows), so a test or a demo draws the same graph.
The panel
import { GraphPanel } from '@skaile/forge-editor-graph/vue' // or '@skaile/forge-editor-graph/react'
<GraphPanel :document-id="documentId" :block-id="blockId" :open="openBlock" />
| Prop | |
|---|---|
documentId | the Document the graph is about |
blockId | optional: the graph around this Block; it is drawn larger and in the middle |
open(address) | what a click on a node does; resolve false when it did not happen |
depth | hops to start with (1 to 3, default 2); the panel has a select for it |
Opening a Block is the host's job, as for a search hit: in the open Document editor.commands.revealBlock(blockId), else navigate to the Document with ?block=<id>. The workspace host of this repository does both in openHit; the same function serves the graph ({ documentId, blockId } is what a BlockSearchHit also carries).
The panel draws:
| Node | one per Block, labelled with its title (else its type); colour by Block type, the focus Block in rose |
| Edge colour | the kind: reference, derivation, embed, and one colour per authored kind (supports, contradicts, ...), the same one every time |
| Edge dashed | the Edge was written by hand (origin: 'meta', in the inspector's „Kanten“ section); content Edges are solid |
| Arrow | at the Block the Edge points to |
| Legend | the kinds present, as they are drawn |
| List | every node again as a button, for keyboards and screen readers (the canvas is WebGL and has only a label) |
| Notice | truncated: more than 500 Blocks, the rest left out |
The panel renders nothing when the backend has no index.graph.
Framework-free parts
@skaile/forge-editor-graph (no sigma, no DOM):
buildGraph(data, { focus?, iterations? }) | the graphology MultiDirectedGraph: node attributes label x y size color documentId blockId blockType focus, Edge attributes color size dash gap kind origin; positions are deterministic (circle, then ForceAtlas2) |
edgeStyleOf({ kind, origin }) | { color, dashed }: the style table of the panel |
legendOf(data) | the legend entries |
createGraphPanelModel({ load, open, depth? }) | the model both components render: state (target, depth, status, last answer), setTarget, setDepth, reload, open, dispose |
graphLoaderOf(backend) | load for createGraphPanelModel, over backend.index.graph |
createGraphTranslator(locale, overrides?) | the add-on's own German and English strings |
sigma (WebGL) loads when a panel mounts, so importing the package costs nothing and server-side rendering never meets it. The dashed Edges use sigma's layerDashed with the Edge attributes dash and gap: an authored Edge has 8 px dashes and 5 px gaps, every other Edge one dash longer than the canvas and no gap.
Wiring a host
The add-on needs no feature and no port of its own. The host only mounts the panel where it wants it (a sidebar tab, the Block's details) and passes open:
<script setup lang="ts">
import { GraphPanel } from '@skaile/forge-editor-graph/vue'
</script>
<template>
<GraphPanel :document-id="documentId" :block-id="blockId" :open="openHit" />
</template>
The repository's document pages (the file-drawer layout, app/layouts/default.vue) show it behind NUXT_PUBLIC_ADDONS_GRAPH=1; the kitchen sink's standalone pages have no workspace index and show no graph.
Speech: text to audio, and subtitle files
The SpeechProvider port with its engines and voices, the SSE route that streams progress and stores the audio as an asset, the daily character budget, the client call, and the block action that exports a transcript as WebVTT or SubRip.
PDF viewer: passages, anchors and quotes
The add-on @skaile/forge-editor-pdf-viewer: the source-anchor property, a pdfjs viewer with a text layer, scrolling to a passage on reveal, and „Zitat anlegen“, which creates a quote block derived from the PDF.