@pilotiq/tiptap 3.11.0 → 3.13.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 +20 -0
- package/dist/Block.d.ts +8 -2
- package/dist/Block.js +9 -3
- package/dist/RichTextField.d.ts +17 -2
- package/dist/RichTextField.js +38 -7
- package/dist/blocks/alert.d.ts +10 -0
- package/dist/blocks/alert.js +23 -0
- package/dist/blocks/faq.d.ts +9 -0
- package/dist/blocks/faq.js +21 -0
- package/dist/blocks/index.d.ts +22 -0
- package/dist/blocks/index.js +20 -0
- package/dist/blocks/keyTakeaways.d.ts +10 -0
- package/dist/blocks/keyTakeaways.js +15 -0
- package/dist/blocks/prosCons.d.ts +9 -0
- package/dist/blocks/prosCons.js +15 -0
- package/dist/blocks/summary.d.ts +9 -0
- package/dist/blocks/summary.js +14 -0
- package/dist/extensions/AiInlineDiffExtension.d.ts +14 -2
- package/dist/extensions/AiInlineDiffExtension.js +175 -5
- package/dist/extensions/DragHandleExtension.js +20 -3
- package/dist/extensions/SlashCommandExtension.js +71 -0
- package/dist/extensions/contentBlocks.d.ts +19 -0
- package/dist/extensions/contentBlocks.js +281 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/react/CollabTextRenderer.js +2 -1
- package/dist/react/MarkdownEditor.js +2 -1
- package/dist/react/TiptapEditor.js +6 -1
- package/dist/react/useAiInlineDiff.d.ts +16 -0
- package/dist/react/useAiInlineDiff.js +23 -2
- package/dist/render.d.ts +5 -2
- package/dist/render.js +136 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# @pilotiq/tiptap
|
|
2
2
|
|
|
3
|
+
## 3.13.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- ded54a8: Ship five built-in **inline content blocks** in every `RichTextField` (free + pro): **FAQ**, **Alert** (info/warning/success/tip), **Summary**, **Key takeaways**, and **Pros & cons**.
|
|
8
|
+
|
|
9
|
+
They are **inline editable nodes** — a small label on top, content typed straight into the block in place (no card, no popup, no border/background). Inserted from the slash menu's **Content** group. Each renders read-side to semantic `pilotiq-*` HTML via `renderRichTextToHtml` (consumer owns the CSS). Quote and Table remain the native `blockquote` / table extensions.
|
|
10
|
+
|
|
11
|
+
- Nodes live in `extensions/contentBlocks.ts`; registered by default in the editor.
|
|
12
|
+
- Alert's type is the label (Info/Warning/Success/Tip), chosen from the slash menu.
|
|
13
|
+
- Pros & cons is two labelled list columns.
|
|
14
|
+
|
|
15
|
+
The `Block.make().schema([...])` API (card + side-panel form) stays for **custom** blocks via `RichTextField.blocks([...])`, but no schema block ships as a default. `Block.toMeta()` / `RichTextField.toMeta()` are now `async` so option-fields (Select/Radio/ToggleButtons) resolve correctly inside custom schema blocks.
|
|
16
|
+
|
|
17
|
+
## 3.12.0
|
|
18
|
+
|
|
19
|
+
### Minor Changes
|
|
20
|
+
|
|
21
|
+
- 7252aaa: GitHub-style line-mode rendering for the AI inline diff. `startAiInlineDiff` / `applySurgicalAiInlineDiff` accept an optional `displayMode: 'inline' | 'lines'` — in `'lines'` mode every block touched by an insert renders as a full-width green row (`+` gutter) and deleted content renders as stacked red rows (`−` gutter) above the change, instead of the inline word-flow. `useAiInlineDiff` gained `resolveDisplayMode`, and all three editor surfaces (rich text, markdown, collab text) resolve it from a `data-ai-diff-view` wrapper marker — stamped by `@pilotiq-pro/ai`'s `Field.aiDiffView('lines')` setter. Default stays `'inline'`.
|
|
22
|
+
|
|
3
23
|
## 3.11.0
|
|
4
24
|
|
|
5
25
|
### Minor Changes
|
package/dist/Block.d.ts
CHANGED
|
@@ -41,7 +41,13 @@ export declare class Block {
|
|
|
41
41
|
getLabel(): string;
|
|
42
42
|
getIcon(): string | undefined;
|
|
43
43
|
getSchema(): readonly Field[];
|
|
44
|
-
/**
|
|
45
|
-
|
|
44
|
+
/**
|
|
45
|
+
* @internal
|
|
46
|
+
* Async because schema fields can be async — `SelectField` / `RadioField` /
|
|
47
|
+
* `ToggleButtonsField` resolve their options at meta-build time and return a
|
|
48
|
+
* `Promise<FieldMeta>`. We await every field so the serialized schema never
|
|
49
|
+
* contains an unresolved Promise (which would JSON-stringify to `{}`).
|
|
50
|
+
*/
|
|
51
|
+
toMeta(): Promise<BlockMeta>;
|
|
46
52
|
}
|
|
47
53
|
//# sourceMappingURL=Block.d.ts.map
|
package/dist/Block.js
CHANGED
|
@@ -43,13 +43,19 @@ export class Block {
|
|
|
43
43
|
getLabel() { return this._label ?? this._name; }
|
|
44
44
|
getIcon() { return this._icon; }
|
|
45
45
|
getSchema() { return this._schema; }
|
|
46
|
-
/**
|
|
47
|
-
|
|
46
|
+
/**
|
|
47
|
+
* @internal
|
|
48
|
+
* Async because schema fields can be async — `SelectField` / `RadioField` /
|
|
49
|
+
* `ToggleButtonsField` resolve their options at meta-build time and return a
|
|
50
|
+
* `Promise<FieldMeta>`. We await every field so the serialized schema never
|
|
51
|
+
* contains an unresolved Promise (which would JSON-stringify to `{}`).
|
|
52
|
+
*/
|
|
53
|
+
async toMeta() {
|
|
48
54
|
return {
|
|
49
55
|
name: this._name,
|
|
50
56
|
label: this._label ?? this._name,
|
|
51
57
|
icon: this._icon,
|
|
52
|
-
schema: this._schema.map((f) => f.toMeta()),
|
|
58
|
+
schema: (await Promise.all(this._schema.map((f) => f.toMeta()))),
|
|
53
59
|
};
|
|
54
60
|
}
|
|
55
61
|
}
|
package/dist/RichTextField.d.ts
CHANGED
|
@@ -107,6 +107,7 @@ export interface RichTextFieldMeta extends FieldMeta {
|
|
|
107
107
|
*/
|
|
108
108
|
export declare class RichTextField extends Field {
|
|
109
109
|
private _blocks;
|
|
110
|
+
private _includeDefaultBlocks;
|
|
110
111
|
private _slashCommand;
|
|
111
112
|
/** `undefined` = use default, `null` = hidden, array = explicit override. */
|
|
112
113
|
private _toolbarOverride;
|
|
@@ -127,8 +128,22 @@ export declare class RichTextField extends Field {
|
|
|
127
128
|
private _mentionsUrl?;
|
|
128
129
|
private constructor();
|
|
129
130
|
static make(name: string): RichTextField;
|
|
130
|
-
/** Custom blocks available via the slash menu. */
|
|
131
|
+
/** Custom blocks available via the slash menu (merged on top of the defaults). */
|
|
131
132
|
blocks(blocks: Block[]): this;
|
|
133
|
+
/**
|
|
134
|
+
* Toggle the built-in default blocks (FAQ, Alert, Summary, Key takeaways,
|
|
135
|
+
* Pros & cons) for this field. On by default — every field gets them in the
|
|
136
|
+
* slash menu and the agent block catalog. Pass `false` to ship only the
|
|
137
|
+
* blocks declared via `.blocks([...])`.
|
|
138
|
+
*/
|
|
139
|
+
withDefaultBlocks(enabled?: boolean): this;
|
|
140
|
+
/**
|
|
141
|
+
* The blocks this field actually exposes — the default set (unless opted
|
|
142
|
+
* out) merged with the field's own `.blocks([...])`. A field block with the
|
|
143
|
+
* same name overrides the default. This is what the editor slash menu, the
|
|
144
|
+
* side panel, and the agent block catalog all consume.
|
|
145
|
+
*/
|
|
146
|
+
private resolveBlocks;
|
|
132
147
|
/** Toggle the slash menu (`/`) on/off. Defaults to `true`. */
|
|
133
148
|
slashCommand(enabled: boolean): this;
|
|
134
149
|
/**
|
|
@@ -281,6 +296,6 @@ export declare class RichTextField extends Field {
|
|
|
281
296
|
*/
|
|
282
297
|
getToolbarGroups(): ToolbarGroups | null;
|
|
283
298
|
isFloatingToolbarEnabled(): boolean;
|
|
284
|
-
toMeta(ctx?: RenderContext): RichTextFieldMeta
|
|
299
|
+
toMeta(ctx?: RenderContext): Promise<RichTextFieldMeta>;
|
|
285
300
|
}
|
|
286
301
|
//# sourceMappingURL=RichTextField.d.ts.map
|
package/dist/RichTextField.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Field } from '@pilotiq/pilotiq';
|
|
2
|
+
import { defaultBlocks } from './blocks/index.js';
|
|
2
3
|
/** Default top-level toolbar groups — mirrors the reference admin's layout. */
|
|
3
4
|
export const DEFAULT_TOOLBAR_GROUPS = [
|
|
4
5
|
['bold', 'italic', 'underline', 'strike', 'subscript', 'superscript', 'link'],
|
|
@@ -59,6 +60,7 @@ export const DEFAULT_HIGHLIGHT_COLORS = [
|
|
|
59
60
|
*/
|
|
60
61
|
export class RichTextField extends Field {
|
|
61
62
|
_blocks = [];
|
|
63
|
+
_includeDefaultBlocks = true;
|
|
62
64
|
_slashCommand = true;
|
|
63
65
|
/** `undefined` = use default, `null` = hidden, array = explicit override. */
|
|
64
66
|
_toolbarOverride = undefined;
|
|
@@ -83,11 +85,37 @@ export class RichTextField extends Field {
|
|
|
83
85
|
static make(name) {
|
|
84
86
|
return new RichTextField(name);
|
|
85
87
|
}
|
|
86
|
-
/** Custom blocks available via the slash menu. */
|
|
88
|
+
/** Custom blocks available via the slash menu (merged on top of the defaults). */
|
|
87
89
|
blocks(blocks) {
|
|
88
90
|
this._blocks = blocks;
|
|
89
91
|
return this;
|
|
90
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Toggle the built-in default blocks (FAQ, Alert, Summary, Key takeaways,
|
|
95
|
+
* Pros & cons) for this field. On by default — every field gets them in the
|
|
96
|
+
* slash menu and the agent block catalog. Pass `false` to ship only the
|
|
97
|
+
* blocks declared via `.blocks([...])`.
|
|
98
|
+
*/
|
|
99
|
+
withDefaultBlocks(enabled = true) {
|
|
100
|
+
this._includeDefaultBlocks = enabled;
|
|
101
|
+
return this;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The blocks this field actually exposes — the default set (unless opted
|
|
105
|
+
* out) merged with the field's own `.blocks([...])`. A field block with the
|
|
106
|
+
* same name overrides the default. This is what the editor slash menu, the
|
|
107
|
+
* side panel, and the agent block catalog all consume.
|
|
108
|
+
*/
|
|
109
|
+
resolveBlocks() {
|
|
110
|
+
const byName = new Map();
|
|
111
|
+
if (this._includeDefaultBlocks) {
|
|
112
|
+
for (const b of defaultBlocks)
|
|
113
|
+
byName.set(b.getName(), b);
|
|
114
|
+
}
|
|
115
|
+
for (const b of this._blocks)
|
|
116
|
+
byName.set(b.getName(), b);
|
|
117
|
+
return [...byName.values()];
|
|
118
|
+
}
|
|
91
119
|
/** Toggle the slash menu (`/`) on/off. Defaults to `true`. */
|
|
92
120
|
slashCommand(enabled) {
|
|
93
121
|
this._slashCommand = enabled;
|
|
@@ -256,7 +284,7 @@ export class RichTextField extends Field {
|
|
|
256
284
|
this._mentions = providers;
|
|
257
285
|
return this;
|
|
258
286
|
}
|
|
259
|
-
getBlocks() { return this.
|
|
287
|
+
getBlocks() { return this.resolveBlocks(); }
|
|
260
288
|
getMergeTags() { return this._mergeTags; }
|
|
261
289
|
getMentionProviders() { return this._mentions; }
|
|
262
290
|
/**
|
|
@@ -326,10 +354,13 @@ export class RichTextField extends Field {
|
|
|
326
354
|
isFloatingToolbarEnabled() {
|
|
327
355
|
return this._floatingToolbar;
|
|
328
356
|
}
|
|
329
|
-
toMeta(ctx) {
|
|
330
|
-
//
|
|
331
|
-
//
|
|
332
|
-
|
|
357
|
+
async toMeta(ctx) {
|
|
358
|
+
// Block fields can be async — Select / Radio / ToggleButtons resolve their
|
|
359
|
+
// options at meta-build time and return `Promise<FieldMeta>`. So block
|
|
360
|
+
// metas, and therefore this method, are async. The core resolves every
|
|
361
|
+
// field via `await field.toMeta(ctx)` (`resolveField`), so returning a
|
|
362
|
+
// Promise is the supported path.
|
|
363
|
+
const base = (await Promise.resolve(super.toMeta(ctx)));
|
|
333
364
|
// Strip `attachFiles` server-side when the panel hasn't registered an
|
|
334
365
|
// upload adapter — same posture as `MarkdownField` and the editor
|
|
335
366
|
// chrome stays clean. `uploadUrl` is the wire-side URL for the picker
|
|
@@ -342,7 +373,7 @@ export class RichTextField extends Field {
|
|
|
342
373
|
.filter(g => g.length > 0) ?? null;
|
|
343
374
|
return {
|
|
344
375
|
...base,
|
|
345
|
-
blocks: this.
|
|
376
|
+
blocks: await Promise.all(this.resolveBlocks().map((b) => b.toMeta())),
|
|
346
377
|
slashCommand: this._slashCommand,
|
|
347
378
|
toolbarGroups: filteredGroups,
|
|
348
379
|
floatingToolbar: this._floatingToolbar,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Block } from '../Block.js';
|
|
2
|
+
/**
|
|
3
|
+
* Default **Alert** (callout) block — a typed notice.
|
|
4
|
+
*
|
|
5
|
+
* Wire shape: `{ type: 'info' | 'warning' | 'success' | 'tip'; content: string }`.
|
|
6
|
+
* Renders read-side as `<div class="pilotiq-alert pilotiq-alert-<type>">`. An
|
|
7
|
+
* empty / unknown `type` falls back to `info` at render time.
|
|
8
|
+
*/
|
|
9
|
+
export declare const alertBlock: Block;
|
|
10
|
+
//# sourceMappingURL=alert.d.ts.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { SelectField, TextareaField } from '@pilotiq/pilotiq';
|
|
2
|
+
import { Block } from '../Block.js';
|
|
3
|
+
/**
|
|
4
|
+
* Default **Alert** (callout) block — a typed notice.
|
|
5
|
+
*
|
|
6
|
+
* Wire shape: `{ type: 'info' | 'warning' | 'success' | 'tip'; content: string }`.
|
|
7
|
+
* Renders read-side as `<div class="pilotiq-alert pilotiq-alert-<type>">`. An
|
|
8
|
+
* empty / unknown `type` falls back to `info` at render time.
|
|
9
|
+
*/
|
|
10
|
+
export const alertBlock = Block.make('alert')
|
|
11
|
+
.label('Alert')
|
|
12
|
+
.icon('⚠️')
|
|
13
|
+
.schema([
|
|
14
|
+
SelectField.make('type')
|
|
15
|
+
.label('Type')
|
|
16
|
+
.options([
|
|
17
|
+
{ value: 'info', label: 'Info' },
|
|
18
|
+
{ value: 'warning', label: 'Warning' },
|
|
19
|
+
{ value: 'success', label: 'Success' },
|
|
20
|
+
{ value: 'tip', label: 'Tip' },
|
|
21
|
+
]),
|
|
22
|
+
TextareaField.make('content').label('Content').placeholder('Alert message…'),
|
|
23
|
+
]);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Block } from '../Block.js';
|
|
2
|
+
/**
|
|
3
|
+
* Default **FAQ** block — a list of question / answer pairs.
|
|
4
|
+
*
|
|
5
|
+
* Wire shape: `{ items: Array<{ question: string; answer: string }> }`.
|
|
6
|
+
* Renders read-side as a `<div class="pilotiq-faq">` of `<details>` items.
|
|
7
|
+
*/
|
|
8
|
+
export declare const faqBlock: Block;
|
|
9
|
+
//# sourceMappingURL=faq.d.ts.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Repeater, TextField, TextareaField } from '@pilotiq/pilotiq';
|
|
2
|
+
import { Block } from '../Block.js';
|
|
3
|
+
/**
|
|
4
|
+
* Default **FAQ** block — a list of question / answer pairs.
|
|
5
|
+
*
|
|
6
|
+
* Wire shape: `{ items: Array<{ question: string; answer: string }> }`.
|
|
7
|
+
* Renders read-side as a `<div class="pilotiq-faq">` of `<details>` items.
|
|
8
|
+
*/
|
|
9
|
+
export const faqBlock = Block.make('faq')
|
|
10
|
+
.label('FAQ')
|
|
11
|
+
.icon('❓')
|
|
12
|
+
.schema([
|
|
13
|
+
Repeater.make('items')
|
|
14
|
+
.label('Questions')
|
|
15
|
+
.schema([
|
|
16
|
+
TextField.make('question').label('Question').placeholder('What is …?'),
|
|
17
|
+
TextareaField.make('answer').label('Answer').placeholder('A short, direct answer.'),
|
|
18
|
+
])
|
|
19
|
+
.reorderable()
|
|
20
|
+
.addActionLabel('Add question'),
|
|
21
|
+
]);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Block } from '../Block.js';
|
|
2
|
+
/**
|
|
3
|
+
* Schema-form block factories — `Block.make().schema([...])` style, edited via
|
|
4
|
+
* the right-docked side panel. Kept for **custom / legacy** use via
|
|
5
|
+
* `RichTextField.blocks([...])`. They are NO LONGER defaults.
|
|
6
|
+
*
|
|
7
|
+
* The built-in content blocks — FAQ, Alert, Summary, Key takeaways, Pros & cons
|
|
8
|
+
* — are now **inline editable nodes** (`extensions/contentBlocks.ts`):
|
|
9
|
+
* labelled, edited in place, registered directly in the editor + slash menu.
|
|
10
|
+
*/
|
|
11
|
+
export { faqBlock } from './faq.js';
|
|
12
|
+
export { alertBlock } from './alert.js';
|
|
13
|
+
export { summaryBlock } from './summary.js';
|
|
14
|
+
export { keyTakeawaysBlock } from './keyTakeaways.js';
|
|
15
|
+
export { prosConsBlock } from './prosCons.js';
|
|
16
|
+
/**
|
|
17
|
+
* Schema blocks shipped by default in every `RichTextField`. Empty — the
|
|
18
|
+
* default content blocks are inline nodes now, not schema-form blocks. A
|
|
19
|
+
* field's own `.blocks([...])` still merge in via `RichTextField`.
|
|
20
|
+
*/
|
|
21
|
+
export declare const defaultBlocks: readonly Block[];
|
|
22
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema-form block factories — `Block.make().schema([...])` style, edited via
|
|
3
|
+
* the right-docked side panel. Kept for **custom / legacy** use via
|
|
4
|
+
* `RichTextField.blocks([...])`. They are NO LONGER defaults.
|
|
5
|
+
*
|
|
6
|
+
* The built-in content blocks — FAQ, Alert, Summary, Key takeaways, Pros & cons
|
|
7
|
+
* — are now **inline editable nodes** (`extensions/contentBlocks.ts`):
|
|
8
|
+
* labelled, edited in place, registered directly in the editor + slash menu.
|
|
9
|
+
*/
|
|
10
|
+
export { faqBlock } from './faq.js';
|
|
11
|
+
export { alertBlock } from './alert.js';
|
|
12
|
+
export { summaryBlock } from './summary.js';
|
|
13
|
+
export { keyTakeawaysBlock } from './keyTakeaways.js';
|
|
14
|
+
export { prosConsBlock } from './prosCons.js';
|
|
15
|
+
/**
|
|
16
|
+
* Schema blocks shipped by default in every `RichTextField`. Empty — the
|
|
17
|
+
* default content blocks are inline nodes now, not schema-form blocks. A
|
|
18
|
+
* field's own `.blocks([...])` still merge in via `RichTextField`.
|
|
19
|
+
*/
|
|
20
|
+
export const defaultBlocks = [];
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Block } from '../Block.js';
|
|
2
|
+
/**
|
|
3
|
+
* Default **Key takeaways** block — a short bulleted list of the article's
|
|
4
|
+
* main points.
|
|
5
|
+
*
|
|
6
|
+
* Wire shape: `{ points: string[] }`. Renders read-side as
|
|
7
|
+
* `<div class="pilotiq-key-takeaways">` with a label and a `<ul>`.
|
|
8
|
+
*/
|
|
9
|
+
export declare const keyTakeawaysBlock: Block;
|
|
10
|
+
//# sourceMappingURL=keyTakeaways.d.ts.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { TagsInput } from '@pilotiq/pilotiq';
|
|
2
|
+
import { Block } from '../Block.js';
|
|
3
|
+
/**
|
|
4
|
+
* Default **Key takeaways** block — a short bulleted list of the article's
|
|
5
|
+
* main points.
|
|
6
|
+
*
|
|
7
|
+
* Wire shape: `{ points: string[] }`. Renders read-side as
|
|
8
|
+
* `<div class="pilotiq-key-takeaways">` with a label and a `<ul>`.
|
|
9
|
+
*/
|
|
10
|
+
export const keyTakeawaysBlock = Block.make('key-takeaways')
|
|
11
|
+
.label('Key takeaways')
|
|
12
|
+
.icon('🔑')
|
|
13
|
+
.schema([
|
|
14
|
+
TagsInput.make('points').label('Key takeaways'),
|
|
15
|
+
]);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Block } from '../Block.js';
|
|
2
|
+
/**
|
|
3
|
+
* Default **Pros & cons** block — two lists, side by side.
|
|
4
|
+
*
|
|
5
|
+
* Wire shape: `{ pros: string[]; cons: string[] }`. Renders read-side as
|
|
6
|
+
* `<div class="pilotiq-pros-cons">` with a `pros` and a `cons` column.
|
|
7
|
+
*/
|
|
8
|
+
export declare const prosConsBlock: Block;
|
|
9
|
+
//# sourceMappingURL=prosCons.d.ts.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { TagsInput } from '@pilotiq/pilotiq';
|
|
2
|
+
import { Block } from '../Block.js';
|
|
3
|
+
/**
|
|
4
|
+
* Default **Pros & cons** block — two lists, side by side.
|
|
5
|
+
*
|
|
6
|
+
* Wire shape: `{ pros: string[]; cons: string[] }`. Renders read-side as
|
|
7
|
+
* `<div class="pilotiq-pros-cons">` with a `pros` and a `cons` column.
|
|
8
|
+
*/
|
|
9
|
+
export const prosConsBlock = Block.make('pros-cons')
|
|
10
|
+
.label('Pros & cons')
|
|
11
|
+
.icon('⚖️')
|
|
12
|
+
.schema([
|
|
13
|
+
TagsInput.make('pros').label('Pros'),
|
|
14
|
+
TagsInput.make('cons').label('Cons'),
|
|
15
|
+
]);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Block } from '../Block.js';
|
|
2
|
+
/**
|
|
3
|
+
* Default **Summary** block — a short, labelled summary of the article.
|
|
4
|
+
*
|
|
5
|
+
* Wire shape: `{ content: string }`. Renders read-side as
|
|
6
|
+
* `<div class="pilotiq-summary">` with a label and the body.
|
|
7
|
+
*/
|
|
8
|
+
export declare const summaryBlock: Block;
|
|
9
|
+
//# sourceMappingURL=summary.d.ts.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { TextareaField } from '@pilotiq/pilotiq';
|
|
2
|
+
import { Block } from '../Block.js';
|
|
3
|
+
/**
|
|
4
|
+
* Default **Summary** block — a short, labelled summary of the article.
|
|
5
|
+
*
|
|
6
|
+
* Wire shape: `{ content: string }`. Renders read-side as
|
|
7
|
+
* `<div class="pilotiq-summary">` with a label and the body.
|
|
8
|
+
*/
|
|
9
|
+
export const summaryBlock = Block.make('summary')
|
|
10
|
+
.label('Summary')
|
|
11
|
+
.icon('📝')
|
|
12
|
+
.schema([
|
|
13
|
+
TextareaField.make('content').label('Summary').placeholder('A short summary of the article…'),
|
|
14
|
+
]);
|
|
@@ -48,7 +48,7 @@ declare module '@tiptap/core' {
|
|
|
48
48
|
* banner / approve handlers can correlate the editor state with
|
|
49
49
|
* the queue entry.
|
|
50
50
|
*/
|
|
51
|
-
startAiInlineDiff: (id: string, newDocSlice: Slice) => ReturnType;
|
|
51
|
+
startAiInlineDiff: (id: string, newDocSlice: Slice, displayMode?: AiDiffDisplayMode) => ReturnType;
|
|
52
52
|
/**
|
|
53
53
|
* Start the inline-diff review session for a surgical edit.
|
|
54
54
|
* Snapshots the current doc as the baseline, then runs
|
|
@@ -62,7 +62,7 @@ declare module '@tiptap/core' {
|
|
|
62
62
|
* `delete_block` / `update_block_mark` AI ops. Returns false (no
|
|
63
63
|
* dispatch) when `applyFn` produced no doc change.
|
|
64
64
|
*/
|
|
65
|
-
applySurgicalAiInlineDiff: (id: string, applyFn: (tr: Transaction) => void) => ReturnType;
|
|
65
|
+
applySurgicalAiInlineDiff: (id: string, applyFn: (tr: Transaction) => void, displayMode?: AiDiffDisplayMode) => ReturnType;
|
|
66
66
|
/** Clear diff state. Current doc IS the accepted state. */
|
|
67
67
|
acceptAiInlineDiff: () => ReturnType;
|
|
68
68
|
/** Revert doc to the captured baseline and clear diff state. */
|
|
@@ -70,12 +70,24 @@ declare module '@tiptap/core' {
|
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* How the pending diff renders:
|
|
75
|
+
* - `'inline'` (default) — word-flow: green inline decorations on
|
|
76
|
+
* inserted ranges, deleted text struck through in place.
|
|
77
|
+
* - `'lines'` — GitHub-style: every block touched by an insert gets a
|
|
78
|
+
* full-width green row (`+` gutter), deleted content renders as a
|
|
79
|
+
* full-width red row (`−` gutter) above the change. Suits markdown
|
|
80
|
+
* sources / structured text where lines are the meaningful unit.
|
|
81
|
+
*/
|
|
82
|
+
export type AiDiffDisplayMode = 'inline' | 'lines';
|
|
73
83
|
interface DiffState {
|
|
74
84
|
id: string;
|
|
75
85
|
/** Original doc captured at `startAiInlineDiff` time — used for revert. */
|
|
76
86
|
baseline: ProseMirrorNode;
|
|
77
87
|
/** ChangeSet accumulating diffs since baseline. */
|
|
78
88
|
changeset: ChangeSet;
|
|
89
|
+
/** Rendering mode for the decorations — see `AiDiffDisplayMode`. */
|
|
90
|
+
displayMode: AiDiffDisplayMode;
|
|
79
91
|
}
|
|
80
92
|
export declare const aiInlineDiffPluginKey: PluginKey<DiffState | null>;
|
|
81
93
|
/** Read the active diff state, if any. Public for hosts that want to
|
|
@@ -73,30 +73,64 @@ export const AiInlineDiffExtension = Extension.create({
|
|
|
73
73
|
color: rgb(153, 27, 27);
|
|
74
74
|
padding: 0 0.125em;
|
|
75
75
|
}
|
|
76
|
+
.${prefix}-inserted-line {
|
|
77
|
+
display: block;
|
|
78
|
+
background-color: rgba(187, 247, 208, 0.45);
|
|
79
|
+
border-radius: 2px;
|
|
80
|
+
padding-left: 1.25em;
|
|
81
|
+
position: relative;
|
|
82
|
+
/* Some text surfaces style the editor root as a flex row (input
|
|
83
|
+
mimic); full-basis keeps each diff row on its own line there. */
|
|
84
|
+
flex: 0 0 100%;
|
|
85
|
+
width: 100%;
|
|
86
|
+
}
|
|
87
|
+
.${prefix}-inserted-line::before {
|
|
88
|
+
content: '+';
|
|
89
|
+
position: absolute;
|
|
90
|
+
left: 0.25em;
|
|
91
|
+
color: rgb(20, 83, 45);
|
|
92
|
+
opacity: 0.7;
|
|
93
|
+
}
|
|
94
|
+
.${prefix}-deleted-lines { display: block; flex: 0 0 100%; width: 100%; }
|
|
95
|
+
.${prefix}-lines-active { display: block !important; }
|
|
96
|
+
.${prefix}-deleted-line {
|
|
97
|
+
background-color: rgba(254, 226, 226, 0.55);
|
|
98
|
+
color: rgb(153, 27, 27);
|
|
99
|
+
border-radius: 2px;
|
|
100
|
+
padding-left: 1.25em;
|
|
101
|
+
position: relative;
|
|
102
|
+
white-space: pre-wrap;
|
|
103
|
+
}
|
|
104
|
+
.${prefix}-deleted-line::before {
|
|
105
|
+
content: '−';
|
|
106
|
+
position: absolute;
|
|
107
|
+
left: 0.25em;
|
|
108
|
+
opacity: 0.7;
|
|
109
|
+
}
|
|
76
110
|
`;
|
|
77
111
|
document.head.appendChild(style);
|
|
78
112
|
},
|
|
79
113
|
addCommands() {
|
|
80
114
|
return {
|
|
81
|
-
startAiInlineDiff: (id, newDocSlice) => ({ tr, state, dispatch }) => {
|
|
115
|
+
startAiInlineDiff: (id, newDocSlice, displayMode) => ({ tr, state, dispatch }) => {
|
|
82
116
|
const baseline = state.doc;
|
|
83
117
|
const docEnd = state.doc.content.size;
|
|
84
118
|
// Replace the whole doc body with the proposed content. The
|
|
85
119
|
// schema enforces validity — if the slice doesn't fit, ProseMirror
|
|
86
120
|
// throws (callers should pre-validate via `editor.schema`).
|
|
87
121
|
tr.replaceRange(0, docEnd, newDocSlice);
|
|
88
|
-
const meta = { type: 'start', id, baseline };
|
|
122
|
+
const meta = { type: 'start', id, baseline, ...(displayMode ? { displayMode } : {}) };
|
|
89
123
|
tr.setMeta(aiInlineDiffPluginKey, meta);
|
|
90
124
|
if (dispatch)
|
|
91
125
|
dispatch(tr);
|
|
92
126
|
return true;
|
|
93
127
|
},
|
|
94
|
-
applySurgicalAiInlineDiff: (id, applyFn) => ({ tr, state, dispatch }) => {
|
|
128
|
+
applySurgicalAiInlineDiff: (id, applyFn, displayMode) => ({ tr, state, dispatch }) => {
|
|
95
129
|
const baseline = state.doc;
|
|
96
130
|
applyFn(tr);
|
|
97
131
|
if (!tr.docChanged)
|
|
98
132
|
return false;
|
|
99
|
-
const meta = { type: 'start', id, baseline };
|
|
133
|
+
const meta = { type: 'start', id, baseline, ...(displayMode ? { displayMode } : {}) };
|
|
100
134
|
tr.setMeta(aiInlineDiffPluginKey, meta);
|
|
101
135
|
if (dispatch)
|
|
102
136
|
dispatch(tr);
|
|
@@ -141,7 +175,7 @@ export const AiInlineDiffExtension = Extension.create({
|
|
|
141
175
|
// the transaction's step list to compute the diff between
|
|
142
176
|
// the baseline doc and the post-transaction doc.
|
|
143
177
|
const cs = ChangeSet.create(meta.baseline).addSteps(tr.doc, tr.mapping.maps, null);
|
|
144
|
-
return { id: meta.id, baseline: meta.baseline, changeset: cs };
|
|
178
|
+
return { id: meta.id, baseline: meta.baseline, changeset: cs, displayMode: meta.displayMode ?? 'inline' };
|
|
145
179
|
}
|
|
146
180
|
if (meta?.type === 'clear')
|
|
147
181
|
return null;
|
|
@@ -164,12 +198,25 @@ export const AiInlineDiffExtension = Extension.create({
|
|
|
164
198
|
return DecorationSet.empty;
|
|
165
199
|
return buildDiffDecorations(state, ds, ext.options.classPrefix ?? 'pilotiq-ai-diff');
|
|
166
200
|
},
|
|
201
|
+
// While a LINES-mode diff is active, force the editor root to
|
|
202
|
+
// block layout. Some text surfaces style the root as a flex row
|
|
203
|
+
// (single-line input mimic) — without this the stacked diff
|
|
204
|
+
// rows lay out as overflowing columns. Drops automatically on
|
|
205
|
+
// accept / reject.
|
|
206
|
+
attributes(state) {
|
|
207
|
+
const ds = aiInlineDiffPluginKey.getState(state);
|
|
208
|
+
return ds?.displayMode === 'lines'
|
|
209
|
+
? { class: `${ext.options.classPrefix ?? 'pilotiq-ai-diff'}-lines-active` }
|
|
210
|
+
: {};
|
|
211
|
+
},
|
|
167
212
|
},
|
|
168
213
|
}),
|
|
169
214
|
];
|
|
170
215
|
},
|
|
171
216
|
});
|
|
172
217
|
function buildDiffDecorations(state, ds, prefix) {
|
|
218
|
+
if (ds.displayMode === 'lines')
|
|
219
|
+
return buildLineDiffDecorations(state, ds, prefix);
|
|
173
220
|
const decos = [];
|
|
174
221
|
const docSize = state.doc.content.size;
|
|
175
222
|
for (const change of ds.changeset.changes) {
|
|
@@ -213,3 +260,126 @@ function buildDeletedWidget(text, prefix, id) {
|
|
|
213
260
|
root.appendChild(inner);
|
|
214
261
|
return root;
|
|
215
262
|
}
|
|
263
|
+
/**
|
|
264
|
+
* GitHub-style line rendering. Unlike the inline mode (which walks the
|
|
265
|
+
* changeset's MINIMAL change ranges), lines mode treats whole top-level
|
|
266
|
+
* blocks as the diff unit — an LCS over the baseline's block texts vs
|
|
267
|
+
* the current doc's block texts. A partially-edited line therefore
|
|
268
|
+
* renders as one full red row (the old line) above one full green row
|
|
269
|
+
* (the new line), instead of fragmented word-level shards. Mark-only
|
|
270
|
+
* changes (same text, different formatting) read as "kept" here — the
|
|
271
|
+
* block unit is text; use inline mode when formatting deltas matter.
|
|
272
|
+
*
|
|
273
|
+
* The changeset state still drives baseline capture / accept / reject;
|
|
274
|
+
* lines mode just re-derives its presentation from baseline-vs-current
|
|
275
|
+
* on every decoration pass, so remote collab edits during review stay
|
|
276
|
+
* correct for free.
|
|
277
|
+
*/
|
|
278
|
+
function buildLineDiffDecorations(state, ds, prefix) {
|
|
279
|
+
const decos = [];
|
|
280
|
+
const baseTexts = topLevelBlockTexts(ds.baseline);
|
|
281
|
+
const current = [];
|
|
282
|
+
state.doc.forEach((node, pos) => {
|
|
283
|
+
current.push({ text: node.textContent, pos, nodeSize: node.nodeSize });
|
|
284
|
+
});
|
|
285
|
+
// Walk tokens with a pointer into the CURRENT doc's blocks. Removed
|
|
286
|
+
// baseline lines accumulate and flush as ONE widget anchored before
|
|
287
|
+
// the next current block (or at doc end), so consecutive deletions
|
|
288
|
+
// render as a contiguous red row group.
|
|
289
|
+
let j = 0;
|
|
290
|
+
let pendingRemoved = [];
|
|
291
|
+
const flushRemoved = (anchor) => {
|
|
292
|
+
if (pendingRemoved.length === 0)
|
|
293
|
+
return;
|
|
294
|
+
const lines = pendingRemoved;
|
|
295
|
+
pendingRemoved = [];
|
|
296
|
+
decos.push(Decoration.widget(anchor, () => buildDeletedLinesWidget(lines.join('\n'), prefix, ds.id), {
|
|
297
|
+
side: -1,
|
|
298
|
+
ignoreSelection: true,
|
|
299
|
+
key: `pilotiq-ai-diff:deleted-lines:${anchor}:${lines.length}`,
|
|
300
|
+
}));
|
|
301
|
+
};
|
|
302
|
+
for (const tok of lcsBlockDiffTokens(baseTexts, current.map(c => c.text))) {
|
|
303
|
+
if (tok.kind === 'kept') {
|
|
304
|
+
flushRemoved(current[j].pos);
|
|
305
|
+
j++;
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
if (tok.kind === 'added') {
|
|
309
|
+
flushRemoved(current[j].pos);
|
|
310
|
+
decos.push(Decoration.node(current[j].pos, current[j].pos + current[j].nodeSize, {
|
|
311
|
+
class: `${prefix}-inserted-line`,
|
|
312
|
+
'data-pilotiq-ai-diff-id': ds.id,
|
|
313
|
+
}));
|
|
314
|
+
j++;
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
// removed — baseline-only line; accumulate for the next flush.
|
|
318
|
+
pendingRemoved.push(tok.text);
|
|
319
|
+
}
|
|
320
|
+
flushRemoved(state.doc.content.size);
|
|
321
|
+
return DecorationSet.create(state.doc, decos);
|
|
322
|
+
}
|
|
323
|
+
function topLevelBlockTexts(doc) {
|
|
324
|
+
const out = [];
|
|
325
|
+
doc.forEach((node) => { out.push(node.textContent); });
|
|
326
|
+
return out;
|
|
327
|
+
}
|
|
328
|
+
/** Standard LCS walk over two block-text arrays, emitting tokens in
|
|
329
|
+
* presentation order with removed-before-added on replacements. */
|
|
330
|
+
function lcsBlockDiffTokens(a, b) {
|
|
331
|
+
const n = a.length;
|
|
332
|
+
const m = b.length;
|
|
333
|
+
const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
334
|
+
for (let i = 1; i <= n; i++) {
|
|
335
|
+
for (let j = 1; j <= m; j++) {
|
|
336
|
+
if (a[i - 1] === b[j - 1])
|
|
337
|
+
dp[i][j] = dp[i - 1][j - 1] + 1;
|
|
338
|
+
else
|
|
339
|
+
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
const out = [];
|
|
343
|
+
let i = n;
|
|
344
|
+
let j = m;
|
|
345
|
+
while (i > 0 && j > 0) {
|
|
346
|
+
if (a[i - 1] === b[j - 1]) {
|
|
347
|
+
out.push({ kind: 'kept', text: a[i - 1] });
|
|
348
|
+
i--;
|
|
349
|
+
j--;
|
|
350
|
+
}
|
|
351
|
+
// Strict `>` ties toward pushing `added` first in this backward walk,
|
|
352
|
+
// which renders removed-before-added after the final reverse.
|
|
353
|
+
else if (dp[i - 1][j] > dp[i][j - 1]) {
|
|
354
|
+
out.push({ kind: 'removed', text: a[i - 1] });
|
|
355
|
+
i--;
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
out.push({ kind: 'added', text: b[j - 1] });
|
|
359
|
+
j--;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
while (i > 0) {
|
|
363
|
+
out.push({ kind: 'removed', text: a[i - 1] });
|
|
364
|
+
i--;
|
|
365
|
+
}
|
|
366
|
+
while (j > 0) {
|
|
367
|
+
out.push({ kind: 'added', text: b[j - 1] });
|
|
368
|
+
j--;
|
|
369
|
+
}
|
|
370
|
+
out.reverse();
|
|
371
|
+
return out;
|
|
372
|
+
}
|
|
373
|
+
function buildDeletedLinesWidget(text, prefix, id) {
|
|
374
|
+
const root = document.createElement('div');
|
|
375
|
+
root.className = `${prefix}-deleted-lines`;
|
|
376
|
+
root.setAttribute('data-pilotiq-ai-diff-id', id);
|
|
377
|
+
root.contentEditable = 'false';
|
|
378
|
+
for (const line of text.split('\n')) {
|
|
379
|
+
const row = document.createElement('div');
|
|
380
|
+
row.className = `${prefix}-deleted-line`;
|
|
381
|
+
row.textContent = line || ' ';
|
|
382
|
+
root.appendChild(row);
|
|
383
|
+
}
|
|
384
|
+
return root;
|
|
385
|
+
}
|