Commit History
-
fix(auth): use roleInOrg from whoami, drop broken members API
The Space owned by an org never let any signed-in user through to the editor, even with full org write access. Root cause: we were reading the role from \`https://huggingface.co/api/organizations/{name}/members\`, but that endpoint returns members as plain user records (\`{ user, fullname, avatarUrl, isPro, type, _id }\`) with no \`role\` field. The check \`member.role === "write" || "admin"\` was therefore always false and every org-member visitor ended up on the login screen. Switch to the \`roleInOrg\` field that whoami already returns in \`info.orgs[i]\`. It carries one of \`admin | write | contributor | read\`, exactly what we need, and we already paid for that round-trip via \`whoAmI()\`. The org-members API call is dropped entirely. Also pick up the \`resourceGroups\` field from the same payload: HF lets \`contributor\` users get \`write\`/\`admin\` on specific groups, and the Space may live in one of those groups. Treat "contributor at org level + any write/admin group" as wr…7652473 -
feat(publisher): editor pill shows who you're signed in as
Upgrade the editor quick-link from a bare pencil icon to a proper pill that contextualises the action: ┌──────────────────────────────────────────────────────┐ │ [avatar] Signed in as @tfrere [✎ Edit article] │ └──────────────────────────────────────────────────────┘ The visitor sees who they're authenticated as before clicking, which makes the "would I be writing to the real file?" question self-answering. Useful when multiple HF accounts share the same browser session, or when a collaborator wants to confirm they're not editing on someone else's behalf. Implementation: - Render placeholder content (\"@you\" + empty <img>) so the layout doesn't shift when /api/auth/status comes back. The pill is still \`hidden\` by default - no flash for anonymous visitors. - Inline script fills the <img>.src (preferring user.avatarUrl, falling back to /api/users/<name>/avatar) and the @handle, then drops the \`hidden\` attribute. - New \`.edit-pill\` styles: rounded pill, pa…2b90d0d -
feat(publisher): editor quick-link for signed-in editors
The published article and the editor share the same Space, but a visitor who already has edit rights had no way to jump from one to the other without retyping \`/editor\` in the URL bar. Add a small pencil icon top-right, mirroring the existing theme toggle on the top-left, that links to /editor. Visibility: - Hidden by default (\`<a id="edit-link" ... hidden>\`) so non- editors / anonymous visitors never see a flash. - An inline script probes \`/api/auth/status\` on load and removes the \`hidden\` attribute when \`canEdit\` is true. Fetch failures (offline preview, exported HTML opened from disk, etc.) are silently swallowed - the button just stays hidden. - Hidden under 1100px (same breakpoint as the theme toggle) so mobile / tablet layouts aren't cluttered. Styling: matches the theme toggle (40px circle, page-bg fill, 1px border, soft shadow) so the page stays balanced visually. \`mix(--primary-color)\` tint on hover hints at the destination. Snapshot updated to reflect…44626d0 -
fix(agent): disable response buffering for chat streaming
The AI SDK streams tokens chunk-by-chunk via \`streamText\` + \`toUIMessageStreamResponse\`, but on a HF Space the response went through layers that re-coalesced everything into one big payload arriving at message end: - Nginx in front of the Space buffers responses by default unless \`X-Accel-Buffering: no\` is set; - transparent gzip on the path holds chunks until the compression window fills; - TCP_NODELAY off keeps small writes in the kernel until they reach MTU. End result: the user saw \"Thinking...\" then the whole assistant reply dropped at once, masking the streaming. Fix: - Set \`X-Accel-Buffering: no\` and \`Content-Encoding: identity\` on the streamed response so proxies pass bytes through as they arrive. - Add \`Cache-Control: no-cache, no-transform\` to also block transforming/compressing intermediaries. - Call \`res.flushHeaders()\` so the browser commits to streaming mode immediately (first-byte triggers progressive rendering of the assistant bubble)…df28ae4 -
fix(agent): forbid re-pasting tool output in the text reply
After calling \`createEmbed\` / \`patchEmbed\` (embed chat) or \`insertAtCursor\` / \`applyDiff\` (main chat), the model would re-emit the just-inserted code/prose verbatim inside its text reply. The chart is already live in the preview pane and the doc is already updated, so the dump is pure noise - it pollutes the chat history, looks like a "did it stream?" stall to the user (the long reply only appears once the full text is buffered on the client), and bloats the next turn's context window. Tighten both prompts: - embed-system-prompt: new "Reply format (STRICT)" section. Caps the reply at 1-2 sentences, explicitly forbids fenced code blocks of the chart source, bullet "feature" lists and phrases like "Voici le code" / "Just paste this where you want the banner". Keeps an escape hatch for "show me the code" requests (short relevant snippet only). - system-prompt: extend the anti-patterns list with a "NEVER paste the content you just inserted/replaced back into your te…e8173b7 -
feat(editor): WYSIWYG NodeView for HfUser cards
The HfUser block was rendering as the generic AtomicView - a dashed placeholder card with three labelled inputs - which looked nothing like the actual upstream component (avatar + display name + @handle). Authors couldn't tell what the published page would look like without a round-trip through publish. Replace it with a dedicated NodeView that reproduces the exact DOM of `app/src/components/HfUser.astro` in the editor: .hf-user └─ .hf-user__left ├─ .hf-user__avatar (img or "?" placeholder when empty) └─ .hf-user__text ├─ .hf-user__name (display name) └─ .hf-user__row └─ .hf-user__username (@handle) Inline editor: - Revealed when the node is selected, or when `username` is still empty right after insertion (prompts the author to fill at least the handle). - Three rows: Username / Display name / URL. URL placeholder switches to `https://huggingface.co/<username>` once a handle is known, so the user can see they don't need to…7814104 -
docs(readme): remove broken screenshot reference
The README pointed to ./docs/screenshot.png, which was never committed - the Space homepage rendered it as a broken image. Drop the line until we have an actual screenshot to ship. Co-authored-by: Cursor <cursoragent@cursor.com>7b7ba61 -
docs(spec): drop residual OpenRouter mentions
Three references slipped through the HF Inference migration: the system diagram, the API route table (/api/chat description) and the agent sequence diagram all still named OpenRouter as the LLM backend. Replace them with HF Inference (Providers) to match the actual implementation in stream-handler.ts. Co-authored-by: Cursor <cursoragent@cursor.com>68dccd7 -
feat(embed-studio): clickable chart list to switch files
The sidebar already lists every chart in the doc but only the current one was highlighted - the others were inert <li>s. Make non-current entries clickable so the user can jump between charts without going back to the article and reopening another embed. - FilesSidebar: optional onSelectChart prop. Non-current items render as a <button class="es-files__item-main"> (same look as the clickable data files), the current item stays a plain <li> with the "editing" badge. - EmbedStudio: forwards onSelectChart to the sidebar. - App: rotates embedStudioSession when switching, so the studio remounts and useEmbedChat's mount-only loadMessages picks up the chat history persisted for the newly-selected file. - CSS: existing .es-files__item--data { padding: 0 } pattern extended via :has() to any .es-files__item that contains a button child, so the <li> padding doesn't stack with the button's own padding. Co-authored-by: Cursor <cursoragent@cursor.com>79008fb -
fix(embed-studio): contextual empty state and input placeholder
The chat panel always said "Describe the chart you want to create" even when a chart was already rendered in the preview pane next to it - confusing because the obvious next action is to ask for tweaks, not to recreate from scratch. Branch the empty state and the textarea placeholder on three situations: - no chart yet (scratch): "Describe the chart you want to create" + the original example prompt - existing chart: "Ask for changes to the chart" + a tweak example - banner mode: phrase both as "banner" instead of "chart" so the hint matches the banner system prompt's mood-setter intent Co-authored-by: Cursor <cursoragent@cursor.com>56a91d6 -
fix(embed): SVG text now follows the iframe theme
Three stacked bugs were keeping the chart text black in dark mode: 1. EmbedStudio mounted both preview iframes with sandbox="allow-scripts" only (no allow-same-origin). The bootstrap script couldn't reach window.parent.document, so the cascade fell through localStorage to matchMedia(prefers-color-scheme) - which returns the OS preference, not the app theme. Result: the iframe could land on a different theme than the article. Aligned the sandbox with HtmlEmbedView ("allow-scripts allow-same-origin"). 2. The THEME_BOOTSTRAP in shared/embed-doc.ts always overwrote the srcdoc-provided data-theme attribute, even when every detection path failed. Honour the existing data-theme before falling back to matchMedia so an explicit isDark from buildEmbedSrcdoc wins. 3. SVG text/line/path don't inherit CSS color the way HTML elements do - their default fill/stroke is "black", not "currentColor". Charts that scoped colours via `body { color: var(--text-color) }` …be7d6fa -
feat(embed-studio): make agent-generated charts theme-aware
The iframe already supports live light/dark swap via CSS variables and a postMessage(setTheme) hot-swap, but the LLM kept generating charts that hardcode colours into SVG attributes (fill="#333", stroke="black", ...). Those values can't be overridden by the parent's data-theme attribute, so the chart looked correct on whatever theme the model guessed and broken on the other one. Two complementary fixes: 1. Forward the current theme (light/dark) from EmbedStudio through useEmbedChat into the request context, and surface it in the system prompt as a "Current theme" section. The model now knows which background it's painting against and can self-correct. 2. Rewrite the "Colors & theming" section of the embed system prompt with a concrete CSS pattern (scoped .d3-* rules driving fill/stroke via var(--text-color), --axis-color, --grid-color, ...) and an explicit ban on hardcoded SVG colour attributes. Add a hint about window.__chartRerender for JS-driven palettes. No…27abbe5 -
fix(agent): default back to gpt-oss-120b, pin Llama 3.3 to Together
Groq (HF Router's :fastest pick for Llama 3.3 70B) validates tool calls very strictly and was rejecting the editor's 18-tool registry with "Failed to call a function. Please adjust your prompt." Fireworks has deprecated the Llama 3.3 checkpoint; Nscale doesn't support the `tools` parameter at all. Together serves the same model with full tool-calling support, so pin Llama 3.3 to `:together`. Restore openai/gpt-oss-120b as the default - it's HF's trending tool-calling model and the reasoning-strip fix already covers the Cerebras round-trip case. Co-authored-by: Cursor <cursoragent@cursor.com>3afbbdf -
fix(agent): strip reasoning_content from history, default to Llama 3.3 70B
Reasoning models (gpt-oss-*, DeepSeek R1, ...) emit a `reasoning` UI part alongside their text answer. The AI SDK persists those parts and on the next turn round-trips them as `reasoning_content` on the assistant message - which some HF Inference Providers (notably Cerebras, the default for gpt-oss) reject with 400 wrong_api_format. The model doesn't need to see its own prior reasoning to keep the conversation going, so filter those parts out before convertToModelMessages. While we're at it, swap the default model from gpt-oss-120b to meta-llama/Llama-3.3-70B-Instruct: it doesn't emit reasoning, works across every provider exposing it, and is a more conservative starting point for first-time forkers. Co-authored-by: Cursor <cursoragent@cursor.com>e9c2c73 -
feat(agent): switch AI backend from OpenRouter to HF Inference Providers
The chat panel and embed studio now call Hugging Face Inference Providers (OpenAI-compatible router at https://router.huggingface.co/v1) via @ai-sdk/openai-compatible, using the logged-in editor's OAuth token when available and falling back to HF_TOKEN. Forking the Space no longer requires wiring an OpenRouter API key - the user's own HF token funds their own inference. - Replace @openrouter/ai-sdk-provider with @ai-sdk/openai-compatible - Forward the OAuth cookie token per request, fallback to HF_TOKEN - Refresh AVAILABLE_MODELS to HF-served, tool-calling-capable models (gpt-oss 120B/20B, Llama 3.3 70B, Qwen3 Coder 480B, DeepSeek V3.1) - Add inference-api to hf_oauth_scopes and document the new env vars (HF_INFERENCE_MODEL, expanded HF_TOKEN role) in README + SPECIFICATION - Update agent-chat tests to expect the new "Hugging Face token" error Co-authored-by: Cursor <cursoragent@cursor.com>6b6afea
-
feat(publisher): generate llms.txt Markdown twin for LLM agents
External LLMs/agents (Claude, Perplexity, ...) struggle with the heavy published HTML: theme bootstrap, KaTeX/Mermaid runtimes, ~7 inlined CSS files, TOC chrome, and D3 charts wrapped in <iframe srcdoc>. The static research-article-template already ships an `llms.txt` (Astro build plugin); the editor's publisher now does the same, walking the TipTap-JSON we already extract for the HTML/PDF pipeline. Pipeline addition (publisher/index.ts): - After writing index.html, render the same JSON to Markdown via the new markdown-renderer.ts and write `published/<doc>/llms.txt` - Re-render with the public HF meta and include it in the dataset upload commit (hf-storage.ts adds llms.txt to the file list) Routing (create-app.ts): - GET /llms.txt serves the published Markdown twin with Content-Type: text/markdown; charset=utf-8 (404 before publish) - GET /robots.txt advertises it via the `LLMs-Txt:` header, mirroring the upstream template Conventions in the renderer mirror the upstream Ast…3de227d
-
fix(publish-dialog): show Space URL instead of /published/<doc> path
The success state used to display `Live at /published/default` and link to `/published/default/index.html`, surfacing an implementation detail of the publisher pipeline (used internally for PDF rendering and og:image) rather than the canonical URL readers actually visit. When a published article exists, the backend serves it at the Space root (`app.get("*")` in `create-app.ts`). Switch the modal to surface `window.location.origin` instead, both for the displayed URL and the "View published article" link target. Made-with: Cursor8d2864f -
fix(publisher): render HF user card via dedicated transformer
The hfUser atomic node was serialized to an empty `<div data-component="hfUser">` placeholder with no transformer to turn it into the actual profile card, so it rendered as nothing in published articles. Add `hfUserTransformer` mirroring the upstream `HfUser.astro` (avatar + name + @handle), register it in the pipeline, ship the matching `.hf-user` styles in `_publisher.css`, and cover the three cases (full attrs, fallback to username, missing username) with unit tests. Made-with: Cursorf621e1d -
docs(readme): add 3-step guide to fork the Space and wire OpenRouter
- New "Run your own copy on a Hugging Face Space" section: duplicate the Space, grab an OpenRouter key, paste it as a Space secret - Fix the stale OPENAI_API_KEY hint in the local-dev snippet (we use OPENROUTER_API_KEY) Made-with: Cursoraba5856 -
refactor(login-page): neutral title, outlined CTA, drop wordmark
- Title back to default text color (dropping the primary tint) - Switch the CTA from a filled pill to an outlined pill: 1.5px primary border, transparent background, color-mix tint on hover/active - Remove the "Research Article Template Editor" wordmark for a cleaner silhouette Made-with: Cursor0332cd4 -
feat(login-page): full HF logo, primary title, pill CTA, refined layout
- Inline the complete official Hugging Face logo SVG (face + both hugging hands), fixing the previously truncated mark in the button - Move the title to primary color and bump it to a fluid clamp size with Apple-grade letter-spacing - Replace the outlined CTA with a pill-shaped primary button: vertical gradient fill, layered inset highlights, soft elevation, 1px lift on hover with deeper shadow, slightly tighter letter-spacing - Drop the "after signing in, refresh this page" hint since the OAuth flow redirects automatically - Add a soft radial primary tint on the page background and a discreet wordmark below the CTA for brand presence - Use the SF / Source Sans Pro stack with antialiasing tuned for both light and dark modes - Simplify renderLoginPage: hint slot removed, callers now only pass title and description Made-with: Cursor7f90538 -
feat(login-page): align with app design tokens, bigger outlined HF button
- Extract a single renderLoginPage() helper used by both the editor login gate and the published-article fallback (no more duplicated inline HTML) - Use the app's primary color (OKLCH) and Source Sans Pro font, with proper light/dark via prefers-color-scheme so the page no longer looks like an unrelated dark stub - Replace the small filled button with a bigger outlined CTA (14×22 px padding, 1.5px primary border, color-mix hover) - Inline the official Hugging Face brand logo SVG (no external network dependency, no emoji fallback) - Escape user-controlled hint substitutions through a dedicated escapeHtml() Made-with: Cursord8c3c68 -
chore(release): prep v0.1.0 with LICENSE, env example and agent undo test
- Add LICENSE (CC-BY 4.0) aligned with the upstream research-article-template - Complete backend/.env.example to cover every env var consumed by the backend (PORT, DATA_DIR, ENABLE_PDF, OAUTH_*, SPACE_ID/SPACE_HOST, HF_DATASET_ID, HF_TOKEN, OPENROUTER_API_KEY/MODEL, PUBLISH_BASE_URL) - Extract createAgentBatch() helper from useAgentChat into frontend/src/editor/agent-undo-batch.ts (pure module, no React) and add the P0 5.3.1 test from docs/TESTS.md: 3 chained agent edits (replaceSelection + applyDiff + updateFrontmatter style) revert with a single Cmd+Z, while the previous user edit stays untouched - Refresh README known-debt list to reflect the new coverage Made-with: Cursord63dcfe
-
feat(demo): full showcase polish - mouse cursor, robust selection, HF affiliations
Demo orchestration: - Visible mouse cursor overlay (popover top-layer, MutationObserver to stay above native dialogs) + humanMove/humanClick/humanHover helpers - Yjs Node.js clients for Bob and Carol with awareness cursors and a set of high-level helpers (writeBlockMathAtSlot, setAffiliationUrl, waitForAffiliationCount, etc.) so peer interactions never collide - Pre-bold sync barrier: Alice waits for Bob's block math and Carol's code block to land before doing the bubble-toolbar bold so layout is frozen during the drag selection (no more cross-paragraph selections) - Progressive cursor-driven text selection (visible glide + step-wise setTextSelection) for bold and rephrase actions - Custom rAF-eased scroll-to-top on the actual .editor-scroll container before the hue drag so the banner retint reads on camera Embed Studio: - New architecture.json companion data file (transformer-block layers) seeded alongside model-accuracy.csv, so the agent has something concrete to r…f469961
-
fix(ui,publisher): kill theme/color flickers and translation prompts
- Published page and its banner iframe now inline a synchronous theme bootstrap in <head>, so the first paint already uses the correct data-theme (no dark -> light flicker on navigation). - <meta name="google|robots" content="notranslate"> on both the editor (frontend/index.html) and the published article so Chrome stops proposing to translate the page to French. - CollaborationCursorV3: silence a peer's caret + label (via the .collaboration-cursor--silenced class) when that same peer also broadcasts an agentFocus. Fixes the duplicate "<Name>" + "<Name> agent" labels that stacked while a chat action was active. - Embed Studio preview container pinned to --page-bg and the iframe element forced to background: transparent + color-scheme: light dark, so the preview tracks dark mode instead of falling through to the browser's default white. - App.tsx primaryHue sync: when the Yjs settings map has no primaryHue (fresh state or after reset-article), drop the inline --p…08e4c7a -
feat(editor): embed studio with data files and agent-aware editing
Editor - New Embed Studio panel for AI-assisted chart creation: upload and manage data files (3MB limit), browse them in a dedicated viewer, and iterate on the generated HTML via a chat wired to a dedicated embed agent (embed-chat + embed-system-prompt + embed-tools + tiptap-catalog) that can create, rename and edit embeds. - Tiptap extensions for agent-driven editing: - agent-focus: Yjs-awareness ranges so every peer sees where another user's AI is working ("<Name> agent" label). - agent-highlight: local PM decoration to mark the paragraph the agent is currently rewriting. - agent-rewrite: typewriter-animated range rewrite command used by the rephrase flow. - Rephrase flow now goes through agent-rewrite (typewriter) instead of a plain replace; bold demonstration moved to the floating bubble toolbar above a selection. - SlashMenu reorganised with grouping and dev-only items dropped. - New components: DataFileViewer, FilesSidebar, UploadZone. - …8fc8501 -
feat(demo): modular showcase replacing monolithic trio script
- Split trio.ts into showcase.ts + alice.ts / bob.ts / carol.ts and a lib/ of reusable helpers (chat-actions, publish-flow, positions, chromium, recording, scaffolding, editor-actions, selection, embed-studio). - Drift-proof link/citation/rephrase actions: target positions are resolved at dispatch time against the live doc instead of at event receipt time, so concurrent peer edits no longer shift the range. - verifyFinalDoc post-run collision check to surface cases where two personas stepped on each other's slots. - test-actions.ts for isolated regression runs. - Chromium launched with en-US locale + --disable-features=Translate so the Chrome translate prompt never appears; published article is opened in-place in Alice's existing fullscreen window. - .gitignore: demo/recordings/ is now ignored. Made-with: Cursor19b3f9c
-
fix(docker): invoke playwright CLI by package path to survive --omit=dev
`npm install --omit=dev` in the production stage skips @playwright/test but leaves a dangling `.bin/playwright` symlink that used to resolve to it. `npx playwright` then fails with "playwright: not found" even though the runtime `playwright` package is installed. Call `node node_modules/playwright/cli.js install chromium` directly to bypass the `.bin/` lookup and install the Chromium browser from the production `playwright` package. Made-with: Cursord13608d -
fix(docker): symlink frontend node_modules into /app/backend so shared imports resolve
The frontend build aliases `#shared` to `/app/backend/src/shared/`. Files there import npm packages (e.g. shiki) that are declared in frontend/package.json. Rollup walks up from the shared file looking for node_modules and never reaches /app/frontend/ in the Docker layout, which breaks the build with: Rollup failed to resolve import "shiki" from /app/backend/src/shared/shiki-config.ts Local dev avoids this because both workspaces install node_modules side by side. The symlink reproduces that layout inside the frontend-build stage without pulling any of the backend runtime deps into the frontend bundle. Made-with: Cursor1f03cd7 -
docs: refresh README, architecture, spec and add agent teams proposal
- README: align env var table with real backend (OPENROUTER_API_KEY, HF_DATASET_ID, SPACE_ID, PUBLISH_BASE_URL, ENABLE_PDF, OAUTH_SCOPES) and fix prerequisites - ARCHITECTURE / SPECIFICATION: describe the new transformer pipeline, embed studio flow, mobile TOC + CommentPopover; drop the deleted CommentsSidebar reference - TESTS: announce the Playwright E2E suite as part of the stack and add 4.1.4 for the upload rate limiter; note the split between fast hermetic suite and opt-in E2E - Add AGENT_TEAMS_PROPOSAL.md to track the upcoming multi-agent collaboration design Made-with: Cursor32a4aca -
feat(frontend): editor refresh (embed studio, comment popover, shiki, top bar, hooks, styles)
- New top bar + publish dialog + sync indicator + mobile TOC sidebar replace the previous floating actions and full sidebar stack - Add EmbedStudio chat panel wired to the backend embed agent (embed-tools + stream handler), with fullBleed iframe mode for banner rendering and double-buffered previews - Swap legacy comments sidebar for a floating CommentPopover anchored to the active thread - Shiki-based code block extension + scroll guard on top of TipTap v3; richer frontmatter settings (hue slider, template variant, SEO) and Editor footer - Shared hooks (useTheme, useEmbedChat, usePublishStatus) and a singleton MutationObserver so all components subscribe to theme changes from the same source - Reorganize CSS into scoped modules (_ui, _footer, _embed, _embed-studio, _hero, _mermaid, _toc, _form, _code, tokens, layout, publisher, print) and drop the dead postcss config - Add frontend vitest suite + tsconfig(+node) + vite-env typings + typed utils/types modules Made-with: Curs…76fc93a -
feat(demo): add trio Playwright orchestration + seed embeds for the pitch video
- backend/demo/trio.ts drives 3 real Chromium sessions (Alice/Bob/Carol) collaborating on a fresh document in under 30s for the demo reel, with human-like typing and kill-leftovers hygiene on every run - Centralize the hand-crafted banner HTML and demo-specific embeds (confusion matrix, memory bar) so the script stays deterministic for recordings - Expose a `load-demo` entrypoint used by the dev-only script to hydrate the editor in one shot Made-with: Cursorf5f4567 -
test(backend): add Playwright E2E suite + deps for editor basics, chat persistence and publish
- Covers 9 flows: title persistence through Hocuspocus reload, settings drawer, chat panel open/close, new-conversation clear, stable user identity across reloads, publish round-trip - Bundled as `npm run test:e2e` (opt-in, not part of the fast hermetic suite) with spec-driven fixtures that spin up a real Express + Hocuspocus server on an ephemeral port - Brings playwright + @playwright/test into backend/package.json Made-with: Cursor6a34a48 -
feat(backend): rate-limit /api/upload to prevent anonymous abuse
Upload stays intentionally unauthenticated (covered by spec 4.1.3), so guard it with a simple per-IP sliding-window limiter (30 uploads / 60s). The limiter returns 429 with a Retry-After header and uses the X-Forwarded-For head when behind a proxy. Spec updated with the new 4.1.4 test line. Made-with: Cursor5576f9a -
refactor(backend/publisher): transformer pipeline + shared registry + shiki highlighting
- Split html-renderer post-processing into per-feature transformers (accordion, bibliography, citation, footnote, highlight-code, html-embed, mermaid) with a shared types contract - Extract embed iframe srcdoc builder, shiki config and theme resolver into backend/src/shared so the frontend can reuse the exact same palette and embed chrome - Keep the renderer entrypoint (index.ts) as an orchestrator and trim the legacy logic to the new querySelector-based scope selection (simpler + covered by updated snapshot) - Update publisher/css-resolution/security suites + snapshot to the new output Made-with: Cursor7843436 -
refactor(backend): modular server split with new routes, persistence and agent layer
- Introduce create-app factory to compose routes, middleware and Hocuspocus in isolation - Extract route modules (auth, chat, publish) and move storage logic into persistence.ts - Harden auth + HF storage integration (user token fallback, secure cookie in production) - Add embed studio AI pipeline (embed-chat, embed-tools, system prompt) and shared stream handler - Cover the new surface with api-routes, persistence and agent-chat test suites - Ship .env.example and .gitignore so new contributors can boot without guesswork Made-with: Cursorf6678ab
-
refactor: remove MUI, unify publisher pipeline, add shared component registry
Phase 1 - Publisher pipeline: - Extract ~370 lines of inline CSS from html-renderer.ts into _publisher.css - Create shared/component-defs.ts as single source of truth for component definitions - Replace 6 regex blocks in postProcess() with linkedom DOM manipulation - Add GET /api/preview/:docName for testing renders without publishing - Add 12 snapshot tests for each postProcess transformation (46 tests total) Phase 2 - Remove MUI: - Replace all MUI components with native HTML elements + custom CSS - Create _ui.css with editor chrome styles using --ed-* CSS tokens - Create Tooltip.tsx using Floating UI (lightweight replacement) - Replace @mui/icons-material with lucide-react - Delete theme.ts, remove @mui/* and @emotion/* dependencies Phase 3 - Documentation: - Add docs/ARCHITECTURE.md covering CSS layers, HF Spaces constraints, shared registry, publisher pipeline, and test coverage Made-with: Cursord15d7f7 -
fix: TOC mobile toggle not visible due to CSS cascade order
The _toc.css component file (loaded last) set display:none on .toc-mobile-toggle/backdrop/sidebar by default, overriding the @media responsive rules in _layout.css (loaded earlier). Move the responsive display overrides into _toc.css so they appear after the default rules and win in the cascade when the media query matches. This fixes the TOC being completely invisible on viewports < 1100px (including HF Spaces iframe at ~968px). Made-with: Cursorc68749f -
fix: TOC scroll navigation in published articles
- Add scroll-behavior: smooth and scroll-padding-top: 80px to html - Add click handlers on TOC links with smooth scroll and proper offset - Guard lightbox JS against missing elements to prevent script crash that could block TOC initialization Made-with: Cursor9aec70e -
fix: server-side bibliography formatting and citation linking
- Add format-bibliography.ts: formats CSL-JSON entries server-side using citation-js, independent of client-side renderedHtml - Publisher now reads Y.Map("citations") and Y.Map("settings") directly from the Y.Doc to build bibliography at publish time - Inline citations are converted to clickable links pointing to their bibliography entry (with :target highlighting) - Supports both numeric (ieee/vancouver) and author-date styles - Falls back to client-side renderedHtml if server formatting fails - Add CSS for .citation-inline and .csl-entry:target Made-with: Cursor34087bd -
fix: enable PDF generation by adding playwright dependency
- Add playwright as a production dependency in backend - Set PLAYWRIGHT_BROWSERS_PATH in Dockerfile for reliable browser discovery - Remove silent fallback on playwright install to surface errors - Remove non-functional PDF button from editor hero (only relevant in published output) - Respect showPdf frontmatter flag in publisher pipeline Made-with: Cursor8d245e0 -
fix: generate proper footnotes section in published HTML
- Collect all inline footnotes during postProcess and replace with numbered superscript links ([1], [2], etc.) - Append a <section class="footnotes"> with ordered list at the bottom - Footer JS automatically moves this section into the footer area - Add footnote-ref styling (primary color, hover underline) - Make extractBibliographyHtml recursive to find bibliography in nested nodes - Add two demo footnotes to the default article content Made-with: Cursor77b813e -
feat: align component CSS with research-article-template
- Note: left border accent style with color-mix variants (info/success/danger) - QuoteBlock: decorative open-quote mark, large text, author/source footer - Accordion: template-matching border, bg, chevron, open state highlight - Sidenote: muted text, generous padding (matches Sidenote.astro) - Reference: proper spacing and caption styling - Stack: responsive breakpoints matching template (768/1100px) - Tables: zebra stripes, template header bg, vertical dividers - Wide/fullWidth: proper overflow layout in editor - Add PDF download button to hero meta section (using showPdf flag) - Update publisher HTML styles for all components to match template Made-with: Cursor9f7fae9 -
feat: show real HF username and avatar for connected users
- Fetch /api/auth/status on mount to get HF user info (name, avatar) - Replace random names with actual HF fullName and profile picture - Update collaboration cursor to display avatar next to username - Update top bar Chip with avatar component - Sync awareness when user data arrives after initial render - Use deterministic color from username hash instead of random Made-with: Cursor15dff0c -
fix: prevent seed race condition with Y.Map guard and defensive checks
- 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: Cursorce5a372 -
fix: replace undefined __dirname with DATA_DIR in ESM server
__dirname was removed during utils.ts extraction but still referenced in staticDir computation, causing a ReferenceError crash on startup. Made-with: Cursorac48bd2 -
test: add vitest test suite for publisher, auth, security, utils
- Setup vitest with test helpers for Y.Doc fixtures - 34 tests across 6 files covering: - Publisher pipeline (extraction, HTML gen, post-processing, idempotency) - XSS prevention (licence, title, description, authors, DOI) - Auth (token extraction, OAuth config) - Utils (sanitizeName, docPath, path traversal) - CSS @custom-media resolution - HF Storage configuration Made-with: Cursor755a930 -
fix: XSS escaping, DRY utils, agent undo batch, graceful shutdown
- Escape meta.licence in published HTML to prevent XSS - Extract shared sanitizeName/docPath/DATA_DIR into utils.ts (DRY) - Disable WS debug logging in production - Fix startup log message (was /collab/:doc, now /collab) - Remove unused NextFunction import in auth.ts - Remove duplicate JSDoc in publisher/index.ts - Add timeout to graceful shutdown (SIGTERM/SIGINT) - Fix agent undo batch: close batch on stream finish (onFinish) - Fix replaceSelection: return error when no text selected Made-with: Cursore371833 -
fix: prevent Y.Doc content duplication from seed race condition
The editor was seeding default content on an 800ms timer that could fire before the Hocuspocus sync completed. When the server had existing content from the HF dataset, Yjs would merge both copies (different client IDs), causing all content, authors, and affiliations to appear multiple times. Fix: only seed after the "synced" event, and check the Y.Doc fragment directly instead of ProseMirror state. Also adds /api/admin/reset-document endpoint to clear corrupted Y.Doc state. Made-with: Cursor1c21ac9 -
fix: always write published HTML locally before HF upload
The publish pipeline was uploading to HF dataset and returning immediately without updating the local copy. Since GET / serves the local file, the old HTML persisted until a container restart. Now the local copy is written first, so the published page updates instantly. HF upload still happens afterwards for persistence. Made-with: Cursorf2312e5