UNPKG

@atlaskit/editor-plugin-collab-edit

Version:

Collab Edit plugin for @atlaskit/editor-core

159 lines (150 loc) 8.11 kB
import _defineProperty from "@babel/runtime/helpers/defineProperty"; function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } import { Decoration } from '@atlaskit/editor-prosemirror/view'; // Kept out of `plugin-state.ts` so all the agent-shimmer plumbing lives together and is easy to // remove if the approach changes. `plugin-state` just reduces the active ranges and asks this module // to build the decorations. /** Default time the skeleton shimmer stays on the agent-authored content (ms). */ export var AGENT_SHIMMER_DEFAULT_DURATION_MS = 3000; /** * Default time the purple "just edited" highlight stays after the skeleton clears (ms); `0` disables * the highlight phase so the shimmer just reveals the content. */ export var AGENT_EDIT_HIGHLIGHT_DEFAULT_DURATION_MS = 2000; // Skeleton-loader bar over the agent-authored range, plus a Rovo AI telepointer at the end. export var AGENT_SHIMMER_CLASS = 'collab-agent-shimmer'; // Purple "just edited" highlight shown over the range after the skeleton clears — same style as the // editor AI "improve writing" in-editor highlight (subtle purple background + dashed purple underline). export var AGENT_EDIT_HIGHLIGHT_CLASS = 'collab-agent-edit-highlight'; export var ROVO_AGENT_TELEPOINTER_CLASS = 'ai-in-editor-telepointer'; export var ROVO_AGENT_TELEPOINTER_LABEL_CLASS = 'ai-in-editor-telepointer-label'; export var ADD_AGENT_SHIMMER_META = 'addAgentShimmer'; // register the shimmer decorations export var HIGHLIGHT_AGENT_SHIMMER_META = 'highlightAgentShimmer'; // skeleton → purple highlight phase export var REMOVE_AGENT_SHIMMER_META = 'removeAgentShimmer'; // remove them once the shimmer ends // A shimmer runs in two phases: the `skeleton` loader, then (optionally) the purple `highlight` over // the revealed content, before removal. // A range an agent step wrote; the shimmer decorations are drawn over `from`..`to` and kept until // removal (so positions can be re-mapped). Pure data only. `phase` selects skeleton vs purple // highlight. `telepointerLabel` is the label for the trailing agent telepointer (shown through both // phases); when absent, no telepointer is shown. `highlightDurationMs` is the purple-highlight // lifetime, used to size its ease in/out animation so it matches the removal timer. // Rovo AI in-editor telepointer/cursor shown at the end of an agent-authored range (same DOM/style // pattern as editor-plugin-ai's in-editor direct-streaming telepointer). var createRovoAgentTelepointer = function createRovoAgentTelepointer(label) { var element = document.createElement('span'); element.setAttribute('data-testid', 'ai-in-editor-telepointer-widget'); element.className = ROVO_AGENT_TELEPOINTER_CLASS; var labelElement = document.createElement('span'); labelElement.setAttribute('data-testid', 'ai-in-editor-telepointer-widget-label'); labelElement.className = ROVO_AGENT_TELEPOINTER_LABEL_CLASS; labelElement.append(label); element.appendChild(labelElement); return element; }; /** * Pure reducer for the active shimmer ranges from a transaction's changes. Maps existing ranges * forward as the doc changes, replaces them wholesale when a new agent batch lands (a new batch * supersedes any still-in-flight shimmer), and drops a range when its removal timer fires. Returns a * fresh array (never mutates in place) plus whether anything changed. */ export var reduceAgentShimmers = function reduceAgentShimmers(current, tr, added, removedShimmerId, highlightShimmerId) { var next = current; var changed = false; // Ranges added in THIS transaction are already in post-change coords, so map the pre-existing // ones BEFORE replacing with any new batch. if (tr.docChanged && next.length) { next = next.map(function (shimmer) { return _objectSpread(_objectSpread({}, shimmer), {}, { from: tr.mapping.map(shimmer.from, -1), to: tr.mapping.map(shimmer.to, 1) }); }); changed = true; } if (added !== null && added !== void 0 && added.length) { next = added.map(function (shimmer) { return _objectSpread({}, shimmer); }); changed = true; } // Transition a shimmer from the skeleton phase to the purple highlight phase (skeleton timer fired). // Guard on an actual match so a timer firing after the shimmer was already removed (superseded by a // new batch, or cleared) doesn't rebuild the array and trigger a pointless decoration rebuild. if (highlightShimmerId && next.some(function (shimmer) { return shimmer.shimmerId === highlightShimmerId; })) { next = next.map(function (shimmer) { return shimmer.shimmerId === highlightShimmerId ? _objectSpread(_objectSpread({}, shimmer), {}, { phase: 'highlight' }) : shimmer; }); changed = true; } if (removedShimmerId) { next = next.filter(function (shimmer) { return shimmer.shimmerId !== removedShimmerId; }); changed = true; } return { changed: changed, next: next }; }; /** * Builds the inline decorations (skeleton bar or purple highlight, per phase) plus the trailing * telepointer for the active shimmer ranges. `getValidPos` clamps a raw position to a valid * decoration position (owned by `plugin-state`). One bad range is isolated via `onError` so it can't * kill the others. */ export var buildAgentShimmerDecorations = function buildAgentShimmerDecorations(tr, shimmers, getValidPos, onError) { var decorations = []; shimmers.forEach(function (_ref) { var shimmerId = _ref.shimmerId, from = _ref.from, to = _ref.to, telepointerLabel = _ref.telepointerLabel, phase = _ref.phase, highlightDurationMs = _ref.highlightDurationMs; try { // `getValidPos` already clamps to the last valid position, so only the lower bound needs // guarding here (a raw `from < 1` would throw in `doc.resolve`). var validFrom = getValidPos(tr, Math.max(from, 1)); var validTo = getValidPos(tr, to); if (validTo <= validFrom) { return; } // Inline decoration over the whole range: the grey skeleton loader, or (once revealed) the // purple "just edited" highlight. The highlight eases in and out over its lifetime via a CSS // animation whose duration is set inline so it matches the removal timer. var isHighlight = phase === 'highlight'; var inlineAttrs = { class: isHighlight ? AGENT_EDIT_HIGHLIGHT_CLASS : AGENT_SHIMMER_CLASS }; if (isHighlight && highlightDurationMs > 0) { inlineAttrs.style = "animation-duration: ".concat(highlightDurationMs, "ms"); } decorations.push(Decoration.inline(validFrom, validTo, inlineAttrs, { isAgentShimmer: true, shimmerId: shimmerId })); // Rovo AI telepointer/cursor (labelled with the agent's type) at the end of the range, shown // through BOTH the skeleton and purple-highlight phases so the agent's cursor stays put until // the edit is fully revealed. if (telepointerLabel) { decorations.push(Decoration.widget(validTo, createRovoAgentTelepointer(telepointerLabel), { isAgentShimmer: true, shimmerId: shimmerId, class: ROVO_AGENT_TELEPOINTER_CLASS, key: "agent-telepointer-".concat(shimmerId), side: 1 })); } } catch (err) { // One bad range must not kill the others. onError(err); } }); return decorations; };