Team Ai
Apppublic

HuggingFaceBio/carbon-tokenization

sourceHugging Faceupdated 4mo agoView on Hugging Face
11likes
SPECIFICATION.md426 linesDownload Raw Back to docs
1# Collab Editor - Project Specification2 3## 1. Overview4 5A collaborative, real-time scientific article editor deployed as a Hugging Face Space. Users write rich content (math, citations, custom components) in a TipTap-based editor synced via Yjs/Hocuspocus, then publish a self-contained static HTML article. An AI assistant helps with writing and editing.6 7**Stack**: React 18 + TipTap 3 + Yjs (frontend) / Express + Hocuspocus + Node 20 (backend) / Docker on HF Spaces. No CSS-in-JS - all styling via vanilla CSS custom properties.8 9**Relationship to `research-article-template`**: the CSS foundation, design tokens, and visual language come from the [research-article-template](https://huggingface.co/spaces/tfrere/research-article-template) project. The editor imports the template's CSS files (`_variables.css`, `_reset.css`, `_base.css`, `_layout.css`, component partials) and the publisher injects them inline into published HTML. The published output is designed to look identical to articles built with the Astro-based template.10 11---12 13## 2. Architecture14 15```mermaid16graph TB17  subgraph browser [Browser]18    SPA["React SPA<br/>TipTap + Yjs"]19  end20 21  subgraph server [Node Backend - port 8080]22    Express["Express HTTP"]23    Hocuspocus["Hocuspocus<br/>WebSocket"]24    Publisher["Publisher Pipeline<br/>HTML + PDF"]25    Agent["AI Agent<br/>HF Inference"]26    Auth["HF OAuth"]27  end28 29  subgraph storage [Persistence]30    LocalFS["Local FS<br/>data/*.yjs"]31    HFDataset["HF Dataset<br/>articles/ published/"]32  end33 34  SPA -->|"WebSocket /collab"| Hocuspocus35  SPA -->|"REST /api/*"| Express36  Hocuspocus -->|"Database ext"| LocalFS37  LocalFS -->|"schedulePush"| HFDataset38  HFDataset -->|"pullDocument"| LocalFS39  Express --> Publisher40  Express --> Agent41  Express --> Auth42  Publisher --> LocalFS43  Publisher -->|"uploadPublishedAssets"| HFDataset44```45 46**Single process in production**: the backend serves the Vite-built frontend, all REST APIs, the WebSocket collab channel, and static published articles. No reverse proxy needed.47 48---49 50## 3. Data Model (Yjs Shared Types)51 52The entire collaborative state lives in a single `Y.Doc`:53 54- **`Y.XmlFragment("default")`** - TipTap document content (ProseMirror nodes synced via Collaboration extension)55- **`Y.Map("frontmatter")`** - scalar metadata: `title`, `subtitle`, `description`, `published`, `doi`, `template`, `licence`56- **`Y.Array("frontmatter.authors")`** - `{ name, url?, affiliations: number[] }[]`57- **`Y.Array("frontmatter.affiliations")`** - `{ name, url? }[]`58- **`Y.Map("citations")`** - CSL-JSON entries keyed by citation ID59- **`Y.Map("settings")`** - `citationStyle`, `primaryHue`, and future editor preferences60- **`Y.Map("comments")`** - comment threads keyed by `commentId`, each with author/text/resolved61 62All types are concurrently editable by multiple users and persist to `data/default.yjs`.63 64---65 66## 4. Backend Components67 68### 4.1 HTTP Routes69 70| Method | Path | Auth | Purpose |71|--------|------|------|---------|72| `GET` | `/oauth/authorize` | Public | Redirect to HF OAuth |73| `GET` | `/auth/callback` | Public (CSRF state) | Exchange code, set cookie, redirect to `/editor` |74| `GET` | `/api/auth/status` | Cookie | Return `{ authenticated, canEdit, user }` |75| `POST` | `/api/chat` | OAuth (optional) | Stream AI agent responses (HF Inference Providers) |76| `POST` | `/api/publish` | OAuth (canEdit) | Run publish pipeline, generate HTML/PDF |77| `POST` | `/api/admin/reset-document` | OAuth (canEdit) | Delete local `.yjs`, close connections |78| `POST` | `/api/upload` | None (uses cookie for HF) | Upload image (multipart, max 10MB) |79| `POST` | `/api/citations/resolve` | None | Resolve DOI/URL to CSL-JSON |80| `POST` | `/api/citations/format` | None | Format entries to HTML bibliography |81| `POST` | `/api/citations/import-bib` | None | Parse BibTeX to CSL-JSON |82| `GET` | `/editor` | OAuth (canEdit) | Serve SPA (or login page) |83| `GET` | `*` | Public | Serve published article (or login page) |84 85### 4.2 WebSocket Collaboration86 87- Upgrade on `/collab` only; all other paths rejected88- Single document: `DEFAULT_DOC_NAME = "default"`89- Hocuspocus `onAuthenticate`: validates OAuth token if enabled, checks `canEdit`90- `Database` extension: `fetch` reads local `.yjs` or pulls from HF; `store` writes local + schedules HF push (10s debounce)91 92### 4.3 HF Storage93 94- Dataset ID: `HF_DATASET_ID` or `{SPACE_ID}-data`95- Dataset is created **private by default** (`createRepo({ private: true })`). The OAuth grant needs `manage-repos` on the user's first write; subsequent containers reuse the cached token.96- Token: `HF_TOKEN` (env) or cached OAuth token from last authenticated user97- **Documents**: `articles/<name>.yjs` - debounced push on every Hocuspocus store98- **Published assets**: `published/<name>/{index.html, article.pdf, thumb.jpg, meta.json, llms.txt}`99- **Images**: `images/<uuid-filename>` referenced from articles via `/d/images/...` proxy URLs100- `flushAll()` on `SIGTERM`/`SIGINT` to push pending changes101 102### 4.3.1 Storage Status & Recovery103 104The persistence pipeline used to fail silently in multiple places (`createRepo` 403 on a missing scope, `uploadFile` 5xx mid-debounce, `writeFileSync` on a readonly FS, ...) and the editor would happily keep showing "Saved". To make data first-class:105 106- **In-memory tracker** in `hf-storage.ts` records `datasetReady`, `lastLocalSaveAt`, `lastCloudPushAt`, `pendingPush`, `lastError {stage, message, statusCode, at, docName}`. Every write path updates it; every error path records the failure.107- **`GET /api/storage/status`** exposes the tracker (canEdit-gated). The frontend `SyncIndicator` polls it every 5s and displays a three-state badge: green "Saved" / amber "Saving..." / **red "Storage error"** (pulsing, with the exact reason in the tooltip + actionable hint for the 403 / missing-scope case).108- **Eager `ensureDatasetExists`** on first `/api/auth/status` for a canEdit user. A misconfigured fork now surfaces its error within ~10s of login instead of waiting for an edit + 12s debounce cycle.109- **`beforeunload` guard** on the editor: if a local edit is in flight, a push is armed, the WS is offline, or the tracker reports an error, the browser pops the standard "Leave site?" confirm.110- **`GET /api/admin/export-doc`** (canEdit-gated) streams the on-disk `.yjs` snapshot as a download. The escape hatch for disaster recovery: when the cloud push has been failing and the container is about to rebuild, an admin can grab the doc bytes manually.111 112### 4.3.2 Dataset Reverse Proxy (`/d/*`)113 114Since the dataset is private, anonymous viewers of a published article can't fetch its images / PDF / og:image directly from `huggingface.co/datasets/...`. The editor server exposes `GET /d/:path*` as an authenticated forward-proxy:115 116- **Whitelist**: only `images/` and `published/` are reachable; `articles/` (raw `.yjs` drafts) is **always 404** regardless of caller.117- **Token cascade**: request cookie โ†’ cached user token โ†’ `HF_TOKEN` env โ†’ anonymous fetch. The cookie token is also promoted into the cache opportunistically, so the first signed-in viewer warms the proxy for subsequent anonymous viewers within the same container lifetime.118- **Streaming**: WHATWG body piped straight to the Express response - no buffering of full PDFs in Node memory.119- **Caching**: `images/*` is served as `immutable, max-age=1y` (UUID names, never overwritten); `published/*` as `max-age=300, stale-while-revalidate=60` (re-published in place).120- **Error mapping**: upstream 401/403 collapse to 502 so the browser never gets prompted for credentials it can't supply; upstream 404 passes through.121 122### 4.4 Publisher Pipeline123 124```mermaid125flowchart LR126  YDoc["Y.Doc (.yjs)"] --> Extract["extractFromYDoc<br/>frontmatter + JSON"]127  Extract --> GenHTML["generateHTML<br/>@tiptap/html"]128  GenHTML --> PostProc["postProcess<br/>accordion, biblio,<br/>mermaid, htmlEmbed"]129  PostProc --> Render["renderArticleHTML<br/>full HTML page"]130  CSS["loadCSS<br/>template styles"] --> Render131  Render --> LocalWrite["Write local<br/>index.html"]132  Render --> PDF["Playwright<br/>PDF + thumbnail"]133  LocalWrite --> HFUpload["uploadPublishedAssets<br/>HF dataset"]134```135 136- **CSS loading**: reads template CSS files, resolves `@custom-media` queries via `resolveCustomMedia()`, splits into variables/reset/base/layout/components/article/print137- **Post-processing**: accordion divs to `<details>`, bibliography injection, mermaid to `<pre>`, htmlEmbed to `<iframe>`138- **HTML output**: self-contained page with inline CSS, CDN assets (KaTeX, highlight.js, Mermaid), theme toggle (SVG sun/moon), TOC generation (scroll-based, collapsible), lightbox, footer with citation/BibTeX/DOI139- **PDF**: optional Playwright Chromium headless (1200x630 thumbnail + full PDF)140- **Server extensions**: mirror of frontend TipTap extensions for server-side HTML generation141 142### 4.5 Auth143 144- Enabled when `SPACE_ID` + `OAUTH_CLIENT_ID` are set145- OAuth 2.0 flow with HF as provider; cookie `hf_access_token` (httpOnly, secure, sameSite: none)146- `resolveUser`: `whoAmI` via `@huggingface/hub`, then `checkWriteAccess` (Space owner or org member with write/admin role)147- In-memory state map with 10-min TTL for CSRF protection148 149### 4.6 AI Agent150 151- Provider: Hugging Face Inference Providers (`https://router.huggingface.co/v1`), default model `openai/gpt-oss-120b`. Model ids may be suffixed with `:<provider>` (e.g. `meta-llama/Llama-3.3-70B-Instruct:together`) to bypass providers that enforce overly strict tool-call validation (notably Groq) or that don't support the `tools` parameter (Nscale, etc.).152- Auth: per-request bearer token resolved from the editor's OAuth cookie when available, falling back to the server-side `HF_TOKEN`. On a HF Space with `inference-api` scope, no extra secret is needed - the logged-in user pays for their own inference under their HF quota.153- Streaming via Vercel AI SDK `streamText` over `@ai-sdk/openai-compatible`154- Reasoning parts from prior assistant turns are stripped before re-sending the history: providers like Cerebras reject `reasoning_content` on round-trip, and the model doesn't need to see its own past reasoning to continue the conversation.155- **Context**: document text, current selection, frontmatter (sent by frontend with each message)156- **Tools** (declarative, executed client-side by the frontend):157  - `replaceSelection` - replace selected text158  - `insertAtCursor` - insert at cursor position159  - `applyDiff` - search/replace in document160  - `updateFrontmatter` - modify metadata fields161  - `addAuthor` / `removeAuthor` - manage author list162- Agent edits are grouped in a single Yjs `UndoManager` batch for Cmd+Z163 164### 4.7 Citations165 166- Uses `@citation-js/core` with bibtex, doi, csl plugins167- Resolve: DOI URL or identifier to CSL-JSON entries168- Format: entries + style + locale to HTML bibliography169- Import: BibTeX string to CSL-JSON170 171---172 173## 5. Frontend Components174 175### 5.1 App Shell176 177- **No router** - single view with conditional rendering178- **Theme**: CSS custom properties with dynamic primary color from `settings.primaryHue` (OKLCH color model, synced via Yjs settings)179- **Layout**: top bar (undo/redo, settings, publish, user chip) + 3-column CSS grid (TOC / editor / comments)180- **Chat**: floating button bottom-left, `ChatPanel` overlay181- **Modals**: comment dialog, settings drawer, publish confirmation182 183### 5.2 Editor184 185- Creates `Y.Doc` + `HocuspocusProvider` (WebSocket to `/collab`)186- **Seeding**: after provider `synced` event only, if `Y.XmlFragment("default")` is empty, inserts `DEFAULT_CONTENT` + `seedFrontmatter` + `SEED_CITATIONS`187- **Yjs Maps**: `citations`, `settings`, `comments`, `frontmatter` (via dedicated stores)188- **Image handling**: paste/drop with upload to `/api/upload`189 190### 5.3 TipTap Extensions191 192**Built-in (configured)**:193- StarterKit (no codeBlock, no undo), CodeBlockLowlight (all languages), Placeholder, Collaboration, CollaborationCursorV3, Mathematics (KaTeX), Image, Table/Row/Cell/Header194 195**Custom**:196- `CollaborationUndo` - bridges Yjs UndoManager for agent batch edits197- `Comment` - inline mark with `commentId` + `resolved`198- `SlashCommands` - `/` trigger with suggestion popup199- `ImageUpload` - drag-drop upload node with progress200- `Citation` - inline atomic node (key + label), links to `citationsMap`201- `Bibliography` - block node with rendered HTML from citations202- `Glossary` - inline atomic (term + definition tooltip)203- `Footnote` - inline atomic (content shown in footer)204- `Stack` + `StackColumn` - multi-column layout (2/3/4 cols)205 206### 5.4 Component System207 208Registry-based system for MDX-like custom components:209 210| Component | Kind | Purpose |211|-----------|------|---------|212| `accordion` | wrapper | Collapsible section (details/summary) |213| `note` | wrapper | Info/warning/danger/success callout |214| `quoteBlock` | wrapper | Styled blockquote |215| `wide` | wrapper | Content wider than column |216| `fullWidth` | wrapper | Full viewport width |217| `sidenote` | wrapper | Marginal note |218| `reference` | wrapper | Reference container |219| `htmlEmbed` | atomic | External HTML embed (iframe) |220| `hfUser` | atomic | HF user card |221| `rawHtml` | atomic | Raw HTML injection |222| `mermaid` | atomic | Mermaid diagram (live preview) |223 224- **Factory**: `createComponentExtension(def)` generates TipTap nodes from registry definitions (handles both wrapper and atomic kinds)225- **NodeViews**: `WrapperView` (editable content area + chrome), `AtomicView` (placeholder + field editor), `MermaidView` (textarea + SVG preview)226- **Slash menu integration**: each component generates a slash menu item via `getComponentSlashItems()`227 228### 5.5 Frontmatter System229 230- `FrontmatterStore`: wraps `Y.Map` + `Y.Array` for real-time collaborative metadata editing231- `useFrontmatter` hook: React state synced with Yjs observations232- `FrontmatterHero`: WYSIWYG editable hero section (title, subtitle, authors, affiliations, date, DOI)233- `SettingsDrawer`: template variant, SEO, banner, citation style, primary color hue slider, PDF/TOC/licence toggles234- `HueSlider`: OKLCH hue picker (0-360) with live preview, synced to `settingsMap.primaryHue`235 236### 5.6 Other UI237 238- **`TableOfContents`**: extracts headings from TipTap doc, scroll-based active state, collapsible sub-sections239- **`ChatPanel`**: message list + quick actions on selection + input with streaming240- **`CommentPopover`**: positioned comment popover anchored to the active thread (resolve/delete inline)241- **`BubbleToolbar`**: floating toolbar on text selection (bold, italic, link, comment, etc.)242- **`BlockHandle`**: drag handle for block-level nodes243 244### 5.7 CSS Architecture245 246```247styles/248  _variables.css       # Template tokens: --primary-color, breakpoints, @custom-media249  _reset.css           # Scoped reset for article content250  _base.css            # Typography, scoped to article content251  _layout.css          # 3-column grid, .wide/.full-width helpers252  _print.css           # Print styles253  _ui.css              # Editor chrome: buttons, dialogs, drawers, spinner254  tokens.css           # Design tokens (light/dark): text, bg, accent, code, danger, shadows255  article.css          # .tiptap content styles (shared editor/published)256  toc.css              # Editor TOC overrides257  editing.css          # Editor-only: layout, cursors, slash menu258  _publisher.css       # Published-only: theme toggle, wide/fullWidth, footer, lightbox259  components/260    _code.css          # Code blocks + syntax highlighting261    _table.css         # Tables262    _tag.css           # Tags263    _card.css          # Cards264    _mermaid.css       # Mermaid diagrams265    _embed.css         # Embed containers266    _embed-studio.css  # Embed studio overlay267    _hero.css          # Hero section (from template)268    _toc.css           # Base TOC styles (from template)269    _button.css        # Buttons (template)270    _form.css          # Form elements (template)271    _footer.css        # Footer (template)272```273 274The publisher reads these same CSS files server-side and injects them inline into published HTML, using `resolveCustomMedia()` to expand `@custom-media` queries into standard `@media` rules.275 276---277 278## 6. Deployment279 280### 6.1 Docker Build (3-stage)281 2821. **frontend-build**: `npm install` + `npm run build` (Vite)2832. **backend-build**: `npm install` + `npx tsc`2843. **runtime**: `node:20-slim` + Chromium system deps + `npm install --omit=dev` + Playwright Chromium + copy `frontend-dist/` + copy `frontend/src/styles/` to `frontend-styles/`285 286**CMD**: `node dist/server.js` on port 8080.287 288### 6.2 HF Space Configuration (README.md frontmatter)289 290- SDK: `docker`, port `8080`291- OAuth: `hf_oauth: true`, scopes: `manage-repos`, `inference-api`292- Two git remotes: `space` (tfrere/collab-editor, dev) and `prod` (tfrere/research-article-template-editor, production)293 294### 6.3 Environment Variables295 296| Variable | Required | Purpose |297|----------|----------|---------|298| `PORT` | No (default 8080) | HTTP listen port |299| `NODE_ENV` | No | `production` switches to `frontend-dist` path |300| `SPACE_ID` | For OAuth/HF | HF Space identifier, enables OAuth + dataset |301| `SPACE_HOST` | For OAuth | HTTPS callback URL host |302| `OAUTH_CLIENT_ID` | For OAuth | HF OAuth client |303| `OAUTH_CLIENT_SECRET` | For OAuth | HF OAuth secret |304| `OAUTH_SCOPES` | No (default `openid profile`) | OAuth scopes. Add `manage-repos` for dataset persistence and `inference-api` to power AI features with the user's token |305| `HF_DATASET_ID` | No | Override dataset name (default: `{SPACE_ID}-data`) |306| `HF_TOKEN` | For AI chat in local dev | Fallback Hub token for HF API + Inference Providers. Needs the "Make calls to Inference Providers" permission |307| `HF_INFERENCE_MODEL` | No (default `openai/gpt-oss-120b`) | Default chat-completion model id served by HF Inference Providers. May be suffixed with `:<provider>` to pin a specific routing |308| `ENABLE_PDF` | No (default true) | Toggle PDF/thumbnail generation |309 310### 6.4 Local Development311 312```bash313# Terminal 1 - Backend314cd backend && npm install && npm run dev315# Starts on http://localhost:8080316 317# Terminal 2 - Frontend318cd frontend && npm install && npm run dev319# Starts on http://localhost:5678 (proxies /api and /collab to :8080)320```321 322Create a `.env` file in `backend/` with at minimum `HF_TOKEN` for AI chat (must have the "Make calls to Inference Providers" permission). Without `SPACE_ID`, OAuth is disabled and all users can edit.323 324---325 326## 7. Key Data Flows327 328### 7.1 Collaborative Editing329 330```mermaid331sequenceDiagram332  participant ClientA as Client A333  participant Server as Hocuspocus334  participant ClientB as Client B335  participant Disk as Local FS336  participant HF as HF Dataset337 338  ClientA->>Server: WebSocket connect /collab339  Server->>Disk: Database.fetch (load .yjs)340  Server-->>ClientA: sync Y.Doc state341  ClientA->>Server: Y.Doc update (edit)342  Server->>ClientB: broadcast update343  Server->>Disk: Database.store (write .yjs)344  Disk-->>HF: schedulePush (10s debounce)345```346 347### 7.2 Publish Flow348 349```mermaid350sequenceDiagram351  participant User as Editor UI352  participant API as POST /api/publish353  participant HP as Hocuspocus354  participant Pub as Publisher355  participant FS as Local FS356  participant HF as HF Dataset357 358  User->>API: Click Publish359  API->>HP: openDirectConnection360  HP-->>API: Y.Doc snapshot361  API->>FS: Write .yjs snapshot362  API->>Pub: publishDocument()363  Pub->>Pub: extractFromYDoc + loadCSS364  Pub->>Pub: renderArticleHTML + PDF365  Pub->>FS: Write index.html locally366  Pub->>HF: uploadPublishedAssets367  Pub-->>API: { htmlUrl, pdfUrl, success }368  API-->>User: Publish result369```370 371### 7.3 Published Article Lifecycle (Container Restarts)372 373HF Spaces containers are ephemeral. The local filesystem is wiped on every restart (git push, Space rebuild, idle timeout). The published article survives via this restore flow:374 375```mermaid376sequenceDiagram377  participant Container as New Container378  participant FS as Local FS379  participant HF as HF Dataset380  participant Visitor as GET /381 382  Container->>Container: Server starts383  Container->>HF: ensurePublishedRestored()384  HF-->>FS: Pull index.html, PDF, meta.json385  Note over FS: data/published/default/index.html386 387  Visitor->>Container: GET /388  Container->>FS: Check published path389  FS-->>Container: index.html exists390  Container-->>Visitor: Serve published article391```392 393On publish, HTML is **always written locally first** (so `GET /` serves the new version immediately), then uploaded to HF dataset for persistence across restarts.394 395### 7.4 AI Agent Chat396 397```mermaid398sequenceDiagram399  participant User as Chat Panel400  participant Hook as useAgentChat401  participant API as POST /api/chat402  participant LLM as HF Inference403 404  User->>Hook: sendMessage(text)405  Hook->>Hook: Build context (doc, selection, frontmatter)406  Hook->>API: { messages, context }407  API->>LLM: streamText (system prompt + tools)408  LLM-->>API: Stream (text + tool_calls)409  API-->>Hook: SSE stream410  Hook->>Hook: Execute tool calls client-side411  Note over Hook: replaceSelection, applyDiff,<br/>updateFrontmatter, etc.412  Hook->>Hook: UndoManager batch for Cmd+Z413```414 415---416 417## 8. Current Limitations and Known Issues418 419- **Test suite in progress**: P0 tests being added (see `docs/TESTS.md`)420- **Single document**: only `"default"` document supported; no multi-doc421- **Single-user token**: last OAuth token cached globally for all HF API calls422- **No rate limiting** on `/api/chat` or `/api/citations/*`423- **XSS surface**: `meta.licence` and `biblioHtml` not escaped in published HTML424- **WS debug logging**: every WebSocket message logged in production425- **No `.env.example`**: environment variables documented only in code426