@malva-ui/editor 0.1.13 → 0.1.14
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/README.md +5 -5
- package/fesm2022/malva-ui-editor-ai.mjs +17 -0
- package/fesm2022/malva-ui-editor-ai.mjs.map +1 -0
- package/fesm2022/malva-ui-editor.mjs +10227 -0
- package/fesm2022/malva-ui-editor.mjs.map +1 -0
- package/package.json +35 -26
- package/types/malva-ui-editor-ai.d.ts +1 -0
- package/types/malva-ui-editor.d.ts +2995 -0
- package/ai/index.d.ts +0 -12
- package/ai/malva-ui-editor-ai.d.ts +0 -5
- package/esm2022/ai/index.js +0 -12
- package/esm2022/ai/index.js.map +0 -1
- package/esm2022/ai/malva-ui-editor-ai.js +0 -5
- package/esm2022/ai/malva-ui-editor-ai.js.map +0 -1
- package/esm2022/index.js +0 -37
- package/esm2022/index.js.map +0 -1
- package/esm2022/lib/ai/editor-ai-actions.js +0 -69
- package/esm2022/lib/ai/editor-ai-actions.js.map +0 -1
- package/esm2022/lib/ai/editor-ai-context.js +0 -612
- package/esm2022/lib/ai/editor-ai-context.js.map +0 -1
- package/esm2022/lib/ai/editor-ai-menu.js +0 -288
- package/esm2022/lib/ai/editor-ai-menu.js.map +0 -1
- package/esm2022/lib/ai/editor-ai-review-bar.js +0 -223
- package/esm2022/lib/ai/editor-ai-review-bar.js.map +0 -1
- package/esm2022/lib/ai/editor-ai-stream.js +0 -677
- package/esm2022/lib/ai/editor-ai-stream.js.map +0 -1
- package/esm2022/lib/ai/editor-ai-suggestions.js +0 -751
- package/esm2022/lib/ai/editor-ai-suggestions.js.map +0 -1
- package/esm2022/lib/ai/editor-ai.tokens.js +0 -10
- package/esm2022/lib/ai/editor-ai.tokens.js.map +0 -1
- package/esm2022/lib/ai/editor-ai.types.js +0 -1
- package/esm2022/lib/ai/editor-ai.types.js.map +0 -1
- package/esm2022/lib/editor/editor.js +0 -985
- package/esm2022/lib/editor/editor.js.map +0 -1
- package/esm2022/lib/editor-extension-preflight.js +0 -83
- package/esm2022/lib/editor-extension-preflight.js.map +0 -1
- package/esm2022/lib/editor-serialization.js +0 -152
- package/esm2022/lib/editor-serialization.js.map +0 -1
- package/esm2022/lib/editor-toolbar-context.js +0 -259
- package/esm2022/lib/editor-toolbar-context.js.map +0 -1
- package/esm2022/lib/editor.tokens.js +0 -10
- package/esm2022/lib/editor.tokens.js.map +0 -1
- package/esm2022/lib/editor.types.js +0 -48
- package/esm2022/lib/editor.types.js.map +0 -1
- package/esm2022/lib/extensions/editor-block-handle.js +0 -760
- package/esm2022/lib/extensions/editor-block-handle.js.map +0 -1
- package/esm2022/lib/extensions/editor-extensions.js +0 -321
- package/esm2022/lib/extensions/editor-extensions.js.map +0 -1
- package/esm2022/lib/extensions/editor-markdown-compatibility.js +0 -162
- package/esm2022/lib/extensions/editor-markdown-compatibility.js.map +0 -1
- package/esm2022/lib/extensions/editor-upload-placeholder.js +0 -156
- package/esm2022/lib/extensions/editor-upload-placeholder.js.map +0 -1
- package/esm2022/lib/status/editor-status.js +0 -64
- package/esm2022/lib/status/editor-status.js.map +0 -1
- package/esm2022/lib/toolbar/editor-alignment.js +0 -203
- package/esm2022/lib/toolbar/editor-alignment.js.map +0 -1
- package/esm2022/lib/toolbar/editor-block-insert.js +0 -101
- package/esm2022/lib/toolbar/editor-block-insert.js.map +0 -1
- package/esm2022/lib/toolbar/editor-color-control.js +0 -71
- package/esm2022/lib/toolbar/editor-color-control.js.map +0 -1
- package/esm2022/lib/toolbar/editor-command-button.js +0 -115
- package/esm2022/lib/toolbar/editor-command-button.js.map +0 -1
- package/esm2022/lib/toolbar/editor-heading.js +0 -266
- package/esm2022/lib/toolbar/editor-heading.js.map +0 -1
- package/esm2022/lib/toolbar/editor-highlight.js +0 -84
- package/esm2022/lib/toolbar/editor-highlight.js.map +0 -1
- package/esm2022/lib/toolbar/editor-image-upload-dialog.js +0 -289
- package/esm2022/lib/toolbar/editor-image-upload-dialog.js.map +0 -1
- package/esm2022/lib/toolbar/editor-image-upload.js +0 -315
- package/esm2022/lib/toolbar/editor-image-upload.js.map +0 -1
- package/esm2022/lib/toolbar/editor-inline-marks.js +0 -124
- package/esm2022/lib/toolbar/editor-inline-marks.js.map +0 -1
- package/esm2022/lib/toolbar/editor-link.js +0 -265
- package/esm2022/lib/toolbar/editor-link.js.map +0 -1
- package/esm2022/lib/toolbar/editor-list.js +0 -249
- package/esm2022/lib/toolbar/editor-list.js.map +0 -1
- package/esm2022/lib/toolbar/editor-table.js +0 -292
- package/esm2022/lib/toolbar/editor-table.js.map +0 -1
- package/esm2022/lib/toolbar/editor-text-color.js +0 -82
- package/esm2022/lib/toolbar/editor-text-color.js.map +0 -1
- package/esm2022/lib/toolbar/editor-toolbar-widget.js +0 -41
- package/esm2022/lib/toolbar/editor-toolbar-widget.js.map +0 -1
- package/esm2022/lib/toolbar/editor-toolbar.defs.js +0 -60
- package/esm2022/lib/toolbar/editor-toolbar.defs.js.map +0 -1
- package/esm2022/lib/toolbar/editor-toolbar.js +0 -826
- package/esm2022/lib/toolbar/editor-toolbar.js.map +0 -1
- package/esm2022/lib/toolbar/editor-undo-redo.js +0 -72
- package/esm2022/lib/toolbar/editor-undo-redo.js.map +0 -1
- package/esm2022/lib/toolbar/editor-zoom.js +0 -330
- package/esm2022/lib/toolbar/editor-zoom.js.map +0 -1
- package/esm2022/lib/upload/editor-image-upload-coordinator.js +0 -480
- package/esm2022/lib/upload/editor-image-upload-coordinator.js.map +0 -1
- package/esm2022/malva-ui-editor.js +0 -5
- package/esm2022/malva-ui-editor.js.map +0 -1
- package/index.d.ts +0 -35
- package/lib/ai/editor-ai-actions.d.ts +0 -27
- package/lib/ai/editor-ai-context.d.ts +0 -307
- package/lib/ai/editor-ai-menu.d.ts +0 -142
- package/lib/ai/editor-ai-review-bar.d.ts +0 -116
- package/lib/ai/editor-ai-stream.d.ts +0 -207
- package/lib/ai/editor-ai-suggestions.d.ts +0 -219
- package/lib/ai/editor-ai.tokens.d.ts +0 -10
- package/lib/ai/editor-ai.types.d.ts +0 -176
- package/lib/editor/editor.d.ts +0 -243
- package/lib/editor-extension-preflight.d.ts +0 -23
- package/lib/editor-serialization.d.ts +0 -74
- package/lib/editor-toolbar-context.d.ts +0 -116
- package/lib/editor.tokens.d.ts +0 -6
- package/lib/editor.types.d.ts +0 -194
- package/lib/extensions/editor-block-handle.d.ts +0 -59
- package/lib/extensions/editor-extensions.d.ts +0 -165
- package/lib/extensions/editor-markdown-compatibility.d.ts +0 -11
- package/lib/extensions/editor-upload-placeholder.d.ts +0 -75
- package/lib/status/editor-status.d.ts +0 -24
- package/lib/toolbar/editor-alignment.d.ts +0 -61
- package/lib/toolbar/editor-block-insert.d.ts +0 -26
- package/lib/toolbar/editor-color-control.d.ts +0 -32
- package/lib/toolbar/editor-command-button.d.ts +0 -29
- package/lib/toolbar/editor-heading.d.ts +0 -68
- package/lib/toolbar/editor-highlight.d.ts +0 -21
- package/lib/toolbar/editor-image-upload-dialog.d.ts +0 -96
- package/lib/toolbar/editor-image-upload.d.ts +0 -71
- package/lib/toolbar/editor-inline-marks.d.ts +0 -30
- package/lib/toolbar/editor-link.d.ts +0 -78
- package/lib/toolbar/editor-list.d.ts +0 -67
- package/lib/toolbar/editor-table.d.ts +0 -133
- package/lib/toolbar/editor-text-color.d.ts +0 -21
- package/lib/toolbar/editor-toolbar-widget.d.ts +0 -19
- package/lib/toolbar/editor-toolbar.d.ts +0 -157
- package/lib/toolbar/editor-toolbar.defs.d.ts +0 -35
- package/lib/toolbar/editor-undo-redo.d.ts +0 -26
- package/lib/toolbar/editor-zoom.d.ts +0 -108
- package/lib/upload/editor-image-upload-coordinator.d.ts +0 -55
- package/malva-ui-editor.d.ts +0 -5
|
@@ -1,612 +0,0 @@
|
|
|
1
|
-
import { LiveAnnouncer } from '@angular/cdk/a11y';
|
|
2
|
-
import { computed, inject, Injectable, InjectionToken, signal, } from '@angular/core';
|
|
3
|
-
import { MLV_EDITOR_I18N, MlvI18nResolverService } from '@malva-ui/i18n';
|
|
4
|
-
import { MLV_EDITOR_TOOLBAR_CONTEXT } from '../editor-toolbar-context';
|
|
5
|
-
import { runMlvEditorAiStream } from './editor-ai-stream';
|
|
6
|
-
import { applyMlvEditorAiSuggestions } from './editor-ai-suggestions';
|
|
7
|
-
import { MLV_EDITOR_AI_PROVIDER } from './editor-ai.tokens';
|
|
8
|
-
import * as i0 from "@angular/core";
|
|
9
|
-
/**
|
|
10
|
-
* @internal English ICU fallback for the review-started announcement, used
|
|
11
|
-
* when no `MLV_EDITOR_I18N` token is provided — the same template the English
|
|
12
|
-
* pack ships.
|
|
13
|
-
*/
|
|
14
|
-
const REVIEW_STARTED_FALLBACK = '{count, plural, one {# AI suggestion ready for review} other {# AI suggestions ready for review}}';
|
|
15
|
-
/**
|
|
16
|
-
* @internal English ICU fallbacks for the current-suggestion descriptions,
|
|
17
|
-
* used when no `MLV_EDITOR_I18N` token is provided — the same templates the
|
|
18
|
-
* English pack ships.
|
|
19
|
-
*/
|
|
20
|
-
const CURRENT_SUGGESTION_FALLBACKS = {
|
|
21
|
-
aiCurrentSuggestionReplace: 'Suggestion {index} of {count}: replaces "{oldText}" with "{newText}"',
|
|
22
|
-
aiCurrentSuggestionInsert: 'Suggestion {index} of {count}: inserts "{newText}"',
|
|
23
|
-
aiCurrentSuggestionDelete: 'Suggestion {index} of {count}: removes "{oldText}"',
|
|
24
|
-
};
|
|
25
|
-
/**
|
|
26
|
-
* @internal Supplies the owning `mlv-editor`'s `aiProvider` input signal to
|
|
27
|
-
* the per-editor AI context. The input wins over `MLV_EDITOR_AI_PROVIDER`,
|
|
28
|
-
* mirroring the image-uploader precedence rule. Deliberately excluded from
|
|
29
|
-
* the public barrels.
|
|
30
|
-
*/
|
|
31
|
-
export const MLV_EDITOR_AI_INPUT_PROVIDER = new InjectionToken('MLV_EDITOR_AI_INPUT_PROVIDER');
|
|
32
|
-
/**
|
|
33
|
-
* Per-editor AI command state provided by `mlv-editor`.
|
|
34
|
-
*
|
|
35
|
-
* The context owns provider resolution, request shaping, and the streaming
|
|
36
|
-
* and review session lifecycles; the framework-free engines
|
|
37
|
-
* ({@link runMlvEditorAiStream}, {@link applyMlvEditorAiSuggestions}) own
|
|
38
|
-
* document mutation. Every mutation is
|
|
39
|
-
* gated by the toolbar context's guard, so readonly and disabled editors
|
|
40
|
-
* refuse to start AI work — controls stay visible while a provider exists
|
|
41
|
-
* (presence) and disable with the editor (executability), matching the
|
|
42
|
-
* toolbar's presence-vs-executability rule.
|
|
43
|
-
*
|
|
44
|
-
* Failures never throw or reject: they surface as recoverable
|
|
45
|
-
* {@link MlvEditorError} emissions through the owning editor's `editorError`
|
|
46
|
-
* output. Cancellation stays silent.
|
|
47
|
-
*/
|
|
48
|
-
export class MlvEditorAiContext {
|
|
49
|
-
/** @private Guarded command surface and error channel of the owning editor. */
|
|
50
|
-
_toolbar = inject(MLV_EDITOR_TOOLBAR_CONTEXT);
|
|
51
|
-
/** @private Application-level provider used when the input supplies none. */
|
|
52
|
-
_injectedProvider = inject(MLV_EDITOR_AI_PROVIDER, {
|
|
53
|
-
optional: true,
|
|
54
|
-
});
|
|
55
|
-
/** @private The owning editor's `aiProvider` input; wins over the token. */
|
|
56
|
-
_inputProvider = inject(MLV_EDITOR_AI_INPUT_PROVIDER, {
|
|
57
|
-
optional: true,
|
|
58
|
-
});
|
|
59
|
-
/** @private Announces streaming lifecycle politely; decorations are aria-hidden. */
|
|
60
|
-
_liveAnnouncer = inject(LiveAnnouncer);
|
|
61
|
-
/** @private Optional translated announcement strings. */
|
|
62
|
-
_i18n = inject(MLV_EDITOR_I18N, { optional: true });
|
|
63
|
-
/** @private Resolves the ICU review-count announcement template. */
|
|
64
|
-
_resolver = inject(MlvI18nResolverService);
|
|
65
|
-
/** @private Writable lifecycle state behind the readonly public signal. */
|
|
66
|
-
_status = signal('idle', /* @ts-ignore */
|
|
67
|
-
...(ngDevMode ? [{ debugName: "_status" }] : /* istanbul ignore next */ []));
|
|
68
|
-
/** @private Writable pending-suggestion projection behind the readonly signal. */
|
|
69
|
-
_suggestions = signal([], /* @ts-ignore */
|
|
70
|
-
...(ngDevMode ? [{ debugName: "_suggestions" }] : /* istanbul ignore next */ []));
|
|
71
|
-
/**
|
|
72
|
-
* @private Latest streaming session. Retained after settlement so
|
|
73
|
-
* {@link restoreCheckpoint} can undo a committed transform; replaced when
|
|
74
|
-
* the next transform starts.
|
|
75
|
-
*/
|
|
76
|
-
_session = null;
|
|
77
|
-
/** @private Abort control for the in-flight provider request, if any. */
|
|
78
|
-
_abortController = null;
|
|
79
|
-
/**
|
|
80
|
-
* @private Active review wiring: the engine session plus the teardown that
|
|
81
|
-
* detaches the editor `transaction`/`destroy` listeners mirroring it into
|
|
82
|
-
* {@link suggestions}. `null` outside `'reviewing'`.
|
|
83
|
-
*/
|
|
84
|
-
_review = null;
|
|
85
|
-
/**
|
|
86
|
-
* @private Last suggestion id {@link revealSuggestion} announced. Repeated
|
|
87
|
-
* reveals of the same suggestion (the review bar re-runs its outline effect
|
|
88
|
-
* on unrelated signal updates) must not re-announce it.
|
|
89
|
-
*/
|
|
90
|
-
_revealedId = null;
|
|
91
|
-
/** Lifecycle of the AI surface for this editor. */
|
|
92
|
-
status = this._status.asReadonly();
|
|
93
|
-
/**
|
|
94
|
-
* Pending suggestions of the active review session, in document order.
|
|
95
|
-
* Empty outside `'reviewing'`. Refreshed after every editor transaction, so
|
|
96
|
-
* accept/reject calls, intersecting user edits, and an undo of the
|
|
97
|
-
* application are all reflected immediately. Document ranges deliberately
|
|
98
|
-
* stay internal — they remap on every transaction and would be stale the
|
|
99
|
-
* moment a consumer stored them.
|
|
100
|
-
*/
|
|
101
|
-
suggestions = this._suggestions.asReadonly();
|
|
102
|
-
/**
|
|
103
|
-
* Whether a review session with unresolved suggestions is active. Hosts
|
|
104
|
-
* gate saves on this: during `'reviewing'` the document serializes in its
|
|
105
|
-
* accepted-by-default shape (inserted text present, removed text absent).
|
|
106
|
-
*/
|
|
107
|
-
hasPendingSuggestions = computed(() => this._suggestions().length > 0, /* @ts-ignore */
|
|
108
|
-
...(ngDevMode ? [{ debugName: "hasPendingSuggestions" }] : /* istanbul ignore next */ []));
|
|
109
|
-
/**
|
|
110
|
-
* Whether an AI provider is resolvable for this editor, either through the
|
|
111
|
-
* `aiProvider` input or `MLV_EDITOR_AI_PROVIDER`. AI UI modules hide
|
|
112
|
-
* entirely without a provider.
|
|
113
|
-
*/
|
|
114
|
-
hasProvider = computed(() => this._resolveProvider() !== null, /* @ts-ignore */
|
|
115
|
-
...(ngDevMode ? [{ debugName: "hasProvider" }] : /* istanbul ignore next */ []));
|
|
116
|
-
/**
|
|
117
|
-
* Runs one selection transform through the resolved provider and lands the
|
|
118
|
-
* result according to the output mode: `'replace-selection'` and
|
|
119
|
-
* `'insert-below'` stream into the document, while `'review'` collects the
|
|
120
|
-
* complete result without writing and then applies it over the selection
|
|
121
|
-
* captured at start as reviewable tracked suggestions — status moves to
|
|
122
|
-
* `'reviewing'` and the suggestion surface
|
|
123
|
-
* ({@link suggestions}, {@link acceptSuggestion}, …) takes over. A review
|
|
124
|
-
* result identical to the region applies nothing: status returns straight
|
|
125
|
-
* to `'idle'` with a polite no-changes announcement and no error.
|
|
126
|
-
*
|
|
127
|
-
* The returned promise resolves after the session settles and never
|
|
128
|
-
* rejects; every failure is reported as a recoverable error through the
|
|
129
|
-
* editor's `editorError` output instead:
|
|
130
|
-
*
|
|
131
|
-
* - a transform already running, or a review still pending
|
|
132
|
-
* (`configuration`) — concurrent calls are refused rather than queued,
|
|
133
|
-
* so one editor never hosts two writers and pending suggestions are
|
|
134
|
-
* never silently replaced;
|
|
135
|
-
* - a readonly, disabled, or absent editor (`configuration`) — the toolbar
|
|
136
|
-
* guard refuses to start, and a `'review'` run re-checks the same guard
|
|
137
|
-
* after collection: an editor that became readonly or disabled mid-run
|
|
138
|
-
* refuses to apply the collected suggestions (checkpoint untouched — a
|
|
139
|
-
* review collection never wrote);
|
|
140
|
-
* - no resolvable provider (`configuration`);
|
|
141
|
-
* - a provider that throws or whose stream fails (`'ai-transport'`) — the
|
|
142
|
-
* checkpoint is restored;
|
|
143
|
-
* - unusable output (`'ai-result'`) — the checkpoint is restored. Both
|
|
144
|
-
* restore paths are no-ops for `'review'`, which never wrote.
|
|
145
|
-
*
|
|
146
|
-
* Start, successful finish, and cancellation are announced through the CDK
|
|
147
|
-
* `LiveAnnouncer` at `'polite'`; a successful `'review'` run announces the
|
|
148
|
-
* pending suggestion count instead of the generic finish. A failure
|
|
149
|
-
* announces nothing because the error channel already reports it.
|
|
150
|
-
*
|
|
151
|
-
* @param kind Transform requested from the provider.
|
|
152
|
-
* @param options Optional instruction and output mode.
|
|
153
|
-
*/
|
|
154
|
-
async runTransform(kind, options = {}) {
|
|
155
|
-
if (this._status() !== 'idle') {
|
|
156
|
-
this._reportConfiguration(this._status() === 'reviewing'
|
|
157
|
-
? 'An AI review is pending for this editor: resolve the suggestions before starting another transform.'
|
|
158
|
-
: 'An AI transform is already running for this editor.');
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
if (!this._toolbar.can(() => true)) {
|
|
162
|
-
this._reportConfiguration('AI transforms require an editable editor: readonly and disabled editors refuse to start.');
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
const editor = this._toolbar.editor();
|
|
166
|
-
if (!editor)
|
|
167
|
-
return;
|
|
168
|
-
const provider = this._resolveProvider();
|
|
169
|
-
if (!provider) {
|
|
170
|
-
this._reportConfiguration('No AI provider is configured. Supply the aiProvider input or provide MLV_EDITOR_AI_PROVIDER.');
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
const output = options.output ?? 'replace-selection';
|
|
174
|
-
// Review applies over the selection captured at start. External edits
|
|
175
|
-
// abandon the collection session, so a committed run still sees exactly
|
|
176
|
-
// this document region.
|
|
177
|
-
const reviewFrom = editor.state.selection.from;
|
|
178
|
-
const reviewTo = editor.state.selection.to;
|
|
179
|
-
const controller = new AbortController();
|
|
180
|
-
const request = {
|
|
181
|
-
kind,
|
|
182
|
-
...(options.instruction === undefined
|
|
183
|
-
? {}
|
|
184
|
-
: { instruction: options.instruction }),
|
|
185
|
-
context: {
|
|
186
|
-
selection: this._serializeSelection(editor),
|
|
187
|
-
document: this._serializeDocument(editor),
|
|
188
|
-
},
|
|
189
|
-
signal: controller.signal,
|
|
190
|
-
};
|
|
191
|
-
let chunks;
|
|
192
|
-
try {
|
|
193
|
-
chunks = provider.stream(request);
|
|
194
|
-
}
|
|
195
|
-
catch (cause) {
|
|
196
|
-
this._toolbar.reportError({
|
|
197
|
-
code: 'ai-transport',
|
|
198
|
-
message: 'The AI provider failed to start streaming.',
|
|
199
|
-
recoverable: true,
|
|
200
|
-
cause,
|
|
201
|
-
});
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
this._abortController = controller;
|
|
205
|
-
this._status.set('running');
|
|
206
|
-
this._announce('aiStreamingStarted', 'AI generation started');
|
|
207
|
-
const session = runMlvEditorAiStream(editor, { chunks, output });
|
|
208
|
-
this._session = session;
|
|
209
|
-
const result = await session.done;
|
|
210
|
-
if (this._abortController === controller)
|
|
211
|
-
this._abortController = null;
|
|
212
|
-
// Cancellation, abandonment, and failure all release the provider: the
|
|
213
|
-
// request signal aborts so a still-producing transport stops.
|
|
214
|
-
if (result.status !== 'committed')
|
|
215
|
-
controller.abort();
|
|
216
|
-
switch (result.status) {
|
|
217
|
-
case 'committed':
|
|
218
|
-
if (output === 'review') {
|
|
219
|
-
// The guard passed at start, but the editor may have become
|
|
220
|
-
// readonly or disabled while the result was collected. Applying
|
|
221
|
-
// suggestions is a document mutation, so it must re-pass the
|
|
222
|
-
// toolbar guard here; on refusal the checkpoint is untouched — a
|
|
223
|
-
// review collection never wrote.
|
|
224
|
-
if (!this._toolbar.can(() => true)) {
|
|
225
|
-
this._status.set('idle');
|
|
226
|
-
this._reportConfiguration('The editor became readonly or disabled while the AI review result was collected: the suggestions were not applied.');
|
|
227
|
-
break;
|
|
228
|
-
}
|
|
229
|
-
this._startReview(editor, reviewFrom, reviewTo, result.text);
|
|
230
|
-
}
|
|
231
|
-
else {
|
|
232
|
-
this._status.set('idle');
|
|
233
|
-
this._announce('aiStreamingFinished', 'AI generation finished');
|
|
234
|
-
}
|
|
235
|
-
break;
|
|
236
|
-
case 'cancelled':
|
|
237
|
-
case 'abandoned':
|
|
238
|
-
this._status.set('idle');
|
|
239
|
-
this._announce('aiStreamingCancelled', 'AI generation cancelled');
|
|
240
|
-
break;
|
|
241
|
-
case 'failed':
|
|
242
|
-
this._status.set('idle');
|
|
243
|
-
this._toolbar.reportError({
|
|
244
|
-
code: result.error ?? 'ai-result',
|
|
245
|
-
message: result.error === 'ai-transport'
|
|
246
|
-
? 'The AI provider stream failed.'
|
|
247
|
-
: 'The AI provider produced no usable output.',
|
|
248
|
-
recoverable: true,
|
|
249
|
-
});
|
|
250
|
-
break;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
/**
|
|
254
|
-
* Aborts the in-flight request and rolls the document back to the
|
|
255
|
-
* checkpoint captured when the transform started.
|
|
256
|
-
*
|
|
257
|
-
* The provider's `AbortSignal` aborts, the streaming session restores the
|
|
258
|
-
* checkpoint, and the transform resolves silently — no error is emitted
|
|
259
|
-
* and only the cancellation announcement is made. A no-op while idle.
|
|
260
|
-
*/
|
|
261
|
-
cancel() {
|
|
262
|
-
this._abortController?.abort();
|
|
263
|
-
this._abortController = null;
|
|
264
|
-
this._session?.cancel();
|
|
265
|
-
}
|
|
266
|
-
/**
|
|
267
|
-
* Restores the document to the checkpoint of the most recent transform.
|
|
268
|
-
*
|
|
269
|
-
* While a transform is running this behaves exactly like {@link cancel}.
|
|
270
|
-
* After a committed transform it reverts the commit in one history-visible
|
|
271
|
-
* step, so the restore is itself undoable. While `'reviewing'`, restoring
|
|
272
|
-
* reverts the applied suggestions wholesale, which drops the suggestion
|
|
273
|
-
* set and ends the review. The restore is a document mutation and
|
|
274
|
-
* therefore passes the toolbar guard: readonly and disabled editors refuse
|
|
275
|
-
* it. A no-op when no transform has run.
|
|
276
|
-
*/
|
|
277
|
-
restoreCheckpoint() {
|
|
278
|
-
if (this._status() === 'running') {
|
|
279
|
-
this.cancel();
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
|
-
const session = this._session;
|
|
283
|
-
if (!session)
|
|
284
|
-
return;
|
|
285
|
-
this._toolbar.run(() => session.restoreCheckpoint());
|
|
286
|
-
}
|
|
287
|
-
/**
|
|
288
|
-
* Accepts one pending suggestion: the applied change stays in the document
|
|
289
|
-
* and its decorations drop. Delegates to the review engine — a pure
|
|
290
|
-
* plugin-state update that creates no history entry. Announces the
|
|
291
|
-
* acceptance politely; when the last suggestion resolves the review ends
|
|
292
|
-
* and {@link status} returns to `'idle'`.
|
|
293
|
-
*
|
|
294
|
-
* Refused silently — decorations stay visible, nothing changes — while no
|
|
295
|
-
* review is active, the id is unknown, or the editor is readonly or
|
|
296
|
-
* disabled (the same guard rule that blocks every other AI mutation).
|
|
297
|
-
*
|
|
298
|
-
* @param id Identifier from {@link suggestions}.
|
|
299
|
-
*/
|
|
300
|
-
acceptSuggestion(id) {
|
|
301
|
-
const session = this._review?.session;
|
|
302
|
-
if (!session || !this._toolbar.can(() => true))
|
|
303
|
-
return;
|
|
304
|
-
if (session.accept(id)) {
|
|
305
|
-
this._announce('aiSuggestionAccepted', 'AI suggestion accepted');
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
/**
|
|
309
|
-
* Rejects one pending suggestion: the original content is restored over
|
|
310
|
-
* the suggestion's range in exactly one history-visible step and its
|
|
311
|
-
* decorations drop. Announces the rejection politely; when the last
|
|
312
|
-
* suggestion resolves the review ends and {@link status} returns to
|
|
313
|
-
* `'idle'`.
|
|
314
|
-
*
|
|
315
|
-
* Refused silently while no review is active, the id is unknown, or the
|
|
316
|
-
* editor is readonly or disabled — the restore is a document mutation and
|
|
317
|
-
* runs through the toolbar guard.
|
|
318
|
-
*
|
|
319
|
-
* @param id Identifier from {@link suggestions}.
|
|
320
|
-
*/
|
|
321
|
-
rejectSuggestion(id) {
|
|
322
|
-
const session = this._review?.session;
|
|
323
|
-
if (!session)
|
|
324
|
-
return;
|
|
325
|
-
if (this._toolbar.run(() => session.reject(id))) {
|
|
326
|
-
this._announce('aiSuggestionRejected', 'AI suggestion rejected');
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
/**
|
|
330
|
-
* Accepts every pending suggestion in one plugin-state update — no
|
|
331
|
-
* document step, no history entry — then ends the review and returns
|
|
332
|
-
* {@link status} to `'idle'`. Announced politely. Refused silently while
|
|
333
|
-
* no review is active or the editor is readonly or disabled.
|
|
334
|
-
*/
|
|
335
|
-
acceptAll() {
|
|
336
|
-
const session = this._review?.session;
|
|
337
|
-
if (!session || !this._toolbar.can(() => true))
|
|
338
|
-
return;
|
|
339
|
-
if (session.acceptAll()) {
|
|
340
|
-
this._announce('aiAllSuggestionsAccepted', 'All AI suggestions accepted');
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
/**
|
|
344
|
-
* Rejects every pending suggestion in exactly one history-visible step,
|
|
345
|
-
* restoring the original content of the whole reviewed region, then ends
|
|
346
|
-
* the review and returns {@link status} to `'idle'`. Announced politely.
|
|
347
|
-
* Refused silently while no review is active or the editor is readonly or
|
|
348
|
-
* disabled — the restore is a document mutation and runs through the
|
|
349
|
-
* toolbar guard.
|
|
350
|
-
*/
|
|
351
|
-
rejectAll() {
|
|
352
|
-
const session = this._review?.session;
|
|
353
|
-
if (!session)
|
|
354
|
-
return;
|
|
355
|
-
if (this._toolbar.run(() => session.rejectAll())) {
|
|
356
|
-
this._announce('aiAllSuggestionsRejected', 'All AI suggestions rejected');
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
/**
|
|
360
|
-
* Outlines one pending suggestion as the current one under review and
|
|
361
|
-
* scrolls its document range into view; `null` clears the outline (review
|
|
362
|
-
* surfaces call that on teardown so no marker strands).
|
|
363
|
-
*
|
|
364
|
-
* The outline decorations are `aria-hidden`, so revealing a suggestion also
|
|
365
|
-
* announces its {@link describeSuggestion} description politely — index,
|
|
366
|
-
* count, and the removed/added text — the non-visual equivalent the spec
|
|
367
|
-
* requires. Each suggestion is announced once per reveal: repeating the
|
|
368
|
-
* current id moves nothing and re-announces nothing.
|
|
369
|
-
*
|
|
370
|
-
* Otherwise a pure view affordance: the outline is a decoration-only
|
|
371
|
-
* plugin-state update, so no document step and no history entry is created
|
|
372
|
-
* — which is why, unlike accept/reject, it stays available in readonly
|
|
373
|
-
* editors, whose pending reviews remain visible and navigable. Refused
|
|
374
|
-
* silently while no review is active or the id is unknown. The review bar
|
|
375
|
-
* drives this from its navigation cursor.
|
|
376
|
-
*
|
|
377
|
-
* @param id Identifier from {@link suggestions}, or `null` to clear.
|
|
378
|
-
*/
|
|
379
|
-
revealSuggestion(id) {
|
|
380
|
-
const session = this._review?.session;
|
|
381
|
-
if (!session || !session.setCurrent(id))
|
|
382
|
-
return;
|
|
383
|
-
if (id === null) {
|
|
384
|
-
this._revealedId = null;
|
|
385
|
-
return;
|
|
386
|
-
}
|
|
387
|
-
const editor = this._toolbar.editor();
|
|
388
|
-
if (editor) {
|
|
389
|
-
const target = session
|
|
390
|
-
.suggestions()
|
|
391
|
-
.find((suggestion) => suggestion.id === id);
|
|
392
|
-
if (target)
|
|
393
|
-
this._scrollPositionIntoView(editor, target.range.from);
|
|
394
|
-
}
|
|
395
|
-
if (this._revealedId !== id) {
|
|
396
|
-
this._revealedId = id;
|
|
397
|
-
const description = this.describeSuggestion(id);
|
|
398
|
-
if (description) {
|
|
399
|
-
void this._liveAnnouncer.announce(description, 'polite');
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
/**
|
|
404
|
-
* Human-readable description of one pending suggestion — its one-based
|
|
405
|
-
* position in the pending list plus what it replaces, inserts, or removes —
|
|
406
|
-
* resolved through the `aiCurrentSuggestionReplace`/`-Insert`/`-Delete`
|
|
407
|
-
* ICU templates.
|
|
408
|
-
*
|
|
409
|
-
* This is the non-visual equivalent of the `aria-hidden` suggestion
|
|
410
|
-
* decorations: {@link revealSuggestion} announces it, and the review bar
|
|
411
|
-
* additionally exposes it to its accept/reject controls through
|
|
412
|
-
* `aria-describedby`, so the change under decision is inspectable without
|
|
413
|
-
* sight of the decorations. Returns `null` while no review is active or
|
|
414
|
-
* for an unknown id.
|
|
415
|
-
*
|
|
416
|
-
* @param id Identifier from {@link suggestions}.
|
|
417
|
-
*/
|
|
418
|
-
describeSuggestion(id) {
|
|
419
|
-
const pending = this._suggestions();
|
|
420
|
-
const index = pending.findIndex((suggestion) => suggestion.id === id);
|
|
421
|
-
if (index < 0)
|
|
422
|
-
return null;
|
|
423
|
-
const suggestion = pending[index];
|
|
424
|
-
const key = suggestion.kind === 'insert'
|
|
425
|
-
? 'aiCurrentSuggestionInsert'
|
|
426
|
-
: suggestion.kind === 'delete'
|
|
427
|
-
? 'aiCurrentSuggestionDelete'
|
|
428
|
-
: 'aiCurrentSuggestionReplace';
|
|
429
|
-
const template = this._i18n?.()[key] ?? CURRENT_SUGGESTION_FALLBACKS[key];
|
|
430
|
-
return this._resolver.resolve({ [key]: template }, key, {
|
|
431
|
-
index: index + 1,
|
|
432
|
-
count: pending.length,
|
|
433
|
-
oldText: suggestion.oldText,
|
|
434
|
-
newText: suggestion.newText,
|
|
435
|
-
});
|
|
436
|
-
}
|
|
437
|
-
/**
|
|
438
|
-
* @private Scrolls the rendered element at one document position into view.
|
|
439
|
-
* Best effort: position resolution can fail transiently mid-transaction and
|
|
440
|
-
* jsdom offers no `scrollIntoView`, so failure never fails the reveal.
|
|
441
|
-
*/
|
|
442
|
-
_scrollPositionIntoView(editor, position) {
|
|
443
|
-
try {
|
|
444
|
-
const { node } = editor.view.domAtPos(position);
|
|
445
|
-
const element = node instanceof HTMLElement ? node : node.parentElement;
|
|
446
|
-
if (element && typeof element.scrollIntoView === 'function') {
|
|
447
|
-
element.scrollIntoView({ block: 'nearest' });
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
catch {
|
|
451
|
-
// The outline already moved; scrolling is a progressive enhancement.
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
/**
|
|
455
|
-
* @private Routes a committed review collection through the suggestion
|
|
456
|
-
* engine. An engine refusal — the replacement is identical to the region,
|
|
457
|
-
* or otherwise nothing reviewable results — is the no-changes case: status
|
|
458
|
-
* returns to `'idle'` with a polite announcement, no error, and no
|
|
459
|
-
* document change. Otherwise the context enters `'reviewing'`, mirrors the
|
|
460
|
-
* engine's suggestion set into {@link suggestions} after every editor
|
|
461
|
-
* transaction (the session has no change notification of its own), and
|
|
462
|
-
* announces the pending count.
|
|
463
|
-
*/
|
|
464
|
-
_startReview(editor, from, to, replacementMarkdown) {
|
|
465
|
-
const session = applyMlvEditorAiSuggestions(editor, {
|
|
466
|
-
from,
|
|
467
|
-
to,
|
|
468
|
-
replacementMarkdown,
|
|
469
|
-
});
|
|
470
|
-
if (!session) {
|
|
471
|
-
this._status.set('idle');
|
|
472
|
-
this._announce('aiReviewNoChanges', 'The AI suggested no changes');
|
|
473
|
-
return;
|
|
474
|
-
}
|
|
475
|
-
const synchronize = () => {
|
|
476
|
-
if (this._review?.session !== session)
|
|
477
|
-
return;
|
|
478
|
-
const pending = this._projectSuggestions(session);
|
|
479
|
-
this._suggestions.set(pending);
|
|
480
|
-
if (pending.length === 0)
|
|
481
|
-
this._endReview();
|
|
482
|
-
};
|
|
483
|
-
const onDestroy = () => {
|
|
484
|
-
if (this._review?.session === session)
|
|
485
|
-
this._endReview();
|
|
486
|
-
};
|
|
487
|
-
editor.on('transaction', synchronize);
|
|
488
|
-
editor.on('destroy', onDestroy);
|
|
489
|
-
this._revealedId = null;
|
|
490
|
-
this._review = {
|
|
491
|
-
session,
|
|
492
|
-
detach: () => {
|
|
493
|
-
editor.off('transaction', synchronize);
|
|
494
|
-
editor.off('destroy', onDestroy);
|
|
495
|
-
},
|
|
496
|
-
};
|
|
497
|
-
const pending = this._projectSuggestions(session);
|
|
498
|
-
this._suggestions.set(pending);
|
|
499
|
-
if (pending.length === 0) {
|
|
500
|
-
// Defensive: the engine returns null rather than an empty session.
|
|
501
|
-
this._endReview();
|
|
502
|
-
this._announce('aiReviewNoChanges', 'The AI suggested no changes');
|
|
503
|
-
return;
|
|
504
|
-
}
|
|
505
|
-
this._status.set('reviewing');
|
|
506
|
-
this._announceReviewStarted(pending.length);
|
|
507
|
-
}
|
|
508
|
-
/**
|
|
509
|
-
* @private Ends the review session exactly once: detaches the editor
|
|
510
|
-
* listeners, clears the suggestion projection, and returns to `'idle'`.
|
|
511
|
-
* Runs when the suggestion set empties — accept/reject calls, intersecting
|
|
512
|
-
* edits dropping the last suggestion, or an undo of the application — and
|
|
513
|
-
* when the editor is destroyed.
|
|
514
|
-
*/
|
|
515
|
-
_endReview() {
|
|
516
|
-
const review = this._review;
|
|
517
|
-
if (!review)
|
|
518
|
-
return;
|
|
519
|
-
this._review = null;
|
|
520
|
-
this._revealedId = null;
|
|
521
|
-
review.detach();
|
|
522
|
-
this._suggestions.set([]);
|
|
523
|
-
this._status.set('idle');
|
|
524
|
-
}
|
|
525
|
-
/** @private Projects engine suggestions into the range-free public shape. */
|
|
526
|
-
_projectSuggestions(session) {
|
|
527
|
-
return session.suggestions().map(({ id, kind, oldText, newText }) => ({
|
|
528
|
-
id,
|
|
529
|
-
kind,
|
|
530
|
-
oldText,
|
|
531
|
-
newText,
|
|
532
|
-
}));
|
|
533
|
-
}
|
|
534
|
-
/** @private Announces the pending review count politely through the ICU template. */
|
|
535
|
-
_announceReviewStarted(count) {
|
|
536
|
-
const template = this._i18n?.().aiReviewStarted ?? REVIEW_STARTED_FALLBACK;
|
|
537
|
-
void this._liveAnnouncer.announce(this._resolver.resolve({ aiReviewStarted: template }, 'aiReviewStarted', {
|
|
538
|
-
count,
|
|
539
|
-
}), 'polite');
|
|
540
|
-
}
|
|
541
|
-
/** @private Resolves the active provider; the input wins over the token. */
|
|
542
|
-
_resolveProvider() {
|
|
543
|
-
return this._inputProvider?.() ?? this._injectedProvider ?? null;
|
|
544
|
-
}
|
|
545
|
-
/** @private Reports a recoverable configuration refusal through the editor. */
|
|
546
|
-
_reportConfiguration(message) {
|
|
547
|
-
this._toolbar.reportError({
|
|
548
|
-
code: 'configuration',
|
|
549
|
-
message,
|
|
550
|
-
recoverable: true,
|
|
551
|
-
});
|
|
552
|
-
}
|
|
553
|
-
/** @private Announces one localized AI lifecycle message politely. */
|
|
554
|
-
_announce(key, fallback) {
|
|
555
|
-
void this._liveAnnouncer.announce(this._i18n?.()[key] ?? fallback, 'polite');
|
|
556
|
-
}
|
|
557
|
-
/**
|
|
558
|
-
* @private Serializes the current selection as Markdown, or `null` when the
|
|
559
|
-
* selection is empty. Uses the editor's Markdown manager — the same
|
|
560
|
-
* serialization path as `getMarkdown()` — over the selection sliced into
|
|
561
|
-
* its own document; a replacement extension set without Markdown falls back
|
|
562
|
-
* to plain text so providers still receive usable context.
|
|
563
|
-
*/
|
|
564
|
-
_serializeSelection(editor) {
|
|
565
|
-
const { from, to } = editor.state.selection;
|
|
566
|
-
if (from === to)
|
|
567
|
-
return null;
|
|
568
|
-
const manager = editor.markdown;
|
|
569
|
-
if (manager) {
|
|
570
|
-
try {
|
|
571
|
-
const markdown = manager.serialize(editor.state.doc.cut(from, to).toJSON());
|
|
572
|
-
if (markdown.length > 0)
|
|
573
|
-
return markdown;
|
|
574
|
-
}
|
|
575
|
-
catch {
|
|
576
|
-
// Fall back to plain text below.
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
const text = editor.state.doc.textBetween(from, to, '\n\n');
|
|
580
|
-
return text.length > 0 ? text : null;
|
|
581
|
-
}
|
|
582
|
-
/**
|
|
583
|
-
* @private Serializes the whole document as Markdown, or `null` when the
|
|
584
|
-
* document is semantically empty. The document window is currently the full
|
|
585
|
-
* document; a bounded window is a later refinement. Falls back to plain
|
|
586
|
-
* text when the extension set carries no Markdown manager.
|
|
587
|
-
*/
|
|
588
|
-
_serializeDocument(editor) {
|
|
589
|
-
if (editor.isEmpty)
|
|
590
|
-
return null;
|
|
591
|
-
if (typeof editor.getMarkdown === 'function' && editor.markdown) {
|
|
592
|
-
try {
|
|
593
|
-
const markdown = editor.getMarkdown();
|
|
594
|
-
if (markdown.length > 0)
|
|
595
|
-
return markdown;
|
|
596
|
-
}
|
|
597
|
-
catch {
|
|
598
|
-
// Fall back to plain text below.
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
const text = editor.getText();
|
|
602
|
-
return text.length > 0 ? text : null;
|
|
603
|
-
}
|
|
604
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MlvEditorAiContext, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
605
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MlvEditorAiContext });
|
|
606
|
-
}
|
|
607
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MlvEditorAiContext, decorators: [{
|
|
608
|
-
type: Injectable
|
|
609
|
-
}] });
|
|
610
|
-
/** Injects the AI command state belonging to the nearest `mlv-editor`. */
|
|
611
|
-
export const MLV_EDITOR_AI_CONTEXT = new InjectionToken('MLV_EDITOR_AI_CONTEXT');
|
|
612
|
-
//# sourceMappingURL=editor-ai-context.js.map
|