tfrere HF Staff commited on
Commit
ce5a372
·
1 Parent(s): ac48bd2

fix: prevent seed race condition with Y.Map guard and defensive checks

Browse files

- Add Y.Map("meta").seeded flag to prevent multiple clients from
seeding the same empty doc concurrently (Yjs merge duplication)
- Set seededRef early to prevent re-entry
- Add authors array length check in seedFrontmatter as extra guard

Made-with: Cursor

docs/SPECIFICATION.md ADDED
@@ -0,0 +1,398 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Collab Editor - Project Specification
2
+
3
+ ## 1. Overview
4
+
5
+ A 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 + MUI 9 + TipTap 3 + Yjs (frontend) / Express + Hocuspocus + Node 20 (backend) / Docker on HF Spaces.
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. Architecture
14
+
15
+ ```mermaid
16
+ graph TB
17
+ subgraph browser [Browser]
18
+ SPA["React SPA<br/>TipTap + Yjs"]
19
+ end
20
+
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/>OpenRouter"]
26
+ Auth["HF OAuth"]
27
+ end
28
+
29
+ subgraph storage [Persistence]
30
+ LocalFS["Local FS<br/>data/*.yjs"]
31
+ HFDataset["HF Dataset<br/>articles/ published/"]
32
+ end
33
+
34
+ SPA -->|"WebSocket /collab"| Hocuspocus
35
+ SPA -->|"REST /api/*"| Express
36
+ Hocuspocus -->|"Database ext"| LocalFS
37
+ LocalFS -->|"schedulePush"| HFDataset
38
+ HFDataset -->|"pullDocument"| LocalFS
39
+ Express --> Publisher
40
+ Express --> Agent
41
+ Express --> Auth
42
+ Publisher --> LocalFS
43
+ Publisher -->|"uploadPublishedAssets"| HFDataset
44
+ ```
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
+
52
+ The 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 ID
59
+ - **`Y.Map("settings")`** - `citationStyle`, `primaryHue`, and future editor preferences
60
+ - **`Y.Map("comments")`** - comment threads keyed by `commentId`, each with author/text/resolved
61
+
62
+ All types are concurrently editable by multiple users and persist to `data/default.yjs`.
63
+
64
+ ---
65
+
66
+ ## 4. Backend Components
67
+
68
+ ### 4.1 HTTP Routes
69
+
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` | **None** | Stream AI agent responses (OpenRouter) |
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 Collaboration
86
+
87
+ - Upgrade on `/collab` only; all other paths rejected
88
+ - 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 Storage
93
+
94
+ - Dataset ID: `HF_DATASET_ID` or `{SPACE_ID}-data`
95
+ - Token: `HF_TOKEN` (env) or cached OAuth token from last authenticated user
96
+ - **Documents**: `articles/<name>.yjs` - debounced push on every Hocuspocus store
97
+ - **Published assets**: `published/<name>/{index.html, article.pdf, thumb.jpg, meta.json}`
98
+ - **Images**: `images/<uuid-filename>` with public resolve URL
99
+ - `flushAll()` on `SIGTERM`/`SIGINT` to push pending changes
100
+
101
+ ### 4.4 Publisher Pipeline
102
+
103
+ ```mermaid
104
+ flowchart LR
105
+ YDoc["Y.Doc (.yjs)"] --> Extract["extractFromYDoc<br/>frontmatter + JSON"]
106
+ Extract --> GenHTML["generateHTML<br/>@tiptap/html"]
107
+ GenHTML --> PostProc["postProcess<br/>accordion, biblio,<br/>mermaid, htmlEmbed"]
108
+ PostProc --> Render["renderArticleHTML<br/>full HTML page"]
109
+ CSS["loadCSS<br/>template styles"] --> Render
110
+ Render --> LocalWrite["Write local<br/>index.html"]
111
+ Render --> PDF["Playwright<br/>PDF + thumbnail"]
112
+ LocalWrite --> HFUpload["uploadPublishedAssets<br/>HF dataset"]
113
+ ```
114
+
115
+ - **CSS loading**: reads template CSS files, resolves `@custom-media` queries via `resolveCustomMedia()`, splits into variables/reset/base/layout/components/article/print
116
+ - **Post-processing**: accordion divs to `<details>`, bibliography injection, mermaid to `<pre>`, htmlEmbed to `<iframe>`
117
+ - **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/DOI
118
+ - **PDF**: optional Playwright Chromium headless (1200x630 thumbnail + full PDF)
119
+ - **Server extensions**: mirror of frontend TipTap extensions for server-side HTML generation
120
+
121
+ ### 4.5 Auth
122
+
123
+ - Enabled when `SPACE_ID` + `OAUTH_CLIENT_ID` are set
124
+ - OAuth 2.0 flow with HF as provider; cookie `hf_access_token` (httpOnly, secure, sameSite: none)
125
+ - `resolveUser`: `whoAmI` via `@huggingface/hub`, then `checkWriteAccess` (Space owner or org member with write/admin role)
126
+ - In-memory state map with 10-min TTL for CSRF protection
127
+
128
+ ### 4.6 AI Agent
129
+
130
+ - Provider: OpenRouter (`OPENROUTER_API_KEY`), default model `anthropic/claude-sonnet-4`
131
+ - Streaming via Vercel AI SDK `streamText`
132
+ - **Context**: document text, current selection, frontmatter (sent by frontend with each message)
133
+ - **Tools** (declarative, executed client-side by the frontend):
134
+ - `replaceSelection` - replace selected text
135
+ - `insertAtCursor` - insert at cursor position
136
+ - `applyDiff` - search/replace in document
137
+ - `updateFrontmatter` - modify metadata fields
138
+ - `addAuthor` / `removeAuthor` - manage author list
139
+ - Agent edits are grouped in a single Yjs `UndoManager` batch for Cmd+Z
140
+
141
+ ### 4.7 Citations
142
+
143
+ - Uses `@citation-js/core` with bibtex, doi, csl plugins
144
+ - Resolve: DOI URL or identifier to CSL-JSON entries
145
+ - Format: entries + style + locale to HTML bibliography
146
+ - Import: BibTeX string to CSL-JSON
147
+
148
+ ---
149
+
150
+ ## 5. Frontend Components
151
+
152
+ ### 5.1 App Shell
153
+
154
+ - **No router** - single view with conditional rendering
155
+ - **Theme**: MUI `ThemeProvider` with dynamic primary color from `settings.primaryHue` (OKLCH color model)
156
+ - **Layout**: top bar (undo/redo, settings, publish, user chip) + 3-column CSS grid (TOC / editor / comments)
157
+ - **Chat**: floating button bottom-left, `ChatPanel` overlay
158
+ - **Modals**: comment dialog, settings drawer, publish confirmation
159
+
160
+ ### 5.2 Editor
161
+
162
+ - Creates `Y.Doc` + `HocuspocusProvider` (WebSocket to `/collab`)
163
+ - **Seeding**: after provider `synced` event only, if `Y.XmlFragment("default")` is empty, inserts `DEFAULT_CONTENT` + `seedFrontmatter` + `SEED_CITATIONS`
164
+ - **Yjs Maps**: `citations`, `settings`, `comments`, `frontmatter` (via dedicated stores)
165
+ - **Image handling**: paste/drop with upload to `/api/upload`
166
+
167
+ ### 5.3 TipTap Extensions
168
+
169
+ **Built-in (configured)**:
170
+ - StarterKit (no codeBlock, no undo), CodeBlockLowlight (all languages), Placeholder, Collaboration, CollaborationCursorV3, Mathematics (KaTeX), Image, Table/Row/Cell/Header
171
+
172
+ **Custom**:
173
+ - `CollaborationUndo` - bridges Yjs UndoManager for agent batch edits
174
+ - `Comment` - inline mark with `commentId` + `resolved`
175
+ - `SlashCommands` - `/` trigger with suggestion popup
176
+ - `ImageUpload` - drag-drop upload node with progress
177
+ - `Citation` - inline atomic node (key + label), links to `citationsMap`
178
+ - `Bibliography` - block node with rendered HTML from citations
179
+ - `Glossary` - inline atomic (term + definition tooltip)
180
+ - `Footnote` - inline atomic (content shown in footer)
181
+ - `Stack` + `StackColumn` - multi-column layout (2/3/4 cols)
182
+
183
+ ### 5.4 Component System
184
+
185
+ Registry-based system for MDX-like custom components:
186
+
187
+ | Component | Kind | Purpose |
188
+ |-----------|------|---------|
189
+ | `accordion` | wrapper | Collapsible section (details/summary) |
190
+ | `note` | wrapper | Info/warning/danger/success callout |
191
+ | `quoteBlock` | wrapper | Styled blockquote |
192
+ | `wide` | wrapper | Content wider than column |
193
+ | `fullWidth` | wrapper | Full viewport width |
194
+ | `sidenote` | wrapper | Marginal note |
195
+ | `reference` | wrapper | Reference container |
196
+ | `htmlEmbed` | atomic | External HTML embed (iframe) |
197
+ | `hfUser` | atomic | HF user card |
198
+ | `rawHtml` | atomic | Raw HTML injection |
199
+ | `mermaid` | atomic | Mermaid diagram (live preview) |
200
+
201
+ - **Factory**: `createWrapperExtension(def)` / `createAtomicExtension(def)` generate TipTap nodes from registry definitions
202
+ - **NodeViews**: `WrapperView` (editable content area + chrome), `AtomicView` (placeholder + field editor), `MermaidView` (textarea + SVG preview)
203
+ - **Slash menu integration**: each component generates a slash menu item via `getComponentSlashItems()`
204
+
205
+ ### 5.5 Frontmatter System
206
+
207
+ - `FrontmatterStore`: wraps `Y.Map` + `Y.Array` for real-time collaborative metadata editing
208
+ - `useFrontmatter` hook: React state synced with Yjs observations
209
+ - `FrontmatterHero`: WYSIWYG editable hero section (title, subtitle, authors, affiliations, date, DOI)
210
+ - `SettingsDrawer`: template variant, SEO, banner, citation style, primary color hue slider, PDF/TOC/licence toggles
211
+ - `HueSlider`: OKLCH hue picker (0-360) with live preview, synced to `settingsMap.primaryHue`
212
+
213
+ ### 5.6 Other UI
214
+
215
+ - **`TableOfContents`**: extracts headings from TipTap doc, scroll-based active state, collapsible sub-sections
216
+ - **`ChatPanel`**: message list + quick actions on selection + input with streaming
217
+ - **`CommentsSidebar`**: positioned comment threads with resolve/delete
218
+ - **`BubbleToolbar`**: floating toolbar on text selection (bold, italic, link, comment, etc.)
219
+ - **`BlockHandle`**: drag handle for block-level nodes
220
+
221
+ ### 5.7 CSS Architecture
222
+
223
+ ```
224
+ styles/
225
+ _variables.css # Design tokens, --primary-color, breakpoints, @custom-media
226
+ _reset.css # Scoped reset (avoids MUI conflicts)
227
+ _base.css # Typography, scoped to article content
228
+ _layout.css # 3-column grid, .wide/.full-width helpers
229
+ _print.css # Print styles
230
+ _editor-tokens.css # Editor-specific tokens (accents, syntax)
231
+ article.css # .tiptap content styles (shared editor/published concept)
232
+ toc.css # Editor TOC overrides
233
+ editing.css # Editor-only: layout, cursors, slash menu
234
+ components/
235
+ _code.css # Code blocks + syntax highlighting
236
+ _table.css # Tables
237
+ _tag.css # Tags
238
+ _card.css # Cards
239
+ _mermaid.css # Mermaid diagrams
240
+ _hero.css # Hero section (from template)
241
+ _toc.css # Base TOC styles (from template)
242
+ _button.css # NOT imported in editor (MUI conflict)
243
+ _form.css # NOT imported in editor (MUI conflict)
244
+ ```
245
+
246
+ The 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.
247
+
248
+ ---
249
+
250
+ ## 6. Deployment
251
+
252
+ ### 6.1 Docker Build (3-stage)
253
+
254
+ 1. **frontend-build**: `npm install` + `npm run build` (Vite)
255
+ 2. **backend-build**: `npm install` + `npx tsc`
256
+ 3. **runtime**: `node:20-slim` + Chromium system deps + `npm install --omit=dev` + Playwright Chromium + copy `frontend-dist/` + copy `frontend/src/styles/` to `frontend-styles/`
257
+
258
+ **CMD**: `node dist/server.js` on port 8080.
259
+
260
+ ### 6.2 HF Space Configuration (README.md frontmatter)
261
+
262
+ - SDK: `docker`, port `8080`
263
+ - OAuth: `hf_oauth: true`, scopes: `manage-repos`
264
+ - Two git remotes: `space` (tfrere/collab-editor, dev) and `prod` (tfrere/research-article-template-editor, production)
265
+
266
+ ### 6.3 Environment Variables
267
+
268
+ | Variable | Required | Purpose |
269
+ |----------|----------|---------|
270
+ | `PORT` | No (default 8080) | HTTP listen port |
271
+ | `NODE_ENV` | No | `production` switches to `frontend-dist` path |
272
+ | `SPACE_ID` | For OAuth/HF | HF Space identifier, enables OAuth + dataset |
273
+ | `SPACE_HOST` | For OAuth | HTTPS callback URL host |
274
+ | `OAUTH_CLIENT_ID` | For OAuth | HF OAuth client |
275
+ | `OAUTH_CLIENT_SECRET` | For OAuth | HF OAuth secret |
276
+ | `OAUTH_SCOPES` | No (default `openid profile`) | OAuth scopes |
277
+ | `HF_DATASET_ID` | No | Override dataset name (default: `{SPACE_ID}-data`) |
278
+ | `HF_TOKEN` | No | Fallback Hub token for HF API |
279
+ | `OPENROUTER_API_KEY` | For AI chat | OpenRouter API key |
280
+ | `OPENROUTER_MODEL` | No | Default AI model |
281
+ | `ENABLE_PDF` | No (default true) | Toggle PDF/thumbnail generation |
282
+
283
+ ### 6.4 Local Development
284
+
285
+ ```bash
286
+ # Terminal 1 - Backend
287
+ cd backend && npm install && npm run dev
288
+ # Starts on http://localhost:8080
289
+
290
+ # Terminal 2 - Frontend
291
+ cd frontend && npm install && npm run dev
292
+ # Starts on http://localhost:5678 (proxies /api and /collab to :8080)
293
+ ```
294
+
295
+ Create a `.env` file in `backend/` with at minimum `OPENROUTER_API_KEY` for AI chat. Without `SPACE_ID`, OAuth is disabled and all users can edit.
296
+
297
+ ---
298
+
299
+ ## 7. Key Data Flows
300
+
301
+ ### 7.1 Collaborative Editing
302
+
303
+ ```mermaid
304
+ sequenceDiagram
305
+ participant ClientA as Client A
306
+ participant Server as Hocuspocus
307
+ participant ClientB as Client B
308
+ participant Disk as Local FS
309
+ participant HF as HF Dataset
310
+
311
+ ClientA->>Server: WebSocket connect /collab
312
+ Server->>Disk: Database.fetch (load .yjs)
313
+ Server-->>ClientA: sync Y.Doc state
314
+ ClientA->>Server: Y.Doc update (edit)
315
+ Server->>ClientB: broadcast update
316
+ Server->>Disk: Database.store (write .yjs)
317
+ Disk-->>HF: schedulePush (10s debounce)
318
+ ```
319
+
320
+ ### 7.2 Publish Flow
321
+
322
+ ```mermaid
323
+ sequenceDiagram
324
+ participant User as Editor UI
325
+ participant API as POST /api/publish
326
+ participant HP as Hocuspocus
327
+ participant Pub as Publisher
328
+ participant FS as Local FS
329
+ participant HF as HF Dataset
330
+
331
+ User->>API: Click Publish
332
+ API->>HP: openDirectConnection
333
+ HP-->>API: Y.Doc snapshot
334
+ API->>FS: Write .yjs snapshot
335
+ API->>Pub: publishDocument()
336
+ Pub->>Pub: extractFromYDoc + loadCSS
337
+ Pub->>Pub: renderArticleHTML + PDF
338
+ Pub->>FS: Write index.html locally
339
+ Pub->>HF: uploadPublishedAssets
340
+ Pub-->>API: { htmlUrl, pdfUrl, success }
341
+ API-->>User: Publish result
342
+ ```
343
+
344
+ ### 7.3 Published Article Lifecycle (Container Restarts)
345
+
346
+ HF 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:
347
+
348
+ ```mermaid
349
+ sequenceDiagram
350
+ participant Container as New Container
351
+ participant FS as Local FS
352
+ participant HF as HF Dataset
353
+ participant Visitor as GET /
354
+
355
+ Container->>Container: Server starts
356
+ Container->>HF: ensurePublishedRestored()
357
+ HF-->>FS: Pull index.html, PDF, meta.json
358
+ Note over FS: data/published/default/index.html
359
+
360
+ Visitor->>Container: GET /
361
+ Container->>FS: Check published path
362
+ FS-->>Container: index.html exists
363
+ Container-->>Visitor: Serve published article
364
+ ```
365
+
366
+ On publish, HTML is **always written locally first** (so `GET /` serves the new version immediately), then uploaded to HF dataset for persistence across restarts.
367
+
368
+ ### 7.4 AI Agent Chat
369
+
370
+ ```mermaid
371
+ sequenceDiagram
372
+ participant User as Chat Panel
373
+ participant Hook as useAgentChat
374
+ participant API as POST /api/chat
375
+ participant LLM as OpenRouter
376
+
377
+ User->>Hook: sendMessage(text)
378
+ Hook->>Hook: Build context (doc, selection, frontmatter)
379
+ Hook->>API: { messages, context }
380
+ API->>LLM: streamText (system prompt + tools)
381
+ LLM-->>API: Stream (text + tool_calls)
382
+ API-->>Hook: SSE stream
383
+ Hook->>Hook: Execute tool calls client-side
384
+ Note over Hook: replaceSelection, applyDiff,<br/>updateFrontmatter, etc.
385
+ Hook->>Hook: UndoManager batch for Cmd+Z
386
+ ```
387
+
388
+ ---
389
+
390
+ ## 8. Current Limitations and Known Issues
391
+
392
+ - **No test suite**: zero tests across backend and frontend
393
+ - **Single document**: only `"default"` document supported; no multi-doc
394
+ - **Single-user token**: last OAuth token cached globally for all HF API calls
395
+ - **No rate limiting** on `/api/chat` or `/api/citations/*`
396
+ - **XSS surface**: `meta.licence` and `biblioHtml` not escaped in published HTML
397
+ - **WS debug logging**: every WebSocket message logged in production
398
+ - **No `.env.example`**: environment variables documented only in code
frontend/src/editor/Editor.tsx CHANGED
@@ -208,28 +208,29 @@ export function Editor({
208
 
209
  const seed = () => {
210
  if (seededRef.current) return;
211
- // Check the Y.Doc fragment directly (more reliable than ProseMirror state timing)
 
 
 
 
212
  const fragment = ydoc.getXmlFragment("default");
213
- if (fragment.length === 0) {
214
- seededRef.current = true;
215
- editor.commands.setContent(DEFAULT_CONTENT);
216
- seedFrontmatter(frontmatterStore);
217
- for (const [key, entry] of Object.entries(SEED_CITATIONS)) {
218
- if (!citationsMap.has(key)) citationsMap.set(key, entry);
219
- }
220
- if (!settingsMap.has("citationStyle")) {
221
- settingsMap.set("citationStyle", "apa");
222
- }
223
- } else {
224
- seededRef.current = true;
225
  }
226
  };
227
 
228
- // Only seed AFTER server sync to avoid Yjs merge duplication.
229
- // If we seed before sync, the server's existing state gets merged
230
- // with our seeded content, creating duplicates.
231
  const onSync = () => {
232
- setTimeout(seed, 100);
233
  };
234
  provider.on("synced", onSync);
235
 
 
208
 
209
  const seed = () => {
210
  if (seededRef.current) return;
211
+ seededRef.current = true;
212
+
213
+ const meta = ydoc.getMap("meta");
214
+ if (meta.get("seeded")) return;
215
+
216
  const fragment = ydoc.getXmlFragment("default");
217
+ if (fragment.length > 0) return;
218
+
219
+ ydoc.transact(() => {
220
+ meta.set("seeded", true);
221
+ });
222
+ editor.commands.setContent(DEFAULT_CONTENT);
223
+ seedFrontmatter(frontmatterStore);
224
+ for (const [key, entry] of Object.entries(SEED_CITATIONS)) {
225
+ if (!citationsMap.has(key)) citationsMap.set(key, entry);
226
+ }
227
+ if (!settingsMap.has("citationStyle")) {
228
+ settingsMap.set("citationStyle", "apa");
229
  }
230
  };
231
 
 
 
 
232
  const onSync = () => {
233
+ setTimeout(seed, 150);
234
  };
235
  provider.on("synced", onSync);
236
 
frontend/src/editor/frontmatter/seed-frontmatter.ts CHANGED
@@ -6,6 +6,7 @@ import type { FrontmatterStore } from "./frontmatter-store";
6
  */
7
  export function seedFrontmatter(store: FrontmatterStore) {
8
  if (store.get("title")) return;
 
9
 
10
  store.setAll({
11
  title: "Scaling Laws for Neural Language Models",
 
6
  */
7
  export function seedFrontmatter(store: FrontmatterStore) {
8
  if (store.get("title")) return;
9
+ if ((store.get("authors") as any[]).length > 0) return;
10
 
11
  store.setAll({
12
  title: "Scaling Laws for Neural Language Models",