@pixerate/editor-react 0.9.5 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -11,7 +11,11 @@ export { ImageEditor, ImageEditorProps, UseImageEditorReturn, useImageEditor } f
11
11
  import '@pixerate/editor/image-editor';
12
12
 
13
13
  interface UsePromptEditorProps {
14
- /** The initial or controlled content of the editor as plain text */
14
+ /**
15
+ * The editor content as plain text. Changes from the parent (e.g. clearing
16
+ * after submit) are applied to the editor; echoes of the editor's own
17
+ * updates via `onContentChange` are ignored.
18
+ */
15
19
  content: string;
16
20
  /** Callback fired when the editor content changes (returns plain text) */
17
21
  onContentChange: (text: string) => void;
@@ -43,7 +47,10 @@ interface UsePromptEditorProps {
43
47
  clearSlashCommand?: () => void;
44
48
  /** Whether a slash command menu is currently active */
45
49
  slashCommandActive?: boolean;
46
- /** Callback fired when certain milestones are reached (e.g., used_variable, used_magic) */
50
+ /**
51
+ * Callback fired the first time each milestone is reached while the editor
52
+ * is mounted: `used_variable` ({variable}) and `used_magic` (__instruction__).
53
+ */
47
54
  onMilestone?: (milestone: string) => void;
48
55
  /** DOM element ID of the container, used for positioning popovers */
49
56
  containerId?: string;
package/dist/index.d.ts CHANGED
@@ -11,7 +11,11 @@ export { ImageEditor, ImageEditorProps, UseImageEditorReturn, useImageEditor } f
11
11
  import '@pixerate/editor/image-editor';
12
12
 
13
13
  interface UsePromptEditorProps {
14
- /** The initial or controlled content of the editor as plain text */
14
+ /**
15
+ * The editor content as plain text. Changes from the parent (e.g. clearing
16
+ * after submit) are applied to the editor; echoes of the editor's own
17
+ * updates via `onContentChange` are ignored.
18
+ */
15
19
  content: string;
16
20
  /** Callback fired when the editor content changes (returns plain text) */
17
21
  onContentChange: (text: string) => void;
@@ -43,7 +47,10 @@ interface UsePromptEditorProps {
43
47
  clearSlashCommand?: () => void;
44
48
  /** Whether a slash command menu is currently active */
45
49
  slashCommandActive?: boolean;
46
- /** Callback fired when certain milestones are reached (e.g., used_variable, used_magic) */
50
+ /**
51
+ * Callback fired the first time each milestone is reached while the editor
52
+ * is mounted: `used_variable` ({variable}) and `used_magic` (__instruction__).
53
+ */
47
54
  onMilestone?: (milestone: string) => void;
48
55
  /** DOM element ID of the container, used for positioning popovers */
49
56
  containerId?: string;
package/dist/index.js CHANGED
@@ -8,18 +8,22 @@ import { useMemo, useEffect, useRef } from "react";
8
8
  import { useEditor } from "@tiptap/react";
9
9
  import StarterKit from "@tiptap/starter-kit";
10
10
  import Placeholder from "@tiptap/extension-placeholder";
11
- import { Slice, Fragment, Node as ProsemirrorNode } from "@tiptap/pm/model";
11
+ import { Slice } from "@tiptap/pm/model";
12
12
  import {
13
13
  GradientText,
14
14
  TemplateSuggestions,
15
15
  LoadingNode,
16
16
  plainTextToTipTapHtml,
17
- getEditorText
17
+ plainTextToSlice,
18
+ getEditorText,
19
+ tokenizePrompt
18
20
  } from "@pixerate/editor";
19
21
  var DEFAULT_TEMPLATES = [];
20
22
  var DEFAULT_COLOR_MAP = /* @__PURE__ */ new Map([
21
23
  ["default", { from: "#f43f5e", to: "#8b5cf6" }]
22
24
  ]);
25
+ var DEFAULT_CLASS_NAME = "w-full border border-input rounded-md p-3 font-mono text-sm leading-relaxed focus:outline-none focus:ring-2 focus:ring-primary whitespace-pre-wrap break-words";
26
+ var normalizeNewlines = (text) => text.replace(/\r\n?/g, "\n");
23
27
  function usePromptEditor({
24
28
  content,
25
29
  onContentChange,
@@ -60,6 +64,7 @@ function usePromptEditor({
60
64
  const templateKey = useMemo(() => {
61
65
  return (templates || []).map((t) => `${t.id || t.name}:${t.latestVersion || ""}`).sort().join(",");
62
66
  }, [templates]);
67
+ const colorMapKey = userColorMap ? JSON.stringify(Array.from(userColorMap.entries())) : "";
63
68
  const resolvedColorMap = useMemo(() => {
64
69
  if (userColorMap) return userColorMap;
65
70
  const map = /* @__PURE__ */ new Map();
@@ -67,7 +72,9 @@ function usePromptEditor({
67
72
  map.set(t.name, { from: "#f43f5e", to: "#8b5cf6" });
68
73
  });
69
74
  return map.size > 0 ? map : DEFAULT_COLOR_MAP;
70
- }, [userColorMap, templateKey]);
75
+ }, [colorMapKey, templateKey]);
76
+ const jsonVariablesKey = (jsonVariables || []).join("\0");
77
+ const reachedMilestonesRef = useRef(/* @__PURE__ */ new Set());
71
78
  const StarterKitExt = StarterKit?.configure ? StarterKit : StarterKit?.default?.configure ? StarterKit?.default : StarterKit;
72
79
  const PlaceholderExt = Placeholder?.configure ? Placeholder : Placeholder?.default?.configure ? Placeholder?.default : Placeholder;
73
80
  const editor = useEditor(
@@ -97,11 +104,13 @@ function usePromptEditor({
97
104
  const plainText = getEditorText(ed);
98
105
  onContentChangeRef.current?.(plainText);
99
106
  if (onMilestoneRef.current) {
100
- if (plainText.match(/(?<!\{)\{([^{}]+)\}(?!\})/g)) {
101
- onMilestoneRef.current("used_variable");
102
- }
103
- if (plainText.match(/__([^_]+)__/g)) {
104
- onMilestoneRef.current("used_magic");
107
+ const reached = reachedMilestonesRef.current;
108
+ for (const token of tokenizePrompt(plainText)) {
109
+ const milestone = token.type === "variable" ? "used_variable" : token.type === "instruction" ? "used_magic" : null;
110
+ if (milestone && !reached.has(milestone)) {
111
+ reached.add(milestone);
112
+ onMilestoneRef.current(milestone);
113
+ }
105
114
  }
106
115
  }
107
116
  const { state, view } = ed;
@@ -139,7 +148,7 @@ function usePromptEditor({
139
148
  },
140
149
  editorProps: {
141
150
  attributes: {
142
- class: className || "w-full border border-input rounded-md p-3 font-mono text-sm leading-relaxed focus:outline-none focus:ring-2 focus:ring-primary whitespace-pre-wrap break-words",
151
+ class: className || DEFAULT_CLASS_NAME,
143
152
  role: "textbox",
144
153
  "aria-label": "Prompt editor"
145
154
  },
@@ -171,51 +180,61 @@ function usePromptEditor({
171
180
  event.preventDefault();
172
181
  const schema = view.state.schema;
173
182
  if (!schema) return false;
174
- const blocks = text.replace(/\r\n?/g, "\n").split("\n");
175
- const nodes = [];
176
- blocks.forEach((line) => {
177
- const nodeJson = { type: "paragraph" };
178
- if (line.length > 0) {
179
- nodeJson.content = [{ type: "text", text: line }];
180
- }
181
- try {
182
- const node = ProsemirrorNode.fromJSON(schema, nodeJson);
183
- nodes.push(node);
184
- } catch {
185
- }
186
- });
187
- const parsedSlice = Slice.maxOpen(Fragment.fromArray(nodes));
188
- const tr = view.state.tr.replaceSelection(parsedSlice);
189
- view.dispatch(tr);
183
+ view.dispatch(
184
+ view.state.tr.replaceSelection(plainTextToSlice(text, schema))
185
+ );
190
186
  return true;
191
187
  },
192
188
  clipboardTextParser: (text, context, _plain, view) => {
193
189
  const schema = context?.doc?.type?.schema || context?.schema || view?.state?.schema;
194
190
  if (!schema) return Slice.empty;
195
- const blocks = text.replace(/\r\n?/g, "\n").split("\n");
196
- const nodes = [];
197
- blocks.forEach((line) => {
198
- const nodeJson = { type: "paragraph" };
199
- if (line.length > 0) {
200
- nodeJson.content = [{ type: "text", text: line }];
201
- }
202
- try {
203
- const node = ProsemirrorNode.fromJSON(schema, nodeJson);
204
- nodes.push(node);
205
- } catch {
206
- }
207
- });
208
- return Slice.maxOpen(Fragment.fromArray(nodes));
191
+ return plainTextToSlice(text, schema);
209
192
  }
210
193
  }
211
194
  },
212
- [resolvedColorMap, templateKey, isEditing, placeholder, speed]
195
+ // Only options baked into extensions recreate the editor; editable state,
196
+ // placeholder, class names and content are applied to the live instance.
197
+ [resolvedColorMap, templateKey, speed, jsonVariablesKey]
213
198
  );
214
199
  useEffect(() => {
215
200
  if (editor && !editor.isDestroyed) {
216
201
  editor.setEditable(isEditing);
217
202
  }
218
203
  }, [editor, isEditing]);
204
+ useEffect(() => {
205
+ if (!editor || editor.isDestroyed) return;
206
+ if (normalizeNewlines(content) === getEditorText(editor)) return;
207
+ const { from, to } = editor.state.selection;
208
+ editor.commands.setContent(plainTextToTipTapHtml(content), false);
209
+ const max = editor.state.doc.content.size;
210
+ editor.commands.setTextSelection({
211
+ from: Math.min(from, max),
212
+ to: Math.min(to, max)
213
+ });
214
+ }, [editor, content]);
215
+ useEffect(() => {
216
+ if (!editor || editor.isDestroyed) return;
217
+ editor.setOptions({
218
+ editorProps: {
219
+ ...editor.options.editorProps,
220
+ attributes: {
221
+ ...editor.options.editorProps.attributes,
222
+ class: className || DEFAULT_CLASS_NAME
223
+ }
224
+ }
225
+ });
226
+ }, [editor, className]);
227
+ useEffect(() => {
228
+ if (!editor || editor.isDestroyed) return;
229
+ const placeholderExt = editor.extensionManager.extensions.find(
230
+ (ext) => ext.name === "placeholder"
231
+ );
232
+ if (!placeholderExt || placeholderExt.options.placeholder === placeholder) {
233
+ return;
234
+ }
235
+ placeholderExt.options.placeholder = placeholder;
236
+ editor.view.dispatch(editor.state.tr);
237
+ }, [editor, placeholder]);
219
238
  return editor;
220
239
  }
221
240
 
@@ -518,7 +537,7 @@ var BubbleMenu = ({
518
537
  // src/components/TemplateRenderer.tsx
519
538
  import { useMemo as useMemo3 } from "react";
520
539
  import {
521
- tokenizePrompt,
540
+ tokenizePrompt as tokenizePrompt2,
522
541
  resolveTemplates
523
542
  } from "@pixerate/editor";
524
543
  import { jsx as jsx2 } from "react/jsx-runtime";
@@ -536,7 +555,7 @@ var TemplateRenderer = ({
536
555
  return resolveTemplates(content, templates);
537
556
  }, [content, templates, resolve]);
538
557
  const tokens = useMemo3(() => {
539
- return tokenizePrompt(displayText);
558
+ return tokenizePrompt2(displayText);
540
559
  }, [displayText]);
541
560
  return /* @__PURE__ */ jsx2("div", { className, children: tokens.map((token, index) => {
542
561
  if (token.type === "template") {