@oix1987/yjd 2.1.2 → 2.2.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/README.md +63 -4
- package/core.js +1 -0
- package/dist/core.esm.js +1 -1
- package/dist/core.esm.js.map +1 -1
- package/dist/rich-editor.esm.js +1 -1
- package/dist/rich-editor.esm.js.map +1 -1
- package/dist/rich-editor.min.js +1 -1
- package/dist/rich-editor.min.js.map +1 -1
- package/index.d.ts +89 -0
- package/index.js +3 -0
- package/lib/core/editor.js +95 -1
- package/lib/modules/ai.js +494 -0
- package/lib/modules/toolbar.js +14 -2
- package/lib/styles.css +86 -0
- package/lib/styles.css.js +1 -1
- package/lib/styles.min.css +1 -1
- package/lib/ui/icons.js +2 -0
- package/package.json +2 -2
package/index.d.ts
CHANGED
|
@@ -76,6 +76,84 @@ export interface SubmitOptions {
|
|
|
76
76
|
newlineOnShiftEnter?: boolean;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
/** Context passed to the AI `complete` hook for one request. */
|
|
80
|
+
export interface AiContext {
|
|
81
|
+
/** Action id ('improve', 'fix', 'ask', 'autocomplete', or a custom id). */
|
|
82
|
+
action: string;
|
|
83
|
+
/** Instruction for the action (the action's prompt, or the user's question). */
|
|
84
|
+
prompt: string;
|
|
85
|
+
/** The selected text (or, for autocomplete, the preceding context). */
|
|
86
|
+
text: string;
|
|
87
|
+
/** The selected HTML, when available. */
|
|
88
|
+
html: string;
|
|
89
|
+
/** Aborts when the request is superseded or cancelled. */
|
|
90
|
+
signal: AbortSignal;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** A selection-toolbar action. */
|
|
94
|
+
export interface AiAction {
|
|
95
|
+
id: string;
|
|
96
|
+
label: string;
|
|
97
|
+
/** Instruction handed to the `complete` hook as `prompt`. */
|
|
98
|
+
prompt: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Inline ghost-text autocomplete tuning. */
|
|
102
|
+
export interface AiAutocompleteOptions {
|
|
103
|
+
/** Idle delay before requesting a suggestion (ms, default 400). */
|
|
104
|
+
debounce?: number;
|
|
105
|
+
/** Minimum context length before suggesting (default 3). */
|
|
106
|
+
minChars?: number;
|
|
107
|
+
/** Max characters of preceding text sent as context (default 600). */
|
|
108
|
+
maxContext?: number;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* AI configuration. Inert until a `complete` hook is given (BYO-model, like
|
|
113
|
+
* `mention.source`). Enables a selection toolbar (improve/fix/shorten/…/Ask AI)
|
|
114
|
+
* with accept-or-discard, and optional ghost-text autocomplete.
|
|
115
|
+
*/
|
|
116
|
+
export interface AiOptions {
|
|
117
|
+
/**
|
|
118
|
+
* Call your LLM and resolve to the generated text. Stream by invoking
|
|
119
|
+
* `onToken` with each chunk; if you only stream, return undefined and the
|
|
120
|
+
* chunks are joined.
|
|
121
|
+
*/
|
|
122
|
+
complete: (ctx: AiContext, onToken: (chunk: string) => void) => string | void | Promise<string | void>;
|
|
123
|
+
/** Replace/extend the selection-toolbar actions. */
|
|
124
|
+
actions?: AiAction[];
|
|
125
|
+
/** Inline ghost-text autocomplete (Tab to accept). */
|
|
126
|
+
autocomplete?: boolean | AiAutocompleteOptions;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Streaming sink returned by editor.streamInto(). */
|
|
130
|
+
export interface StreamSink {
|
|
131
|
+
/** Append a chunk at the caret (first append replaces the selection). */
|
|
132
|
+
append(chunk: string): void;
|
|
133
|
+
/** Finish the stream. */
|
|
134
|
+
commit(): void;
|
|
135
|
+
/** Undo the whole streamed insertion. */
|
|
136
|
+
cancel(): void;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** A snapshot of the current selection (the context an AI/tool acts on). */
|
|
140
|
+
export interface SelectionSnapshot {
|
|
141
|
+
text: string;
|
|
142
|
+
html: string;
|
|
143
|
+
isEmpty: boolean;
|
|
144
|
+
range: Range;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** The AI module instance, exposed as `editor.ai` when configured. */
|
|
148
|
+
export interface AiController {
|
|
149
|
+
/** Run a built-in/custom action or a free-form prompt on the selection. */
|
|
150
|
+
run(action: AiAction | string, opts?: { text?: string; html?: string }): Promise<string>;
|
|
151
|
+
/** Open the assistant bar (selection actions, or Ask-AI at the caret). */
|
|
152
|
+
openFromToolbar(): void;
|
|
153
|
+
/** Manually trigger a ghost-text completion at the caret. */
|
|
154
|
+
autocomplete(): void;
|
|
155
|
+
}
|
|
156
|
+
|
|
79
157
|
/**
|
|
80
158
|
* Toolbar configuration: a built-in preset, an exclusion of default items,
|
|
81
159
|
* a flat item list (single group), or full custom groups via toolbar1/toolbar2.
|
|
@@ -126,6 +204,8 @@ export interface EditorOptions {
|
|
|
126
204
|
mention?: MentionOptions;
|
|
127
205
|
/** Enter-to-submit behaviour for comment-style editors. */
|
|
128
206
|
submit?: SubmitOptions;
|
|
207
|
+
/** AI assistant (selection toolbar + ghost-text). Inert until `complete` is set. */
|
|
208
|
+
ai?: AiOptions;
|
|
129
209
|
/** Built-in preset / exclusion / flat list, instead of toolbar1/toolbar2. */
|
|
130
210
|
toolbar?: ToolbarOption;
|
|
131
211
|
/** Warn (emit 'content:overflow') when serialized HTML exceeds this many chars. */
|
|
@@ -159,6 +239,8 @@ export class Editor {
|
|
|
159
239
|
): TextareaEditor;
|
|
160
240
|
/** The contentEditable element (public — apps may attach listeners to it). */
|
|
161
241
|
editor: HTMLElement;
|
|
242
|
+
/** The AI module, present when `ai.complete` was configured. */
|
|
243
|
+
ai?: AiController;
|
|
162
244
|
on(event: string, handler: (data: any) => void): void;
|
|
163
245
|
/** Remove a previously-added listener (symmetric with on()). */
|
|
164
246
|
off(event: string, handler: (data: any) => void): void;
|
|
@@ -179,6 +261,12 @@ export class Editor {
|
|
|
179
261
|
clear(): void;
|
|
180
262
|
insertText(text: string): void;
|
|
181
263
|
insertHTML(html: string): void;
|
|
264
|
+
/** Snapshot of the current selection (null when outside the editor). */
|
|
265
|
+
getSelection(): SelectionSnapshot | null;
|
|
266
|
+
/** Replace the current selection with content (sanitized, undo-aware). */
|
|
267
|
+
replaceSelection(content: string, opts?: { asText?: boolean }): void;
|
|
268
|
+
/** Open a streaming sink at the caret for token-by-token AI output. */
|
|
269
|
+
streamInto(): StreamSink;
|
|
182
270
|
clearFormatting(): void;
|
|
183
271
|
insertHorizontalRule(): void;
|
|
184
272
|
insertImageFile(file: File): void;
|
|
@@ -281,6 +369,7 @@ export const CodeView: any;
|
|
|
281
369
|
export const FindReplace: any;
|
|
282
370
|
export const SlashMenu: any;
|
|
283
371
|
export const Mention: any;
|
|
372
|
+
export const Ai: any;
|
|
284
373
|
export const ResizeHandles: any;
|
|
285
374
|
|
|
286
375
|
// UI components
|
package/index.js
CHANGED
|
@@ -41,6 +41,7 @@ import CodeView from './lib/modules/code-view.js';
|
|
|
41
41
|
import FindReplace from './lib/modules/find-replace.js';
|
|
42
42
|
import SlashMenu from './lib/modules/slash-menu.js';
|
|
43
43
|
import Mention from './lib/modules/mention.js';
|
|
44
|
+
import Ai from './lib/modules/ai.js';
|
|
44
45
|
|
|
45
46
|
import ResizeHandles from './lib/modules/resize-handles.js';
|
|
46
47
|
|
|
@@ -97,6 +98,7 @@ registry.register('modules/code-view', CodeView, true);
|
|
|
97
98
|
registry.register('modules/find-replace', FindReplace, true);
|
|
98
99
|
registry.register('modules/slash-menu', SlashMenu, true);
|
|
99
100
|
registry.register('modules/mention', Mention, true);
|
|
101
|
+
registry.register('modules/ai', Ai, true);
|
|
100
102
|
|
|
101
103
|
registry.register('modules/resize-handles', ResizeHandles, true);
|
|
102
104
|
|
|
@@ -206,6 +208,7 @@ export {
|
|
|
206
208
|
FindReplace,
|
|
207
209
|
SlashMenu,
|
|
208
210
|
Mention,
|
|
211
|
+
Ai,
|
|
209
212
|
|
|
210
213
|
ResizeHandles
|
|
211
214
|
};
|
package/lib/core/editor.js
CHANGED
|
@@ -264,6 +264,12 @@ export default class Editor {
|
|
|
264
264
|
modulesToLoad.push('mention');
|
|
265
265
|
}
|
|
266
266
|
|
|
267
|
+
// The AI module is inert without an `ai.complete` hook, so it is never in
|
|
268
|
+
// the default set — load it only when the app configures one.
|
|
269
|
+
if (this.options.ai && !modulesToLoad.includes('ai')) {
|
|
270
|
+
modulesToLoad.push('ai');
|
|
271
|
+
}
|
|
272
|
+
|
|
267
273
|
|
|
268
274
|
modulesToLoad.forEach(moduleName => {
|
|
269
275
|
const ModuleClass = this.registry.get(`modules/${moduleName}`);
|
|
@@ -941,6 +947,86 @@ export default class Editor {
|
|
|
941
947
|
this.onContentChange();
|
|
942
948
|
}
|
|
943
949
|
|
|
950
|
+
/**
|
|
951
|
+
* Snapshot of the current selection — the context an AI action (or any tool)
|
|
952
|
+
* needs to operate on what the user picked. Returns null when the selection
|
|
953
|
+
* is outside this editor.
|
|
954
|
+
* @returns {{text:string, html:string, isEmpty:boolean, range:Range}|null}
|
|
955
|
+
*/
|
|
956
|
+
getSelection() {
|
|
957
|
+
const sel = window.getSelection();
|
|
958
|
+
if (!sel || !sel.rangeCount) return null;
|
|
959
|
+
const range = sel.getRangeAt(0);
|
|
960
|
+
if (!this.editor.contains(range.commonAncestorContainer)) return null;
|
|
961
|
+
const holder = document.createElement('div');
|
|
962
|
+
holder.appendChild(range.cloneContents());
|
|
963
|
+
return {
|
|
964
|
+
text: range.toString(),
|
|
965
|
+
html: holder.innerHTML,
|
|
966
|
+
isEmpty: range.collapsed,
|
|
967
|
+
// A detached clone, so the snapshot doesn't mutate as the caret moves.
|
|
968
|
+
range: range.cloneRange(),
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
/**
|
|
973
|
+
* Replace the current selection with new content (sanitized, undo-aware). The
|
|
974
|
+
* write path for AI rewrites and any "replace what I selected" tool. With a
|
|
975
|
+
* collapsed selection it inserts at the caret.
|
|
976
|
+
* @param {string} content
|
|
977
|
+
* @param {{asText?: boolean}} [opts] - insert as plain text instead of HTML
|
|
978
|
+
*/
|
|
979
|
+
replaceSelection(content, opts = {}) {
|
|
980
|
+
if (typeof content !== 'string') return;
|
|
981
|
+
const history = this.getModule('history');
|
|
982
|
+
if (history && typeof history.saveBeforeFormat === 'function') history.saveBeforeFormat();
|
|
983
|
+
this.focus();
|
|
984
|
+
if (opts.asText) {
|
|
985
|
+
execFormat('insertText', content);
|
|
986
|
+
} else {
|
|
987
|
+
execFormat('insertHTML', sanitizeHtml(content));
|
|
988
|
+
}
|
|
989
|
+
this.onContentChange();
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
/**
|
|
993
|
+
* Open a streaming sink at the current caret/selection so an AI response can
|
|
994
|
+
* be written token-by-token. The first append replaces the selection; later
|
|
995
|
+
* appends extend it. Designed for the "watch it type" UX.
|
|
996
|
+
*
|
|
997
|
+
* const s = editor.streamInto();
|
|
998
|
+
* for await (const chunk of res) s.append(chunk);
|
|
999
|
+
* s.commit(); // or s.cancel() to undo the whole stream
|
|
1000
|
+
*
|
|
1001
|
+
* @returns {{append(t:string):void, commit():void, cancel():void}}
|
|
1002
|
+
*/
|
|
1003
|
+
streamInto() {
|
|
1004
|
+
const history = this.getModule('history');
|
|
1005
|
+
if (history && typeof history.saveBeforeFormat === 'function') history.saveBeforeFormat();
|
|
1006
|
+
this.focus();
|
|
1007
|
+
// Snapshot the pre-stream HTML so cancel() can restore it in one step —
|
|
1008
|
+
// a stream produces several history entries, so a single undo() wouldn't
|
|
1009
|
+
// roll back the whole thing.
|
|
1010
|
+
const snapshot = this.editor.innerHTML;
|
|
1011
|
+
let inserted = '';
|
|
1012
|
+
const append = (chunk) => {
|
|
1013
|
+
if (typeof chunk !== 'string' || chunk === '') return;
|
|
1014
|
+
execFormat('insertText', chunk);
|
|
1015
|
+
inserted += chunk;
|
|
1016
|
+
this.onContentChange();
|
|
1017
|
+
};
|
|
1018
|
+
return {
|
|
1019
|
+
append,
|
|
1020
|
+
commit: () => { this.onContentChange(); },
|
|
1021
|
+
cancel: () => {
|
|
1022
|
+
if (inserted) {
|
|
1023
|
+
this.editor.innerHTML = snapshot;
|
|
1024
|
+
this.onContentChange();
|
|
1025
|
+
}
|
|
1026
|
+
},
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
|
|
944
1030
|
/**
|
|
945
1031
|
* Handle a paste event: sanitize pasted HTML, or paste as plain text.
|
|
946
1032
|
* @param {ClipboardEvent} e
|
|
@@ -1471,6 +1557,9 @@ export default class Editor {
|
|
|
1471
1557
|
if (m && m.isOpen) return true;
|
|
1472
1558
|
const s = this.modules.get('slash-menu');
|
|
1473
1559
|
if (s && s.isOpen) return true;
|
|
1560
|
+
// Note: the AI selection bar is intentionally NOT treated as an open menu —
|
|
1561
|
+
// it doesn't capture Enter, so suppressing submit would only insert a stray
|
|
1562
|
+
// newline. Enter-to-submit stays available while the bar floats.
|
|
1474
1563
|
// Any visible portaled popup (emoji, link, image, table…).
|
|
1475
1564
|
const sel = '.yjd-mention-menu, .yjd-slash-menu, .emoji-picker-popup.visible, .link-popup, .image-popup, .video-popup, .tag-popup';
|
|
1476
1565
|
return [...document.querySelectorAll(sel)].some((el) => {
|
|
@@ -1661,7 +1750,7 @@ export default class Editor {
|
|
|
1661
1750
|
this.emit('toolbar-click', data);
|
|
1662
1751
|
|
|
1663
1752
|
// Commands that should always work regardless of selection location
|
|
1664
|
-
const alwaysAllowedCommands = ['more', 'undo', 'redo', 'code-view', 'theme', 'text-direction', 'find'];
|
|
1753
|
+
const alwaysAllowedCommands = ['more', 'undo', 'redo', 'code-view', 'theme', 'text-direction', 'find', 'ai'];
|
|
1665
1754
|
|
|
1666
1755
|
if (alwaysAllowedCommands.includes(command)) {
|
|
1667
1756
|
// These commands can execute regardless of selection location
|
|
@@ -1685,6 +1774,11 @@ export default class Editor {
|
|
|
1685
1774
|
case 'find':
|
|
1686
1775
|
// Find/replace module listens to 'toolbar-click' and opens its panel
|
|
1687
1776
|
return;
|
|
1777
|
+
case 'ai': {
|
|
1778
|
+
const aiMod = this.getModule('ai');
|
|
1779
|
+
if (aiMod && typeof aiMod.openFromToolbar === 'function') aiMod.openFromToolbar();
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1688
1782
|
}
|
|
1689
1783
|
}
|
|
1690
1784
|
|