@pilotiq/tiptap 3.20.0 → 4.0.0
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/CHANGELOG.md +37 -0
- package/dist/extensions/{AiInlineDiffExtension.d.ts → InlineDiffExtension.d.ts} +23 -23
- package/dist/extensions/{AiInlineDiffExtension.js → InlineDiffExtension.js} +33 -33
- package/dist/extensions/{AiSuggestionExtension.d.ts → SuggestionChipExtension.d.ts} +29 -29
- package/dist/extensions/{AiSuggestionExtension.js → SuggestionChipExtension.js} +52 -52
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -4
- package/dist/react/CollabTextRenderer.js +16 -16
- package/dist/react/MarkdownEditor.js +17 -17
- package/dist/react/{AiSuggestionBanner.d.ts → SuggestionBanner.d.ts} +6 -6
- package/dist/react/{AiSuggestionBanner.js → SuggestionBanner.js} +5 -5
- package/dist/react/TiptapEditor.js +17 -17
- package/dist/react/{useAiInlineDiff.d.ts → useInlineDiff.d.ts} +13 -13
- package/dist/react/{useAiInlineDiff.js → useInlineDiff.js} +36 -19
- package/dist/react/{useAiSuggestionBridge.d.ts → useSuggestionBridge.d.ts} +7 -7
- package/dist/react/{useAiSuggestionBridge.js → useSuggestionBridge.js} +10 -10
- package/dist/surgicalOps.d.ts +15 -2
- package/dist/surgicalOps.js +50 -3
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Extension } from '@tiptap/core';
|
|
2
2
|
import { Plugin, PluginKey } from '@tiptap/pm/state';
|
|
3
3
|
import { Decoration, DecorationSet } from '@tiptap/pm/view';
|
|
4
|
-
export const
|
|
4
|
+
export const suggestionChipPluginKey = new PluginKey('pilotiqSuggestionChip');
|
|
5
5
|
/**
|
|
6
6
|
* Append or replace by id. Pure — exported for tests and so the same dedupe
|
|
7
7
|
* shape can drive consumer-side mirror state.
|
|
@@ -55,7 +55,7 @@ export function sortForApproveAll(suggestions) {
|
|
|
55
55
|
*
|
|
56
56
|
* Usage:
|
|
57
57
|
* ```ts
|
|
58
|
-
* editor.commands.
|
|
58
|
+
* editor.commands.addSuggestion({
|
|
59
59
|
* id: 'seo-1',
|
|
60
60
|
* from: 12,
|
|
61
61
|
* to: 18,
|
|
@@ -63,30 +63,30 @@ export function sortForApproveAll(suggestions) {
|
|
|
63
63
|
* source: { agentLabel: 'SEO' },
|
|
64
64
|
* })
|
|
65
65
|
* // …user clicks ✓ on the chip, or:
|
|
66
|
-
* editor.commands.
|
|
66
|
+
* editor.commands.approveSuggestion('seo-1')
|
|
67
67
|
* ```
|
|
68
68
|
*
|
|
69
69
|
* Mounted by default inside `TiptapEditor`; consumer code reaches it through
|
|
70
70
|
* the editor's command surface.
|
|
71
71
|
*/
|
|
72
|
-
export const
|
|
73
|
-
name: '
|
|
72
|
+
export const SuggestionChipExtension = Extension.create({
|
|
73
|
+
name: 'pilotiqSuggestionChip',
|
|
74
74
|
addOptions() {
|
|
75
75
|
return {
|
|
76
|
-
classPrefix: 'pilotiq-
|
|
76
|
+
classPrefix: 'pilotiq-suggestion',
|
|
77
77
|
};
|
|
78
78
|
},
|
|
79
79
|
onCreate() {
|
|
80
80
|
// Inject minimal default styles for the chip + strikethrough on first
|
|
81
81
|
// mount so consumers see the visualization without wiring CSS. Idempotent
|
|
82
|
-
// via the `data-pilotiq-
|
|
82
|
+
// via the `data-pilotiq-suggestion-styles` sentinel; consumers who
|
|
83
83
|
// want full control just add their own `<style>` with the same class
|
|
84
84
|
// names (last wins — the cascade picks user overrides over our defaults
|
|
85
85
|
// since the user stylesheet appears AFTER our injected one in `<head>`
|
|
86
86
|
// when imported via Vite/Webpack, OR via higher specificity).
|
|
87
87
|
if (typeof document === 'undefined')
|
|
88
88
|
return;
|
|
89
|
-
const SENTINEL = 'data-pilotiq-
|
|
89
|
+
const SENTINEL = 'data-pilotiq-suggestion-styles';
|
|
90
90
|
if (document.head.querySelector(`style[${SENTINEL}]`))
|
|
91
91
|
return;
|
|
92
92
|
const prefix = this.options.classPrefix;
|
|
@@ -134,10 +134,10 @@ export const AiSuggestionExtension = Extension.create({
|
|
|
134
134
|
/* Banner — bottom-of-editor strip for whole-field suggestions on rich
|
|
135
135
|
surfaces (markdown / richtext). Sibling to the chip styles above;
|
|
136
136
|
lives here so both ship via the same extension-mount sentinel.
|
|
137
|
-
Class names live under \`pilotiq-
|
|
137
|
+
Class names live under \`pilotiq-suggestion-banner-*\` (not \`-suggestion-\`)
|
|
138
138
|
since the banner is a host-mounted React component, not a PM
|
|
139
139
|
decoration. */
|
|
140
|
-
.pilotiq-
|
|
140
|
+
.pilotiq-suggestion-banner {
|
|
141
141
|
display: flex;
|
|
142
142
|
align-items: center;
|
|
143
143
|
gap: 0.5rem;
|
|
@@ -150,15 +150,15 @@ export const AiSuggestionExtension = Extension.create({
|
|
|
150
150
|
font-size: 0.875rem;
|
|
151
151
|
line-height: 1.4;
|
|
152
152
|
}
|
|
153
|
-
.pilotiq-
|
|
154
|
-
.pilotiq-
|
|
155
|
-
.pilotiq-
|
|
153
|
+
.pilotiq-suggestion-banner-icon { flex: 0 0 auto; }
|
|
154
|
+
.pilotiq-suggestion-banner-label { flex: 1 1 auto; }
|
|
155
|
+
.pilotiq-suggestion-banner-actions {
|
|
156
156
|
display: inline-flex;
|
|
157
157
|
gap: 0.375rem;
|
|
158
158
|
flex: 0 0 auto;
|
|
159
159
|
}
|
|
160
|
-
.pilotiq-
|
|
161
|
-
.pilotiq-
|
|
160
|
+
.pilotiq-suggestion-banner-reject,
|
|
161
|
+
.pilotiq-suggestion-banner-accept {
|
|
162
162
|
appearance: none;
|
|
163
163
|
cursor: pointer;
|
|
164
164
|
font-size: 0.8125rem;
|
|
@@ -168,50 +168,50 @@ export const AiSuggestionExtension = Extension.create({
|
|
|
168
168
|
border-radius: 0.25rem;
|
|
169
169
|
border: 1px solid transparent;
|
|
170
170
|
}
|
|
171
|
-
.pilotiq-
|
|
171
|
+
.pilotiq-suggestion-banner-reject {
|
|
172
172
|
background-color: transparent;
|
|
173
173
|
color: rgb(120, 53, 15);
|
|
174
174
|
border-color: rgba(180, 83, 9, 0.4);
|
|
175
175
|
}
|
|
176
|
-
.pilotiq-
|
|
176
|
+
.pilotiq-suggestion-banner-reject:hover {
|
|
177
177
|
background-color: rgba(254, 215, 170, 0.4);
|
|
178
178
|
}
|
|
179
|
-
.pilotiq-
|
|
179
|
+
.pilotiq-suggestion-banner-accept {
|
|
180
180
|
background-color: rgb(22, 101, 52);
|
|
181
181
|
color: white;
|
|
182
182
|
}
|
|
183
|
-
.pilotiq-
|
|
183
|
+
.pilotiq-suggestion-banner-accept:hover { background-color: rgb(21, 128, 61); }
|
|
184
184
|
`;
|
|
185
185
|
document.head.appendChild(style);
|
|
186
186
|
},
|
|
187
187
|
addCommands() {
|
|
188
188
|
return {
|
|
189
|
-
|
|
190
|
-
const current =
|
|
189
|
+
addSuggestion: (suggestion) => ({ tr, state, dispatch }) => {
|
|
190
|
+
const current = suggestionChipPluginKey.getState(state)?.suggestions ?? [];
|
|
191
191
|
const next = upsertSuggestion(current, suggestion);
|
|
192
192
|
if (dispatch) {
|
|
193
|
-
tr.setMeta(
|
|
193
|
+
tr.setMeta(suggestionChipPluginKey, { type: 'set', next });
|
|
194
194
|
dispatch(tr);
|
|
195
195
|
}
|
|
196
196
|
return true;
|
|
197
197
|
},
|
|
198
|
-
|
|
199
|
-
const current =
|
|
198
|
+
addSuggestions: (suggestions) => ({ tr, state, dispatch }) => {
|
|
199
|
+
const current = suggestionChipPluginKey.getState(state)?.suggestions ?? [];
|
|
200
200
|
const next = upsertSuggestions(current, suggestions);
|
|
201
201
|
if (dispatch) {
|
|
202
|
-
tr.setMeta(
|
|
202
|
+
tr.setMeta(suggestionChipPluginKey, { type: 'set', next });
|
|
203
203
|
dispatch(tr);
|
|
204
204
|
}
|
|
205
205
|
return true;
|
|
206
206
|
},
|
|
207
|
-
|
|
208
|
-
const current =
|
|
207
|
+
approveSuggestion: (id) => ({ tr, state, dispatch }) => {
|
|
208
|
+
const current = suggestionChipPluginKey.getState(state)?.suggestions ?? [];
|
|
209
209
|
const target = current.find(s => s.id === id);
|
|
210
210
|
if (!target)
|
|
211
211
|
return false;
|
|
212
212
|
if (dispatch) {
|
|
213
213
|
applyApprove(tr, state, target);
|
|
214
|
-
tr.setMeta(
|
|
214
|
+
tr.setMeta(suggestionChipPluginKey, {
|
|
215
215
|
type: 'set',
|
|
216
216
|
next: removeSuggestion(current, id),
|
|
217
217
|
});
|
|
@@ -219,13 +219,13 @@ export const AiSuggestionExtension = Extension.create({
|
|
|
219
219
|
}
|
|
220
220
|
return true;
|
|
221
221
|
},
|
|
222
|
-
|
|
223
|
-
const current =
|
|
222
|
+
rejectSuggestion: (id) => ({ tr, state, dispatch }) => {
|
|
223
|
+
const current = suggestionChipPluginKey.getState(state)?.suggestions ?? [];
|
|
224
224
|
const target = current.find(s => s.id === id);
|
|
225
225
|
if (!target)
|
|
226
226
|
return false;
|
|
227
227
|
if (dispatch) {
|
|
228
|
-
tr.setMeta(
|
|
228
|
+
tr.setMeta(suggestionChipPluginKey, {
|
|
229
229
|
type: 'set',
|
|
230
230
|
next: removeSuggestion(current, id),
|
|
231
231
|
});
|
|
@@ -233,14 +233,14 @@ export const AiSuggestionExtension = Extension.create({
|
|
|
233
233
|
}
|
|
234
234
|
return true;
|
|
235
235
|
},
|
|
236
|
-
|
|
237
|
-
const current =
|
|
236
|
+
approveAllSuggestions: () => ({ tr, state, dispatch }) => {
|
|
237
|
+
const current = suggestionChipPluginKey.getState(state)?.suggestions ?? [];
|
|
238
238
|
if (current.length === 0)
|
|
239
239
|
return false;
|
|
240
240
|
if (dispatch) {
|
|
241
241
|
for (const s of sortForApproveAll(current))
|
|
242
242
|
applyApprove(tr, state, s);
|
|
243
|
-
tr.setMeta(
|
|
243
|
+
tr.setMeta(suggestionChipPluginKey, {
|
|
244
244
|
type: 'set',
|
|
245
245
|
next: [],
|
|
246
246
|
});
|
|
@@ -248,12 +248,12 @@ export const AiSuggestionExtension = Extension.create({
|
|
|
248
248
|
}
|
|
249
249
|
return true;
|
|
250
250
|
},
|
|
251
|
-
|
|
252
|
-
const current =
|
|
251
|
+
rejectAllSuggestions: () => ({ tr, state, dispatch }) => {
|
|
252
|
+
const current = suggestionChipPluginKey.getState(state)?.suggestions ?? [];
|
|
253
253
|
if (current.length === 0)
|
|
254
254
|
return false;
|
|
255
255
|
if (dispatch) {
|
|
256
|
-
tr.setMeta(
|
|
256
|
+
tr.setMeta(suggestionChipPluginKey, {
|
|
257
257
|
type: 'set',
|
|
258
258
|
next: [],
|
|
259
259
|
});
|
|
@@ -261,12 +261,12 @@ export const AiSuggestionExtension = Extension.create({
|
|
|
261
261
|
}
|
|
262
262
|
return true;
|
|
263
263
|
},
|
|
264
|
-
|
|
265
|
-
const current =
|
|
264
|
+
clearSuggestions: () => ({ tr, state, dispatch }) => {
|
|
265
|
+
const current = suggestionChipPluginKey.getState(state)?.suggestions ?? [];
|
|
266
266
|
if (current.length === 0)
|
|
267
267
|
return false;
|
|
268
268
|
if (dispatch) {
|
|
269
|
-
tr.setMeta(
|
|
269
|
+
tr.setMeta(suggestionChipPluginKey, {
|
|
270
270
|
type: 'set',
|
|
271
271
|
next: [],
|
|
272
272
|
});
|
|
@@ -280,11 +280,11 @@ export const AiSuggestionExtension = Extension.create({
|
|
|
280
280
|
const ext = this;
|
|
281
281
|
return [
|
|
282
282
|
new Plugin({
|
|
283
|
-
key:
|
|
283
|
+
key: suggestionChipPluginKey,
|
|
284
284
|
state: {
|
|
285
285
|
init: () => ({ suggestions: [] }),
|
|
286
286
|
apply(tr, prev) {
|
|
287
|
-
const meta = tr.getMeta(
|
|
287
|
+
const meta = tr.getMeta(suggestionChipPluginKey);
|
|
288
288
|
const base = meta?.type === 'set' ? meta.next : prev.suggestions;
|
|
289
289
|
if (!tr.docChanged)
|
|
290
290
|
return { suggestions: base };
|
|
@@ -295,17 +295,17 @@ export const AiSuggestionExtension = Extension.create({
|
|
|
295
295
|
},
|
|
296
296
|
props: {
|
|
297
297
|
decorations(state) {
|
|
298
|
-
const ps =
|
|
298
|
+
const ps = suggestionChipPluginKey.getState(state);
|
|
299
299
|
if (!ps || ps.suggestions.length === 0)
|
|
300
300
|
return DecorationSet.empty;
|
|
301
301
|
return buildDecorations(state, ps.suggestions, ext.options.classPrefix, ext.editor);
|
|
302
302
|
},
|
|
303
303
|
},
|
|
304
304
|
view(view) {
|
|
305
|
-
let last =
|
|
305
|
+
let last = suggestionChipPluginKey.getState(view.state)?.suggestions;
|
|
306
306
|
return {
|
|
307
307
|
update(updated) {
|
|
308
|
-
const next =
|
|
308
|
+
const next = suggestionChipPluginKey.getState(updated.state)?.suggestions;
|
|
309
309
|
if (next === last)
|
|
310
310
|
return;
|
|
311
311
|
last = next;
|
|
@@ -344,13 +344,13 @@ function buildDecorations(state, suggestions, prefix, editor) {
|
|
|
344
344
|
if (from < to) {
|
|
345
345
|
decos.push(Decoration.inline(from, to, {
|
|
346
346
|
class: `${prefix}-original`,
|
|
347
|
-
'data-pilotiq-
|
|
347
|
+
'data-pilotiq-suggestion-id': s.id,
|
|
348
348
|
}));
|
|
349
349
|
}
|
|
350
350
|
decos.push(Decoration.widget(to, () => buildChip(s, prefix, editor), {
|
|
351
351
|
side: 1,
|
|
352
352
|
ignoreSelection: true,
|
|
353
|
-
key: `pilotiq-
|
|
353
|
+
key: `pilotiq-suggestion:${s.id}`,
|
|
354
354
|
}));
|
|
355
355
|
}
|
|
356
356
|
return DecorationSet.create(state.doc, decos);
|
|
@@ -368,7 +368,7 @@ export function clampPos(pos, max) {
|
|
|
368
368
|
function buildChip(s, prefix, editor) {
|
|
369
369
|
const root = document.createElement('span');
|
|
370
370
|
root.className = `${prefix}-chip`;
|
|
371
|
-
root.setAttribute('data-pilotiq-
|
|
371
|
+
root.setAttribute('data-pilotiq-suggestion-id', s.id);
|
|
372
372
|
root.contentEditable = 'false';
|
|
373
373
|
if (s.replacement.length > 0) {
|
|
374
374
|
const insert = document.createElement('span');
|
|
@@ -377,16 +377,16 @@ function buildChip(s, prefix, editor) {
|
|
|
377
377
|
root.appendChild(insert);
|
|
378
378
|
}
|
|
379
379
|
if (s.source?.agentLabel) {
|
|
380
|
-
root.setAttribute('data-pilotiq-
|
|
380
|
+
root.setAttribute('data-pilotiq-suggestion-source', s.source.agentLabel);
|
|
381
381
|
}
|
|
382
382
|
if (s.source?.agentSlug) {
|
|
383
|
-
root.setAttribute('data-pilotiq-
|
|
383
|
+
root.setAttribute('data-pilotiq-suggestion-source-slug', s.source.agentSlug);
|
|
384
384
|
}
|
|
385
385
|
root.appendChild(buildButton(prefix, 'accept', '✓', 'Accept suggestion', () => {
|
|
386
|
-
editor.chain().focus().
|
|
386
|
+
editor.chain().focus().approveSuggestion(s.id).run();
|
|
387
387
|
}));
|
|
388
388
|
root.appendChild(buildButton(prefix, 'reject', '✕', 'Reject suggestion', () => {
|
|
389
|
-
editor.chain().focus().
|
|
389
|
+
editor.chain().focus().rejectSuggestion(s.id).run();
|
|
390
390
|
}));
|
|
391
391
|
return root;
|
|
392
392
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -6,10 +6,10 @@ export { registerTiptap } from './register.js';
|
|
|
6
6
|
export { createPlainTextEditor, plainTextOf, plainTextToDoc, type PlainTextEditorOptions, } from './PlainTextEditor.js';
|
|
7
7
|
export { tiptap } from './plugin.js';
|
|
8
8
|
export { TiptapEditor } from './react/TiptapEditor.js';
|
|
9
|
-
export {
|
|
10
|
-
export {
|
|
11
|
-
export {
|
|
12
|
-
export { planReplaceBlock, planInsertBlockBefore, planDeleteBlock, planWrapBlocks, planUpdateBlockMark, summarizeBlockStructure, type BlockMarkRange, type TransactionModifier, } from './surgicalOps.js';
|
|
9
|
+
export { SuggestionChipExtension, suggestionChipPluginKey, upsertSuggestion, upsertSuggestions, removeSuggestion, remapSuggestions, sortForApproveAll, clampPos, type InlineSuggestion, type SuggestionChipExtensionOptions, } from './extensions/SuggestionChipExtension.js';
|
|
10
|
+
export { useSuggestionBridge } from './react/useSuggestionBridge.js';
|
|
11
|
+
export { InlineDiffExtension, inlineDiffPluginKey, getInlineDiffState, type InlineDiffExtensionOptions, } from './extensions/InlineDiffExtension.js';
|
|
12
|
+
export { planReplaceBlock, planInsertBlockBefore, planDeleteBlock, planWrapBlocks, planUpdateBlockMark, planReplaceText, summarizeBlockStructure, type BlockMarkRange, type TransactionModifier, } from './surgicalOps.js';
|
|
13
13
|
export { renderRichTextToHtml, isRichTextValue, type RenderRichTextOptions, type TiptapNode, type TiptapMark, } from './render.js';
|
|
14
14
|
export { contentBlockNodes, Intro, Faq, FaqItem, FaqQuestion, FaqAnswer, Alert, AlertTitle, AlertBody, Summary, KeyTakeaways, ProsCons, ProsColumn, ConsColumn, ContentBlockKeymap, LabeledBlockExitKeymap, planExitLabeledBlock, isSelectionInAlert, ALERT_VARIANTS, ALERT_VARIANT_LABEL, coerceAlertType, type AlertType, } from './extensions/contentBlocks.js';
|
|
15
15
|
export { shouldShowFloatingToolbar, TOOLBAR_MARKS } from './react/floatingToolbarVisibility.js';
|
package/dist/index.js
CHANGED
|
@@ -6,10 +6,10 @@ export { registerTiptap } from './register.js';
|
|
|
6
6
|
export { createPlainTextEditor, plainTextOf, plainTextToDoc, } from './PlainTextEditor.js';
|
|
7
7
|
export { tiptap } from './plugin.js';
|
|
8
8
|
export { TiptapEditor } from './react/TiptapEditor.js';
|
|
9
|
-
export {
|
|
10
|
-
export {
|
|
11
|
-
export {
|
|
12
|
-
export { planReplaceBlock, planInsertBlockBefore, planDeleteBlock, planWrapBlocks, planUpdateBlockMark, summarizeBlockStructure, } from './surgicalOps.js';
|
|
9
|
+
export { SuggestionChipExtension, suggestionChipPluginKey, upsertSuggestion, upsertSuggestions, removeSuggestion, remapSuggestions, sortForApproveAll, clampPos, } from './extensions/SuggestionChipExtension.js';
|
|
10
|
+
export { useSuggestionBridge } from './react/useSuggestionBridge.js';
|
|
11
|
+
export { InlineDiffExtension, inlineDiffPluginKey, getInlineDiffState, } from './extensions/InlineDiffExtension.js';
|
|
12
|
+
export { planReplaceBlock, planInsertBlockBefore, planDeleteBlock, planWrapBlocks, planUpdateBlockMark, planReplaceText, summarizeBlockStructure, } from './surgicalOps.js';
|
|
13
13
|
export { renderRichTextToHtml, isRichTextValue, } from './render.js';
|
|
14
14
|
// Default content-block node specs (Intro / FAQ / Alert / Summary / Key takeaways /
|
|
15
15
|
// Pros & cons). `contentBlockNodes` is the exact array `TiptapEditor` registers,
|
|
@@ -5,11 +5,11 @@ import { Slice } from '@tiptap/pm/model';
|
|
|
5
5
|
import { useCollabRoom, getCollabExtensions, } from '@pilotiq/pilotiq/react';
|
|
6
6
|
import { useCollabSeed } from '@rudderjs/sync/react';
|
|
7
7
|
import { createPlainTextEditor, plainTextOf, plainTextToDoc } from '../PlainTextEditor.js';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
8
|
+
import { SuggestionChipExtension } from '../extensions/SuggestionChipExtension.js';
|
|
9
|
+
import { InlineDiffExtension } from '../extensions/InlineDiffExtension.js';
|
|
10
|
+
import { useSuggestionBridge } from './useSuggestionBridge.js';
|
|
11
|
+
import { useInlineDiff, useIsInlineDiffActive, readDiffViewMarker } from './useInlineDiff.js';
|
|
12
|
+
import { SuggestionBanner } from './SuggestionBanner.js';
|
|
13
13
|
/**
|
|
14
14
|
* Tiptap-backed plain-text editor for pilotiq's `TextField` / `TextareaField`
|
|
15
15
|
* / similar single-line / multi-line text fields when collab is on.
|
|
@@ -100,11 +100,11 @@ export function CollabTextRenderer({ name, fragmentKey, multiline, defaultValue,
|
|
|
100
100
|
// AI suggestions — chip extension (producer-supplied range
|
|
101
101
|
// suggestions) + inline-diff extension (whole-field suggestions:
|
|
102
102
|
// red strikethrough on removed runs, green on inserted, with the
|
|
103
|
-
// `<
|
|
103
|
+
// `<SuggestionBanner>` Accept / Reject below). Both idle until
|
|
104
104
|
// a suggestion arrives via the bridges below. Matches the
|
|
105
105
|
// `TiptapEditor` wiring so the review surface reads identically
|
|
106
106
|
// across RichTextField / MarkdownField / TextField+TextareaField.
|
|
107
|
-
extensions: [...collabExtensions,
|
|
107
|
+
extensions: [...collabExtensions, SuggestionChipExtension, InlineDiffExtension],
|
|
108
108
|
onUpdate: (text) => onChange(text),
|
|
109
109
|
...(onSubmit ? { onSubmit: () => { onSubmit(); return false; } } : {}),
|
|
110
110
|
...(className || editorAttributes
|
|
@@ -128,11 +128,11 @@ export function CollabTextRenderer({ name, fragmentKey, multiline, defaultValue,
|
|
|
128
128
|
editor.setEditable(!disabled);
|
|
129
129
|
}, [editor, disabled]);
|
|
130
130
|
// Cross-package suggestion bridge — sync the host's
|
|
131
|
-
// `<PendingSuggestionsContext>` queue with the editor's `
|
|
131
|
+
// `<PendingSuggestionsContext>` queue with the editor's `InlineSuggestion`
|
|
132
132
|
// extension. No-op when no provider is mounted (default no-op context).
|
|
133
133
|
//
|
|
134
134
|
// Whole-field suggestions do NOT synthesize a chip range anymore —
|
|
135
|
-
// they render through `
|
|
135
|
+
// they render through `useInlineDiff` below (same red/green inline
|
|
136
136
|
// diff + banner as `TiptapEditor`), replacing the old green-pill chip
|
|
137
137
|
// that read differently from the rich-text surface. The bridge stays
|
|
138
138
|
// mounted for producer-supplied `meta.editorRange` suggestions (precise
|
|
@@ -143,12 +143,12 @@ export function CollabTextRenderer({ name, fragmentKey, multiline, defaultValue,
|
|
|
143
143
|
return;
|
|
144
144
|
editor.commands.setContent(plainTextToDoc(value, !!multiline));
|
|
145
145
|
};
|
|
146
|
-
|
|
146
|
+
useSuggestionBridge(editor ?? null, name, {
|
|
147
147
|
onApplyWholeField: applyWholeField,
|
|
148
148
|
});
|
|
149
149
|
// Inline diff for whole-field suggestions — plain-text shape: each
|
|
150
150
|
// line wraps in a `paragraph` node, mirroring `plainTextToDoc`.
|
|
151
|
-
|
|
151
|
+
useInlineDiff(editor ?? null, name, {
|
|
152
152
|
parseSuggestion: (ed, value) => {
|
|
153
153
|
try {
|
|
154
154
|
const node = ed.schema.nodeFromJSON(plainTextToDoc(value, !!multiline));
|
|
@@ -158,9 +158,9 @@ export function CollabTextRenderer({ name, fragmentKey, multiline, defaultValue,
|
|
|
158
158
|
return null;
|
|
159
159
|
}
|
|
160
160
|
},
|
|
161
|
-
resolveDisplayMode: () =>
|
|
161
|
+
resolveDisplayMode: () => readDiffViewMarker(name),
|
|
162
162
|
});
|
|
163
|
-
const isDiffActive =
|
|
163
|
+
const isDiffActive = useIsInlineDiffActive(editor ?? null);
|
|
164
164
|
// First-load seed when collab is active. Collaboration starts the editor
|
|
165
165
|
// empty regardless of `defaultValue`; once the room's first sync
|
|
166
166
|
// resolves, `useCollabSeed` runs the callback inside `ydoc.transact`.
|
|
@@ -216,10 +216,10 @@ export function CollabTextRenderer({ name, fragmentKey, multiline, defaultValue,
|
|
|
216
216
|
// Banner mounts below the editor exactly like `TiptapEditor`'s — it
|
|
217
217
|
// renders nothing while no suggestion is pending for this field, so
|
|
218
218
|
// the single-line text surface keeps its normal footprint.
|
|
219
|
-
return (_jsxs(_Fragment, { children: [_jsx(EditorContent, { editor: editor }), _jsx(
|
|
219
|
+
return (_jsxs(_Fragment, { children: [_jsx(EditorContent, { editor: editor }), _jsx(SuggestionBanner, { fieldName: name, onApplyWholeField: applyWholeField, ...(isDiffActive && editor
|
|
220
220
|
? {
|
|
221
|
-
onAcceptViaEditor: () => editor.commands.
|
|
222
|
-
onRejectViaEditor: () => editor.commands.
|
|
221
|
+
onAcceptViaEditor: () => editor.commands.acceptInlineDiff(),
|
|
222
|
+
onRejectViaEditor: () => editor.commands.rejectInlineDiff(),
|
|
223
223
|
}
|
|
224
224
|
: {}) })] }));
|
|
225
225
|
}
|
|
@@ -13,12 +13,12 @@ import { DOMParser as ProseMirrorDOMParser } from '@tiptap/pm/model';
|
|
|
13
13
|
import { Markdown } from '../markdownExtension.js';
|
|
14
14
|
import { useCollabRoom, getCollabExtensions, useToast, } from '@pilotiq/pilotiq/react';
|
|
15
15
|
import { useCollabSeed } from '@rudderjs/sync/react';
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
16
|
+
import { SuggestionChipExtension } from '../extensions/SuggestionChipExtension.js';
|
|
17
|
+
import { InlineDiffExtension } from '../extensions/InlineDiffExtension.js';
|
|
18
18
|
import { Alert, AlertTitle, AlertBody, ContentBlockKeymap } from '../extensions/contentBlocks.js';
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
19
|
+
import { useSuggestionBridge } from './useSuggestionBridge.js';
|
|
20
|
+
import { useInlineDiff, useIsInlineDiffActive, readDiffViewMarker } from './useInlineDiff.js';
|
|
21
|
+
import { SuggestionBanner } from './SuggestionBanner.js';
|
|
22
22
|
import { getMarkdownString, parseMarkdownToHtml } from '../markdownStorage.js';
|
|
23
23
|
// Inline lucide.dev SVGs — same posture as `toolbarButtons.tsx` so this
|
|
24
24
|
// package doesn't pull `lucide-react` as a peer dep. Keep stroke / size
|
|
@@ -135,13 +135,13 @@ export function MarkdownEditor({ name, fragmentKey, defaultValue, placeholder, d
|
|
|
135
135
|
Image.configure({ inline: false, allowBase64: false }),
|
|
136
136
|
Placeholder.configure({ placeholder: placeholder ?? 'Write in markdown…' }),
|
|
137
137
|
// AI suggestions — chip widget for surgical (range-anchored) edits.
|
|
138
|
-
|
|
138
|
+
SuggestionChipExtension,
|
|
139
139
|
// AI inline diff — Tiptap-Pro-style visualization for whole-field
|
|
140
140
|
// suggestions (prosemirror-changeset under the hood). Decorations
|
|
141
141
|
// show green-background inserts inline + red-strikethrough widgets
|
|
142
|
-
// for deleted text. Host's `<
|
|
142
|
+
// for deleted text. Host's `<SuggestionBanner>` drives Accept /
|
|
143
143
|
// Reject via the extension's commands.
|
|
144
|
-
|
|
144
|
+
InlineDiffExtension,
|
|
145
145
|
// Alert content block — round-trips to `:::alert{type=…} Title` via the
|
|
146
146
|
// node's `markdown` storage spec; renders the same shadcn NodeView.
|
|
147
147
|
Alert,
|
|
@@ -167,13 +167,13 @@ export function MarkdownEditor({ name, fragmentKey, defaultValue, placeholder, d
|
|
|
167
167
|
editor.setEditable(!disabled && tab === 'editor');
|
|
168
168
|
}, [editor, disabled, tab]);
|
|
169
169
|
// Cross-package suggestion bridge — sync the host's
|
|
170
|
-
// `<PendingSuggestionsContext>` queue with the editor's `
|
|
170
|
+
// `<PendingSuggestionsContext>` queue with the editor's `InlineSuggestion`
|
|
171
171
|
// extension. No-op when no provider is mounted (default no-op context).
|
|
172
172
|
//
|
|
173
173
|
// Whole-field handling: NO chip widget here. The chip's `textContent`
|
|
174
174
|
// renderer surfaces raw markdown (`## Heading\n- item`) as literal text
|
|
175
175
|
// inside the green pill — visually unparseable for multi-paragraph
|
|
176
|
-
// rewrites. Instead, `<
|
|
176
|
+
// rewrites. Instead, `<SuggestionBanner>` mounts below the editor
|
|
177
177
|
// (see render below). Producer-supplied range suggestions still ride
|
|
178
178
|
// the inline chip path — those have a precise anchor worth showing
|
|
179
179
|
// in context.
|
|
@@ -182,7 +182,7 @@ export function MarkdownEditor({ name, fragmentKey, defaultValue, placeholder, d
|
|
|
182
182
|
return;
|
|
183
183
|
editor.commands.setContent(value);
|
|
184
184
|
};
|
|
185
|
-
|
|
185
|
+
useSuggestionBridge(editor ?? null, name, {
|
|
186
186
|
onApplyWholeField: applyWholeField,
|
|
187
187
|
});
|
|
188
188
|
// Inline diff for whole-field suggestions — replaces the editor doc with
|
|
@@ -194,7 +194,7 @@ export function MarkdownEditor({ name, fragmentKey, defaultValue, placeholder, d
|
|
|
194
194
|
// that HTML into a Slice against THIS editor's schema — same path
|
|
195
195
|
// the editor's own clipboard-paste uses, so the slice is guaranteed
|
|
196
196
|
// schema-valid.
|
|
197
|
-
|
|
197
|
+
useInlineDiff(editor ?? null, name, {
|
|
198
198
|
parseSuggestion: (ed, value) => {
|
|
199
199
|
try {
|
|
200
200
|
const html = parseMarkdownToHtml(ed, value);
|
|
@@ -208,9 +208,9 @@ export function MarkdownEditor({ name, fragmentKey, defaultValue, placeholder, d
|
|
|
208
208
|
return null;
|
|
209
209
|
}
|
|
210
210
|
},
|
|
211
|
-
resolveDisplayMode: () =>
|
|
211
|
+
resolveDisplayMode: () => readDiffViewMarker(name),
|
|
212
212
|
});
|
|
213
|
-
const isDiffActive =
|
|
213
|
+
const isDiffActive = useIsInlineDiffActive(editor ?? null);
|
|
214
214
|
// First-load seed for collab. Collaboration starts the editor empty
|
|
215
215
|
// regardless of `content`; once the room's first sync resolves,
|
|
216
216
|
// `useCollabSeed` runs the callback inside `ydoc.transact`. Empty
|
|
@@ -439,10 +439,10 @@ export function MarkdownEditor({ name, fragmentKey, defaultValue, placeholder, d
|
|
|
439
439
|
: 'hover:bg-accent hover:text-accent-foreground',
|
|
440
440
|
'disabled:opacity-50',
|
|
441
441
|
].join(' '), onClick: () => exec(b), disabled: disabled || (isAttach && uploading), title: labels[b] ?? b, "aria-label": labels[b] ?? b, "aria-pressed": active, children: isAttach && uploading ? Spinner : icon }, b));
|
|
442
|
-
}) }))] }), tab === 'editor' && (_jsx("div", { className: "prose prose-sm dark:prose-invert max-w-none px-3 py-2 [&_.ProseMirror]:outline-none [&_.ProseMirror]:min-h-[6rem]", style: wrapperStyle, children: _jsx(EditorContent, { editor: editor }) })), tab === 'source' && (_jsx("textarea", { className: "w-full resize-y bg-transparent px-3 py-2 text-sm font-mono leading-relaxed outline-none disabled:opacity-50", style: wrapperStyle, value: sourceDraft, onChange: (e) => setSourceDraft(e.target.value), ...(placeholder !== undefined ? { placeholder } : {}), disabled: disabled, "aria-label": `${name} (markdown source)` })), tab === 'preview' && (_jsx("div", { className: "prose prose-sm dark:prose-invert max-w-none px-3 py-2", style: wrapperStyle, dangerouslySetInnerHTML: { __html: previewHtml || '<p class="text-muted-foreground italic">Nothing to preview</p>' } })), _jsx(
|
|
442
|
+
}) }))] }), tab === 'editor' && (_jsx("div", { className: "prose prose-sm dark:prose-invert max-w-none px-3 py-2 [&_.ProseMirror]:outline-none [&_.ProseMirror]:min-h-[6rem]", style: wrapperStyle, children: _jsx(EditorContent, { editor: editor }) })), tab === 'source' && (_jsx("textarea", { className: "w-full resize-y bg-transparent px-3 py-2 text-sm font-mono leading-relaxed outline-none disabled:opacity-50", style: wrapperStyle, value: sourceDraft, onChange: (e) => setSourceDraft(e.target.value), ...(placeholder !== undefined ? { placeholder } : {}), disabled: disabled, "aria-label": `${name} (markdown source)` })), tab === 'preview' && (_jsx("div", { className: "prose prose-sm dark:prose-invert max-w-none px-3 py-2", style: wrapperStyle, dangerouslySetInnerHTML: { __html: previewHtml || '<p class="text-muted-foreground italic">Nothing to preview</p>' } })), _jsx(SuggestionBanner, { fieldName: name, onApplyWholeField: applyWholeField, ...(isDiffActive && editor
|
|
443
443
|
? {
|
|
444
|
-
onAcceptViaEditor: () => editor.commands.
|
|
445
|
-
onRejectViaEditor: () => editor.commands.
|
|
444
|
+
onAcceptViaEditor: () => editor.commands.acceptInlineDiff(),
|
|
445
|
+
onRejectViaEditor: () => editor.commands.rejectInlineDiff(),
|
|
446
446
|
}
|
|
447
447
|
: {}) })] }));
|
|
448
448
|
}
|
|
@@ -25,7 +25,7 @@ import { type PendingSuggestion } from '@pilotiq/pilotiq/react';
|
|
|
25
25
|
* same field stack — Accept all / Reject all collapse the queue in one
|
|
26
26
|
* pass.
|
|
27
27
|
*/
|
|
28
|
-
export interface
|
|
28
|
+
export interface SuggestionBannerProps {
|
|
29
29
|
/** Field name, matches the suggestion's `fieldName`. */
|
|
30
30
|
fieldName: string;
|
|
31
31
|
/**
|
|
@@ -35,8 +35,8 @@ export interface AiSuggestionBannerProps {
|
|
|
35
35
|
* MarkdownEditor, HTML / JSON for TiptapEditor).
|
|
36
36
|
*
|
|
37
37
|
* Skipped when `onAcceptViaEditor` is supplied — that path means the
|
|
38
|
-
* editor already holds the proposed state via `
|
|
39
|
-
* and Accept routes through `
|
|
38
|
+
* editor already holds the proposed state via `InlineDiffExtension`,
|
|
39
|
+
* and Accept routes through `acceptInlineDiff()` instead. The host
|
|
40
40
|
* still calls `pendingSuggestions.approve(id)` afterwards to dismiss
|
|
41
41
|
* the queue entry.
|
|
42
42
|
*/
|
|
@@ -64,10 +64,10 @@ export interface AiSuggestionBannerProps {
|
|
|
64
64
|
* Hook variant — returns banner state without rendering, for renderers
|
|
65
65
|
* that want to compose their own chrome. Renderer-agnostic.
|
|
66
66
|
*/
|
|
67
|
-
export declare function
|
|
67
|
+
export declare function useSuggestionBanner(fieldName: string): {
|
|
68
68
|
pending: readonly PendingSuggestion[];
|
|
69
69
|
approveAll: (apply: (value: string) => void) => void;
|
|
70
70
|
rejectAll: () => void;
|
|
71
71
|
};
|
|
72
|
-
export declare function
|
|
73
|
-
//# sourceMappingURL=
|
|
72
|
+
export declare function SuggestionBanner({ fieldName, onApplyWholeField, onAcceptViaEditor, onRejectViaEditor, className, }: SuggestionBannerProps): React.ReactElement | null;
|
|
73
|
+
//# sourceMappingURL=SuggestionBanner.d.ts.map
|
|
@@ -5,7 +5,7 @@ import { usePendingSuggestionsForField, usePendingSuggestions, } from '@pilotiq/
|
|
|
5
5
|
* Hook variant — returns banner state without rendering, for renderers
|
|
6
6
|
* that want to compose their own chrome. Renderer-agnostic.
|
|
7
7
|
*/
|
|
8
|
-
export function
|
|
8
|
+
export function useSuggestionBanner(fieldName) {
|
|
9
9
|
const { list, dismiss } = usePendingSuggestionsForField(fieldName);
|
|
10
10
|
// Only whole-field suggestions land in the banner. Range-anchored ones
|
|
11
11
|
// ride the editor chip widget.
|
|
@@ -28,8 +28,8 @@ function hasEditorRange(s) {
|
|
|
28
28
|
const range = meta['editorRange'];
|
|
29
29
|
return !!(range && typeof range.from === 'number' && typeof range.to === 'number');
|
|
30
30
|
}
|
|
31
|
-
export function
|
|
32
|
-
const { pending, approveAll, rejectAll } =
|
|
31
|
+
export function SuggestionBanner({ fieldName, onApplyWholeField, onAcceptViaEditor, onRejectViaEditor, className, }) {
|
|
32
|
+
const { pending, approveAll, rejectAll } = useSuggestionBanner(fieldName);
|
|
33
33
|
const { dismiss } = usePendingSuggestions();
|
|
34
34
|
if (pending.length === 0)
|
|
35
35
|
return null;
|
|
@@ -63,9 +63,9 @@ export function AiSuggestionBanner({ fieldName, onApplyWholeField, onAcceptViaEd
|
|
|
63
63
|
// Per-suggestion controls when there's more than one — keeps the UX
|
|
64
64
|
// discoverable. Single suggestion: Accept / Reject only.
|
|
65
65
|
const single = pending.length === 1;
|
|
66
|
-
return (_jsxs("div", { role: "region", "aria-label": "AI suggested changes", "data-pilotiq-
|
|
66
|
+
return (_jsxs("div", { role: "region", "aria-label": "AI suggested changes", "data-pilotiq-suggestion-banner": "", className: className ?? 'pilotiq-suggestion-banner', children: [_jsx("span", { className: "pilotiq-suggestion-banner-icon", "aria-hidden": "true", children: "\uD83D\uDCA1" }), _jsx("span", { className: "pilotiq-suggestion-banner-label", children: single
|
|
67
67
|
? sourceLabel
|
|
68
68
|
? `Changes suggested by ${sourceLabel}`
|
|
69
69
|
: 'Changes suggested'
|
|
70
|
-
: `${pending.length} changes suggested` }), _jsxs("div", { className: "pilotiq-
|
|
70
|
+
: `${pending.length} changes suggested` }), _jsxs("div", { className: "pilotiq-suggestion-banner-actions", children: [_jsx("button", { type: "button", className: "pilotiq-suggestion-banner-reject", onClick: handleReject, children: single ? 'Reject' : 'Reject all' }), _jsx("button", { type: "button", className: "pilotiq-suggestion-banner-accept", onClick: handleAccept, children: single ? 'Accept' : 'Accept all' })] })] }));
|
|
71
71
|
}
|