tfrere's picture
tfrere HF Staff
docs: refresh README, architecture, spec and add agent teams proposal
32a4aca
# Architecture
## Overview
The collab-editor is a collaborative article editor built for Hugging Face Spaces. It runs as a single Docker container serving both the backend (Express + Hocuspocus) and the frontend (React + TipTap).
```
┌─────────────────────────────────────────────────────────┐
│ Docker Container (port 8080) │
│ │
│ ┌──────────────────┐ ┌────────────────────────────┐ │
│ │ Express Server │ │ Hocuspocus (Y.js collab) │ │
│ │ │ │ │ │
│ │ /api/* │ │ /collab (WebSocket) │ │
│ │ /published/* │ │ │ │
│ │ /editor │ │ │ │
│ │ / (published) │ │ │ │
│ └──────────────────┘ └────────────────────────────┘ │
│ │
│ ┌──────────────────┐ ┌────────────────────────────┐ │
│ │ Publisher │ │ Frontend (static) │ │
│ │ (HTML renderer) │ │ /editor -> SPA │ │
│ │ (PDF generator) │ │ React + TipTap │ │
│ └──────────────────┘ └────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
```
## Key directories
| Path | Description |
|------|-------------|
| `backend/src/server.ts` | Entry point: imports `createApp()`, starts listener, signal handlers |
| `backend/src/create-app.ts` | Express app factory: routes, Hocuspocus, WebSocket, middleware |
| `backend/src/publisher/` | HTML rendering, PDF generation, bibliography formatting |
| `backend/src/publisher/html-renderer.ts` | Converts TipTap JSON to static HTML page |
| `backend/src/publisher/extensions.ts` | Server-side TipTap extensions (mirrors frontend) |
| `backend/src/shared/component-defs.ts` | Shared component definitions (single source of truth) |
| `backend/src/utils.ts` | Shared utilities: `docPath()`, `sanitizeName()`, injectable `DATA_DIR` |
| `backend/src/auth.ts` | OAuth flow, token extraction, user resolution |
| `backend/src/hf-storage.ts` | HF dataset sync (push/pull documents and assets) |
| `frontend/src/editor/` | TipTap editor, toolbars, components |
| `frontend/src/editor/components/registry.ts` | Component registry (imports from shared defs) |
| `frontend/src/styles/` | All CSS files |
## Styling architecture
### CSS layers
The project uses five CSS layers, loaded in this order by `main.tsx`:
1. **Template foundation** (`_variables.css`, `_reset.css`, `_base.css`, `_layout.css`, `_print.css`, `components/*`)
- Defines the article's visual identity (typography, grid, components)
- Uses CSS custom properties (`--text-color`, `--surface-bg`, `--primary-color`, etc.)
- Layout tokens centralize grid math (`--layout-toc-width`, `--layout-content-width`, `--layout-gap`, breakpoints)
- Shared between the editor preview and the published output
2. **Editor chrome** (`_ui.css`)
- Styles the editor UI: top-bar, sidebars, dialogs, chat panel, embed studio
- Uses `--ed-*` custom properties for the dark editor theme
- Only loaded in the editor, never in published output
3. **Design tokens** (`tokens.css`)
- Light/dark theming tokens for both editor and article
- Text, background, border, accent, code highlighting, danger, shadows
- Supports `data-theme` attribute and `prefers-color-scheme` media query
- Shared between editor and published output (injected by publisher)
4. **Shared article styles** (`article.css`, `toc.css`)
- Article content styles shared between editor preview and published output
- `article.css` includes wrapper components (Note, Stack, Quote, Sidenote...), with editor-specific variants scoped by `.editor-app`
5. **Editor-only styles** (`styles/editor/*.css`, 5 files)
- `_layout.css`: grid overrides (3-col symmetric, aligned with template), TOC drawer, responsive breakpoints (1100px collapse, 768px mobile)
- `_chrome.css`: ProseMirror editing visuals (placeholder, selection, cursors, comment marks, math editing)
- `_block-tools.css`: block handles (drag + add) and slash menu
- `_panels.css`: image upload card, footnote tooltip, citation panel
- `_hero-editable.css`: transparent click-to-edit inputs for FrontmatterHero
6. **Publisher CSS** (`_publisher.css`)
- Styles specific to the published static HTML page
- Theme toggle animations, wide/fullWidth breakout, sidenote float, lightbox, PDF link
- Only injected by the HTML renderer (backend), never loaded in the editor
### No CSS-in-JS
The project does not use MUI, Emotion, or any CSS-in-JS library. All styling is done via:
- CSS custom properties for theming
- Vanilla CSS files with BEM-like class naming
- `Floating UI` for tooltip positioning (lightweight, no CSS-in-JS)
### Color theming
- The article area uses `data-theme="light"` / `data-theme="dark"` with CSS variable overrides
- The editor chrome is always dark, using `--ed-*` tokens
- The primary accent color is controlled via `--primary-color` (synced via Yjs settings)
## HF Spaces constraints
### Iframe embedding
When deployed as a HF Space, the app runs inside an iframe. This affects:
- **Viewport width**: the iframe is ~968px wide, not the full browser width
- **No `target="_top"` navigation**: links open within the iframe unless using `target="_blank"`
- **OAuth flow**: the OAuth callback URL must match the Space URL
- **CSP restrictions**: sandboxed iframes may restrict certain APIs
### Two-page architecture
| URL | What it serves |
|-----|----------------|
| `/` | Published article (static HTML) or login prompt |
| `/editor` | The SPA editor (requires authentication) |
This means:
- The published article is a completely standalone HTML file
- It does NOT load React or any JS framework
- The editor is a separate React SPA at `/editor`
### CSS cascade
Because the published HTML is self-contained (all CSS inlined in `<style>`), there are no CSS conflicts with the HF Spaces iframe CSS. The editor uses Vite's CSS pipeline.
## Shared component registry
Component definitions (name, kind, fields, defaults) are defined once in `backend/src/shared/component-defs.ts`. This file is the single source of truth.
- **Backend** (`extensions.ts`): imports `SHARED_COMPONENT_DEFS` to generate TipTap server extensions for `generateHTML()`
- **Frontend** (`registry.ts`): imports `SHARED_COMPONENT_DEFS` via Vite alias `#shared` and decorates each entry with UI metadata (icon, label, description, placeholders)
Adding a new component:
1. Add the entry to `shared/component-defs.ts`
2. Add UI metadata to `frontend/src/editor/components/registry.ts` in `UI_META`
3. Add CSS for the published view to `frontend/src/styles/_publisher.css`
## Publisher pipeline
```
Y.Doc -> TiptapTransformer -> JSON -> generateHTML() -> postProcess() -> full HTML page
|
v
PDF (Playwright)
|
v
Upload to HF dataset
```
### Post-processing (linkedom)
The `postProcess()` function uses `linkedom` for DOM manipulation instead of regex:
- Accordion `<div>` -> `<details>/<summary>`
- Citation `<span>` -> `<a>` links with bibliography anchors
- Bibliography placeholder -> formatted HTML with entry IDs
- Mermaid `<div>` -> `<pre class="mermaid">`
- HtmlEmbed `<div>` -> `<iframe>`
- Footnotes -> superscript links + appended section
### Preview endpoint
`GET /api/preview/:docName` renders the HTML without saving or uploading. Useful for testing the publisher pipeline.
## Auth and security
- AI chat routes (`/api/chat`, `/api/embed-chat`) are auth-guarded when OAuth is enabled
- The `requireEditor` middleware checks cookie token and verifies write access
- Published articles are served without auth (public)
## Testing
Tests use Vitest + Supertest. Run with `npm test` from `backend/`.
| Test file | What it covers |
|-----------|----------------|
| `tests/publisher.test.ts` | Y.Doc extraction, HTML generation, post-processing, idempotency |
| `tests/html-renderer-snapshot.test.ts` | Snapshot tests for each postProcess transformation |
| `tests/security.test.ts` | XSS prevention in published HTML |
| `tests/css-resolution.test.ts` | @custom-media resolution |
| `tests/utils.test.ts` | Path sanitization utilities |
| `tests/auth.test.ts` | Token extraction, OAuth configuration |
| `tests/hf-storage.test.ts` | HF dataset storage configuration |
| `tests/persistence.test.ts` | Local file persistence, debounced save |
| `tests/api-routes.test.ts` | API route integration tests (publish, auth status) |