Spaces:
Sleeping
Sleeping
Commit History
-
fix(highlight): auto-detect language for code blocks without explicit lang
Code blocks authored without a language rendered as plain text in both the editor and the published output (Shiki falls back to "text"). Add a shared lowlight-based detector and use it as a fallback in the editor decorations and the publish transformer, so language-less blocks get highlighted identically everywhere - including in already-written articles. Co-authored-by: Cursor <cursoragent@cursor.com>bf2abd0 -
fix(publisher): render math server-side via KaTeX
TipTap's math extension emits empty inline/block-math placeholders during generateHTML (rendering is a view-only step). Add a math transformer that fills those placeholders with static KaTeX HTML so formulas appear in the published article. Pin the KaTeX CSS CDN version to the server-side katex. Co-authored-by: Cursor <cursoragent@cursor.com>d062662
-
feat(editor): dedupe connected collaborators by identity
The presence avatars were keyed by Yjs awareness clientId, so the same person showed up once per connection: every extra tab/device, and every lingering pre-timeout socket left behind by a reconnect (amplified by the refocus re-sync), rendered the same circle again. Collapse awareness clients sharing an identity (HF username, falling back to avatar URL then clientId) into a single avatar, with a "xN" badge + tooltip when a person has multiple live sessions. Co-authored-by: Cursor <cursoragent@cursor.com>a286ca4 -
feat(editor): authors & affiliations manager modal
Replace the undiscoverable inline drag with a single, discoverable modal for the whole byline. From the hero (pencil button, clicking an author, or clicking an affiliation) users open AuthorsManager to add/edit/remove authors and affiliations and reorder both lists via explicit up/down controls. - store: add updateAffiliation/removeAffiliation; removeAffiliation reconciles every author's 1-based references (drop + shift) like moveAffiliation does - extract AuthorInlineForm into its own file, reused as the add/edit form (stacked dialog) inside the manager - affiliations get inline name/URL editing with debounced commits - drop the inline drag-and-drop handlers/styles from the displayed header Co-authored-by: Cursor <cursoragent@cursor.com>1f88239 -
fix(editor): preview the banner correctly in the Embed Studio
The studio built every preview srcdoc without fullBleed and let the iframe fill the whole preview pane, so the banner (an edge-to-edge 5:2 embed) was rendered like a height-reporting chart and looked nothing like the hero. When editing banner.html, build the srcdoc with `fullBleed: true` and render it inside a centered 5:2 stage (matching .hero-banner dimensions), so the studio preview is a true WYSIWYG of the published header. Regular charts are unchanged. Co-authored-by: Cursor <cursoragent@cursor.com>09a820f -
Update README.md
Verified signatureae6c9d8 -
feat(editor): Embed Studio button in the top bar
Adds a chart button to the TopBar that opens the Embed Studio. Smart entry point: when charts already exist it opens the studio on one of them (prefers a real chart over the always-present banner) without mutating the document; only when there is no chart does it fall back to inserting a fresh one, mirroring the slash menu's "New Chart" flow. Both paths route through the existing `open-embed-studio` event so session/key handling stays in one place. Co-authored-by: Cursor <cursoragent@cursor.com>2dfb335 -
feat(editor): drag-and-drop reordering for authors and affiliations
Authors and affiliations in the hero meta header can now be reordered by dragging. Reordering affiliations remaps every author's 1-based affiliation indices in the same Yjs transaction, so superscripts keep pointing at the right institution. Drop affordances (grab cursor + primary-color edge marker) live in the editor-only _hero-editable.css and never leak into the published HTML. Co-authored-by: Cursor <cursoragent@cursor.com>a2d6d01 -
fix(publisher): render banner in fullBleed mode to match the editor
The editor preview builds the banner srcdoc with `{ fullBleed: true }` (FrontmatterHero), but the publisher built it with no options. Without fullBleed the published banner kept the default 16px body padding, lacked the 100%-height chain, and ran the height reporter (resizing the iframe to the content scrollHeight instead of the parent's 5:2 aspect-ratio box) - so the published banner looked different from the editor. Pass `{ fullBleed: true }` for parity. Co-authored-by: Cursor <cursoragent@cursor.com>2c3dcf7
-
test(editor): extract refocus re-sync into a unit-tested module
Pull the tab-refocus liveness/lock logic out of the Editor effect into a pure createRefocusSync() so it can be exercised without a browser. Add vitest coverage for the four cases it must distinguish: healthy (no lock), disconnected (lock + unlock on synced), dead readyState, half-open socket (lock after the post-forceSync probe), and the offline grace release. The effect now only wires the visibilitychange trigger. Co-authored-by: Cursor <cursoragent@cursor.com>45d6656 -
chore(deps): align @hocuspocus /provider with server (v3)
The backend declared @hocuspocus/provider ^4.0.0 while the server and frontend run ^3.4.4. A v4 provider (used by the demo clients) against a v3 server risks protocol drift; pin it to the same major. Co-authored-by: Cursor <cursoragent@cursor.com>24e2428 -
feat(editor): local-first persistence + robust refocus re-sync
Prevents a stale never-closed tab from clobbering a collaborator's newer content (the core data-loss incident): - Mesh y-indexeddb onto the shared Y.Doc so edits survive refresh / offline, and a client can re-sync its durable local copy back up if the server ever loses or resurrects a stale document. - On tab refocus, force a sync round-trip and hold the editor read-only until it completes, so the user can't type onto a visibly-stale buffer. Detect dead sockets via websocket status, readyState and last-message timestamp (incl. half-open sockets via a post-forceSync probe). Always release after a 4s grace window so offline editing is never trapped (local-first). Co-authored-by: Cursor <cursoragent@cursor.com>fe9b487
-
feat(hero): persistent Edit + Delete buttons on the banner
A filled banner now exposes two buttons in a top-right action cluster: - Edit (PencilLine icon) - opens the Embed Studio on the banner key so authors can iterate on the HTML via the AI agent, just like any other htmlEmbed. Works whether the banner is an image upload, a chart placeholder, or a custom fragment. - Remove (Trash2 icon, danger hover) - same behavior as before. Both buttons are constantly visible instead of fading in on hover: the banner is a first-class authoring surface and a hidden mutation affordance was confusing once you'd seeded the placeholder content. CSS shifts from .hero-banner-remove (single button, opacity-gated) to .hero-banner-actions (cluster) + .hero-banner-action (per-button styling, always opacity:1). Danger variant tints the Remove button red on hover/focus so the destructive action is still distinguishable from the Edit one. Co-authored-by: Cursor <cursoragent@cursor.com>55f5851 -
feat(auth): surface canEdit in the topbar with an access badge
`/api/auth/status` already returned a `canEdit` flag derived from the HF whoami roleInOrg (Space owner, or org write/admin/contributor with a write resource group), but the frontend was ignoring it. Result: read-only users saw an enabled Publish button + the regular user chip, then hit a confusing 403 from the backend on every mutating action. - App.tsx plumbs `canEdit` from /api/auth/status into a new state and forwards it to TopBar. - TopBar renders a small badge next to the user chip: "Editor" (green outline + PencilLine icon) when canEdit, "Read-only" (muted + Lock icon) otherwise, both with a tooltip explaining the state. - Publish button is disabled when !canEdit with a matching tooltip, so the read-only restriction is visible up front instead of after a failed network round-trip. - `.chip--editor` and `.chip--readonly` variants added to _ui.css. Co-authored-by: Cursor <cursoragent@cursor.com>0e7d2a3 -
feat(editor): Iframe embed component for remote URLs
Adds a new atomic component `Iframe` (slash menu icon 🔗) that embeds an arbitrary remote URL via `<iframe src=...>` instead of inlining HTML from the embed store. Use case: integrating widget Spaces (e.g. carbon- tokenization-widgets) without having to download and adapt their HTML to the strict create-html-embed conventions. - Shared component def (atomic, fields: src, title, desc, height, wide) - Custom NodeView with live iframe preview, Settings + Reload buttons, auto-resize when the remote page emits postMessage({type:"embedResize"}) - Publisher transformer emits <figure class="html-embed"> reusing the existing iframe/figcaption markup so styling and figure numbering carry over for free - llms.txt renderer surfaces the URL as a Markdown link - 3 publisher tests covering happy path, empty-src drop, and wide flag Co-authored-by: Cursor <cursoragent@cursor.com>0c69852
-
fix(toc): scope the hide-on-mobile rule to the desktop TOC only
The previous rule hid every `.table-of-contents` on narrow viewports, which also matched the React component re-rendered inside `.toc-mobile-sidebar` (the hamburger drawer). Result: the drawer opened to an empty body. Scope the hide to `.content-grid .table-of-contents` so the drawer copy of the same component stays visible. Co-authored-by: Cursor <cursoragent@cursor.com>bf08f85 -
feat(editor): code block button in the bubble toolbar
Code block was reachable only via the slash menu (`/code`). The bubble toolbar already exposes Bold / Italic / Strike / inline Code / Heading / Quote / Link, so a code block button fits naturally alongside Quote (the other block-level toggle in that row). - Uses lucide `Code2` (chevrons with a slash) to keep it visually distinct from the existing inline `Code` icon next to it. - Active state mirrors the slash-menu behaviour: lights up when the caret is inside an existing `codeBlock` node, so a second click toggles back to a regular paragraph. - Placed between Quote and Link to mirror the slash menu's own order (Quote -> Code block -> Divider). Co-authored-by: Cursor <cursoragent@cursor.com>add3959 -
fix(auth): cookie lifetime now matches the JWT exp, not a stale 8h fallback
Users who came back to the editor 9+ hours after logging in were getting a confusing "OAuth token has expired" error from HF when the server tried to push to the dataset on their behalf. The underlying HF JWT is valid for 30 days but our cookie was set to 8 hours, so the cookie died first, the browser stopped sending it, /api/auth/status flipped to unauthenticated, and the background push retried with the old _cachedToken until it finally got 401'd by HF. Fix the cookie lifetime to match the real JWT exp claim: - Decode the JWT (no signature verification - we just want exp, HF re-checks the signature on every API call anyway) and use `exp - now` directly. That's the most authoritative "when does this stop working" signal available. - Fall back to `expires_in` from the OAuth /token response if decoding fails (malformed JWT, future format change, ...). - Floor at 30 days. The previous fallback was 8 hours which guaranteed friction even when HF returned a healthy long-lived …5908601 -
feat(storage): first-class data - no silent failures in the persistence pipeline
The "Saved" label in the top bar used to be a comforting lie: several failure modes (createRepo 403 on a missing manage-repos scope, writeFileSync on a readonly FS, uploadFile mid-debounce network blip) all collapsed into a console.error that the editor never surfaced. A user could type for an hour, see "Saved" the whole time, lose their tab, and discover only after the container rebuilt that nothing had ever made it to the dataset. Make every link in the chain observable, and make the worst signal always win in the UI. Backend ------- - In-memory `StorageStatus` tracker in hf-storage.ts: - `datasetReady` (createRepo succeeded or returned 409) - `lastLocalSaveAt` / `lastCloudPushAt` epoch ms - `pendingPush` (a debounce timer is armed) - `lastError {stage, message, statusCode, at, docName}` where `stage` is one of `dataset-create | local-save | cloud-push` Every write path records success; every error path records the failure. Success on one stage clears the error for …7a42df5 -
feat(storage): private dataset by default + /d/* reverse proxy
The dataset that backs persistence was being created `private: false` on every first write. For org-owned Spaces with strict data policies (notably `HuggingFaceBio/carbon-tokenization`) this could either silently fail (org blocks public dataset creation) or be flat-out wrong (working drafts of an internal research paper shouldn't be sitting on a public dataset). Flip the default to `private: true`. The complication this creates is that published articles' `<img>` tags, og:image and PDF download all pointed at `huggingface.co/datasets/.../resolve/...` URLs that would now 401 for anonymous viewers. Solve that with a small reverse-proxy route on the editor itself. - New `GET /d/:path*` handler streams from `huggingface.co/datasets/${id}/resolve/main/${path}` to the caller, attaching the right HF token along the way: 1. cookie token from the current request (most viewers are signed in on private Spaces anyway) 2. last-known cached user token (any signed-in request warm…c362999
-
feat(editor): show connected collaborators in the top bar
The collab cursors already render inline next to each peer's selection, but if no one's actively editing right now you have no way to see who else is in the room. Surface that in the top bar as an overlapping row of small avatars (24px circles, -6px overlap, max 4 visible + "+N" badge for the tail), placed just before the SyncIndicator. Data source: the same Y.js awareness states that CollaborationC ursorV3 already populates with \`{ name, color, avatarUrl }\` for every client. We listen to awareness "change" events and re-derive the list on each tick, excluding our own clientID so the user doesn't show up to themselves (they already see their own chip on the far right). Implementation notes: - Border color of each avatar mirrors the user's collab color (same hue as their cursor caret in the doc) so it's obvious at a glance which avatar maps to which inline cursor. - Fallback to a colored initial when avatarUrl is missing (Anonymous sessions before HF OAuth resolves). - Sort by…aa09c01 -
Update README.md
990dc4f -
fix(editor): footer credit links to the editor Space too
The editor's own footer (EditorFooter.tsx) still pointed at the legacy Astro-only static template. Mirror the publisher-side fix and link to the editor Space instead, so authors see the same canonical entry point whether they're editing or reading the published page. Co-authored-by: Cursor <cursoragent@cursor.com>2bed071 -
fix(hero): live-commit on every keystroke, not only on blur
EditableText (the inline text field used for the title, subtitle, description and link labels in the hero) used to maintain a local React draft and only push the value to Yjs on blur or Enter. Net effect from the user's POV: the sync indicator stayed on "Saved" while typing, the doc didn't actually update until focus left the field, and on a slow tab-out or a browser crash mid-edit the work was lost. Felt unsaved. Switch to a 300ms debounce model: - Every onChange schedules a commit. If the user keeps typing, the timer resets - so we still get one Yjs update per "pause" instead of one per keypress. - onBlur and Enter flush immediately (no laggy "wait for the debounce" feel when committing the field by leaving it). - Escape cancels the pending commit and reverts the on-screen draft to the last server-confirmed value. Note that anything already committed during typing is still in the doc - the user gets it back with Yjs undo (Ctrl+Z), same as Notion / Linear. - las…82e43b6 -
fix(editor): SyncIndicator now reflects hero/settings/citation edits
The "Saving..." / "Saved" badge in the topbar used to listen only to TipTap's editor.on('update') event. That event fires solely for prosemirror document changes - i.e. when the article BODY is edited. The hero (title/subtitle/authors/links/...), the per-article settings, citations, embeds and the comment store all live in sibling Y.Maps on the same Yjs document. Edits there don't round-trip through TipTap, so they never triggered the indicator. Net effect for users: editing the hero felt like a no-op - no "Saving..." feedback - and the natural conclusion was "this isn't being saved", even though the backend's Hocuspocus onChange handler was happily persisting the full ydoc on every Yjs update. Subscribe one layer lower, on ydoc.on('update', ...). That callback fires for any Y.Map / Y.Array / Y.XmlFragment change in the document, which exactly mirrors what the backend persists. The indicator now flashes "Saving..." for every kind of edit and settles back to "Saved" once the write deb…adb1aa1 -
revert(auth): drop OAuth popup flow, restore plain new-tab login
The popup + window.top.location workaround (bdbc54d, e94e499) turned out to break the very thing it was meant to improve. In practice modern browsers blocked the cookie on iframe reload because of third-party cookie policies, and the fallback that navigated window.top dumped the visitor out of the HF embed altogether - net result: extra moving parts AND a worse UX than the original target="_blank" tab. Revert to the original behavior: the "Sign in" button is a plain <a href="/oauth/authorize" target="_blank"> that opens the auth flow in a new tab. The new tab lands on *.hf.space/editor first-party once auth completes; the visitor uses that tab. Specifically this revert removes: - The pendingStates entry shape upgrade (popup boolean). Map is back to a flat Map<string, number> timestamp store. - The ?popup=1 query in handleOAuthAuthorize. - The popup branch in handleOAuthCallback that served a self-closing landing page. - The renderPopupClosePage() helper. - The inlin…9fba033 -
fix(auth): drop HF iframe on popup close so cookie is first-party
The popup OAuth flow shipped in bdbc54d had a latent third-party cookie problem that only surfaces on modern browsers: - The popup runs as its own top-level browsing context with top-level = *.hf.space. The Set-Cookie at the end of the OAuth round-trip therefore lands as a first-party cookie there. So far so good. - The parent iframe's top-level is huggingface.co (HF embeds every Space inside huggingface.co/spaces/<owner>/<name>). Reloading the iframe issues a request to *.hf.space FROM that top-level - i.e. a third-party request. - Chrome 124+'s ongoing TPC phase-out, Safari ITP, and Firefox strict mode all refuse to attach the *.hf.space cookie to a third-party request now. The reload therefore arrives with no auth cookie and the login page reappears, looking like "the popup did nothing". Fix: when the popup closes, navigate window.top (not window) to *.hf.space/editor. That's a cross-origin top-level navigation, which browsers ALLOW (o…e94e499 -
revert(readme): restore body and title
A previous "Update README.md" commit (3557bec) was made via the HF web UI on the carbon-tokenization Space and ended up propagating to all three Spaces during the last force-push, stripping the README down to just the frontmatter and renaming every Space to "Carbon tokenization". The OAuth frontmatter (hf_oauth + scopes) was untouched so the auth flow is technically still wired, but having a one-line README on Spaces meant for the editor template hides the doc visitors land on. Restoring the full body + original title keeps the editor and editor2 Spaces self-documenting. The carbon-tokenization Space owner can rewrite the title via the web UI in two clicks if they want it back to "Carbon tokenization". Co-authored-by: Cursor <cursoragent@cursor.com>2b3982d -
feat(auth): sign-out button in the editor topbar
Wraps the user chip in the topbar with a dropdown that exposes a "Sign out" action. Matches the visual + interaction pattern of the adjacent "More actions" menu so the two feel like a coherent toolbar element rather than a one-off. Backend (routes/auth.ts + auth.ts): - New POST /api/auth/logout (with a GET alias for ops/debug). handleOAuthLogout clears the hf_access_token cookie, mirroring every attribute the original res.cookie(...) used in handleOAuthCallback - browsers only delete a cookie if the new "expired" Set-Cookie matches path/secure/sameSite, otherwise we end up shipping two cookies and the session sticks. - POST is the canonical method so the call can't be hijacked by a cross-site <img>/<form> request; GET stays available for curl debugging and incident response. Frontend (TopBar.tsx): - Chip becomes a <button> with aria-haspopup="menu" that toggles an account menu. Color/avatar/name unchanged - same visual. - Dedicated state + ref (accountMenu…d10f68e -
feat(auth): OAuth popup window instead of full-tab redirect
When the visitor is inside the HF Space iframe (i.e. browsing huggingface.co/spaces/<owner>/<name> rather than the bare *.hf.space backend URL), clicking "Sign in" used to open a brand new full-screen tab. That kicks the user out of the Space's visual context, drops them on the *.hf.space origin until they manually navigate back, and requires a hard refresh of the iframe to pick up the new cookie. Replace with a centered popup window that closes itself once the auth round-trip completes: Frontend (login page, served by renderLoginPage): - Inline script intercepts the button click ONLY when we're in an iframe (window.self !== window.top). Same-tab direct visits keep the classic href behavior - that path was already optimal. - Appends ?popup=1 to /oauth/authorize so the backend knows to render a self-closing landing page on return. - Polls w.closed every 400ms; on close, reloads the iframe to pick up the freshly-set cookie. - If window.open returns null (popup blocke…bdbc54d -
fix(publisher): footer credit links to the editor Space
The footer credit used to point at the legacy Astro-only static template (tfrere/research-article-template). The editor is now the canonical entry point - it bundles the static renderer and is what people fork to start a new article. Update the credit link accordingly so readers landing on a published article find the right starting point. Snapshot refreshed. Co-authored-by: Cursor <cursoragent@cursor.com>3c813e0 -
Update README.md
Verified signature3557bec -
fix(editor): SyncIndicator was stuck on "Disconnected" forever
The <Editor> creates the HocuspocusProvider lazily and assigns it to providerRef.current AFTER the surrounding components have mounted. <SyncIndicator> was wired with \`useEffect(..., [providerRef])\` and early-returned when providerRef.current was null. Because a ref doesn't trigger re-renders, the effect never re-ran once the provider arrived, so no listeners were attached and the status stayed "disconnected" regardless of the actual WebSocket state. The bug was always latent but became visible now that org-owned private Spaces actually get a working WS connection - the auth fix in the previous commit just exposed it. Replace the dependency-only effect with a small polling bootstrap: - Poll providerRef.current every 100ms until it lands. - Once we have the provider, attach connect/disconnect/synced listeners and seed the initial status from configuration.websocketProvider.status. - Add a 1s reconciliation interval that re-reads websocketProvider.status while attached. Some Ho…50f7a20 -
fix(hf-storage): forward OAuth token on the direct-fetch fallback
Private (org-owned) Spaces back their published artefacts on a private HF dataset. \`pullPublishedAssets\` walks that dataset on boot and after each login to restore the latest published file set. Two issues showed up in the Space logs: 1. When the Hub lib already returned a clean 404 ("file isn't on the repo"), we still re-tried with a direct \`fetch()\`. That second call had no token, so on a private dataset it 401'd - double the noise per missing file (404 + 401 lines), and the 401 looks scarier than it is because it suggests an auth problem where there isn't one. Skip the fallback when the Hub lib gave us a definitive 404. 2. The direct-fetch fallback never carried the bearer token, so it could never actually substitute for the Hub lib on private datasets - even a real network glitch would silently 401. Pass the same \`accessToken\` we use for the Hub call. Anonymous fallback is still allowed (no header) when there's no token, matching the old beh…0add695 -
chore(ws): verbose auth logs to triage Disconnected reports
Add structured logging on both ends of the WS handshake so a "Disconnected" label in the editor top bar can be pinpointed without guessing: - Upgrade handler logs the cookie header size and whether extractToken found our session cookie. This catches the case where the browser doesn't ship cookies on the WS upgrade (some HF Space gating + private/org setups strip them on upgrade even when plain HTTP requests send them fine). - onAuthenticate logs which source supplied the token (the client subprotocol or the server-side cookie fallback), the resolved user, the configured Space owner, and any accessIssue surfaced by resolveUser. Errors thrown to Hocuspocus now also include the username + accessIssue so the reason is visible in the rejected-connection log line, not just a generic "Unauthorized". Logs intentionally avoid printing the token itself - they only expose its length and origin, matching the existing privacy posture of the auth pipeline. Co-authored-by: Curs…038fa8d -
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