@homecode/ui 5.20.19 → 5.20.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/src/components/Autocomplete/Autocomplete.js +1 -0
- package/dist/esm/src/components/InputFile/InputFile.js +1 -0
- package/dist/esm/src/components/PromptComposer/PromptComposer.styl.js +2 -2
- package/dist/esm/src/components/PromptComposer/promptComposerDoc.js +2 -13
- package/dist/esm/src/components/PromptComposer/promptComposerInsert.js +3 -2
- package/dist/esm/src/components/PromptComposer/usePromptComposerEditor.js +16 -9
- package/dist/esm/src/tiptap/prompt-blocks/createPromptBlocksExtension.js +260 -0
- package/dist/esm/src/tiptap/prompt-blocks/promptBlocksText.js +143 -0
- package/dist/esm/types/src/components/PromptComposer/promptComposerDoc.d.ts +0 -1
- package/dist/esm/types/src/tiptap/prompt-blocks/createPromptBlocksExtension.d.ts +25 -0
- package/dist/esm/types/src/tiptap/prompt-blocks/index.d.ts +3 -0
- package/dist/esm/types/src/tiptap/prompt-blocks/promptBlocks.test.d.ts +1 -0
- package/dist/esm/types/src/tiptap/prompt-blocks/promptBlocksText.d.ts +19 -0
- package/package.json +2 -1
|
@@ -80,6 +80,7 @@ import '../Progress/Progress.styl.js';
|
|
|
80
80
|
import '@tiptap/extensions';
|
|
81
81
|
import '@tiptap/starter-kit';
|
|
82
82
|
import '../PromptComposer/PromptComposer.styl.js';
|
|
83
|
+
import '../../tiptap/prompt-blocks/createPromptBlocksExtension.js';
|
|
83
84
|
import '@tiptap/pm/view';
|
|
84
85
|
import 'linkifyjs';
|
|
85
86
|
import '../../tiptap/link-highlight/nonInclusiveLink.js';
|
|
@@ -76,6 +76,7 @@ import '@tiptap/extensions';
|
|
|
76
76
|
import '@tiptap/react';
|
|
77
77
|
import '@tiptap/starter-kit';
|
|
78
78
|
import '../PromptComposer/PromptComposer.styl.js';
|
|
79
|
+
import '../../tiptap/prompt-blocks/createPromptBlocksExtension.js';
|
|
79
80
|
import '@tiptap/core';
|
|
80
81
|
import '@tiptap/pm/state';
|
|
81
82
|
import '@tiptap/pm/view';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
var css_248z = ".PromptComposer_root__gfdXN{width:100%}.PromptComposer_scroller__Sueif{flex:1;max-height:200px;min-height:40px;width:100%}.PromptComposer_editorMount__B9G-o{background:transparent;border:none;border-radius:0!important;box-shadow:none!important;display:flex;flex:1;flex-direction:column;min-height:40px;min-width:0;padding:0!important}.PromptComposer_editorMount__B9G-o:focus-within{box-shadow:none!important}.PromptComposer_editorMount__B9G-o .PromptComposer_promptComposerEditor__yQIpq{border:none!important;box-shadow:none!important;flex:1;margin:0;min-height:40px!important;outline:none!important;overflow:hidden!important;padding:var(--p-2) 0 0!important;resize:none!important;transition:opacity .1s ease-out;white-space:pre-wrap;word-break:break-word}.PromptComposer_editorMount__B9G-o .PromptComposer_promptComposerEditor__yQIpq .PromptComposer_promptComposerLink__rKhhp,.PromptComposer_editorMount__B9G-o .PromptComposer_promptComposerEditor__yQIpq a{color:var(--link-color)}.PromptComposer_editorMount__B9G-o .PromptComposer_promptComposerEditor__yQIpq a:hover{cursor:pointer;opacity:.8}.PromptComposer_editorMount__B9G-o .PromptComposer_promptComposerEmptyNode__62Ylm:before{color:var(--muted-foreground);content:attr(data-placeholder);float:left;height:0;opacity:.3;pointer-events:none}";
|
|
2
|
-
var S = {"root":"PromptComposer_root__gfdXN","scroller":"PromptComposer_scroller__Sueif","editorMount":"PromptComposer_editorMount__B9G-o","promptComposerEditor":"PromptComposer_promptComposerEditor__yQIpq","promptComposerLink":"PromptComposer_promptComposerLink__rKhhp","promptComposerEmptyNode":"PromptComposer_promptComposerEmptyNode__62Ylm"};
|
|
1
|
+
var css_248z = ".PromptComposer_root__gfdXN{width:100%}.PromptComposer_scroller__Sueif{flex:1;max-height:200px;min-height:40px;width:100%}.PromptComposer_editorMount__B9G-o{background:transparent;border:none;border-radius:0!important;box-shadow:none!important;display:flex;flex:1;flex-direction:column;min-height:40px;min-width:0;padding:0!important}.PromptComposer_editorMount__B9G-o:focus-within{box-shadow:none!important}.PromptComposer_editorMount__B9G-o .PromptComposer_promptComposerEditor__yQIpq{border:none!important;box-shadow:none!important;flex:1;margin:0;min-height:40px!important;outline:none!important;overflow:hidden!important;padding:var(--p-2) 0 0!important;resize:none!important;transition:opacity .1s ease-out;white-space:pre-wrap;word-break:break-word}.PromptComposer_editorMount__B9G-o .PromptComposer_promptComposerEditor__yQIpq .PromptComposer_promptComposerLink__rKhhp,.PromptComposer_editorMount__B9G-o .PromptComposer_promptComposerEditor__yQIpq a{color:var(--link-color)}.PromptComposer_editorMount__B9G-o .PromptComposer_promptComposerEditor__yQIpq a:hover{cursor:pointer;opacity:.8}.PromptComposer_editorMount__B9G-o .PromptComposer_promptComposerEmptyNode__62Ylm:before{color:var(--muted-foreground);content:attr(data-placeholder);float:left;height:0;opacity:.3;pointer-events:none}.PromptComposer_editorMount__B9G-o .PromptComposer_promptComposerList__PqkM-{list-style:disc;margin:0;padding-left:1.4em}.PromptComposer_editorMount__B9G-o .PromptComposer_promptComposerList__PqkM- li,.PromptComposer_editorMount__B9G-o .PromptComposer_promptComposerList__PqkM- p{margin:0}.PromptComposer_editorMount__B9G-o .PromptComposer_promptComposerQuote__AGoIb{color:var(--accent-color-alpha-600);font-size:85%;margin:0 0 var(--p-1);padding:2px 0 2px calc(4px + var(--p-2));position:relative;white-space:pre-wrap}.PromptComposer_editorMount__B9G-o .PromptComposer_promptComposerQuote__AGoIb:before{background:var(--accent-color);border-radius:2px;bottom:2px;content:\"\";left:2px;opacity:.3;pointer-events:none;position:absolute;top:2px;width:4px}.PromptComposer_editorMount__B9G-o .PromptComposer_promptComposerQuote__AGoIb p{margin:0}.PromptComposer_editorMount__B9G-o div.PromptComposer_promptComposerQuote__AGoIb{cursor:default;-webkit-user-select:none;-moz-user-select:none;user-select:none;white-space:pre-wrap}.PromptComposer_editorMount__B9G-o div.PromptComposer_promptComposerQuote__AGoIb.ProseMirror-selectednode{background:var(--accent-color-alpha-100);border-radius:4px}.PromptComposer_editorMount__B9G-o div.PromptComposer_promptComposerQuote__AGoIb.ProseMirror-selectednode:before{opacity:.7}";
|
|
2
|
+
var S = {"root":"PromptComposer_root__gfdXN","scroller":"PromptComposer_scroller__Sueif","editorMount":"PromptComposer_editorMount__B9G-o","promptComposerEditor":"PromptComposer_promptComposerEditor__yQIpq","promptComposerLink":"PromptComposer_promptComposerLink__rKhhp","promptComposerEmptyNode":"PromptComposer_promptComposerEmptyNode__62Ylm","promptComposerList":"PromptComposer_promptComposerList__PqkM-","promptComposerQuote":"PromptComposer_promptComposerQuote__AGoIb"};
|
|
3
3
|
if (typeof document !== 'undefined') {
|
|
4
4
|
var __uilibHead = document.head || document.getElementsByTagName('head')[0];
|
|
5
5
|
if (__uilibHead) {
|
|
@@ -1,17 +1,6 @@
|
|
|
1
1
|
const PROMPT_COMPOSER_EMPTY_DOC = {
|
|
2
2
|
type: 'doc',
|
|
3
3
|
content: [{ type: 'paragraph' }],
|
|
4
|
-
};
|
|
5
|
-
function promptComposerParagraphDoc(text) {
|
|
6
|
-
return {
|
|
7
|
-
type: 'doc',
|
|
8
|
-
content: [
|
|
9
|
-
{
|
|
10
|
-
type: 'paragraph',
|
|
11
|
-
content: text ? [{ type: 'text', text }] : [],
|
|
12
|
-
},
|
|
13
|
-
],
|
|
14
|
-
};
|
|
15
|
-
}
|
|
4
|
+
};
|
|
16
5
|
|
|
17
|
-
export { PROMPT_COMPOSER_EMPTY_DOC
|
|
6
|
+
export { PROMPT_COMPOSER_EMPTY_DOC };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import '../../tiptap/prompt-blocks/createPromptBlocksExtension.js';
|
|
2
|
+
import { promptBlocksDoc } from '../../tiptap/prompt-blocks/promptBlocksText.js';
|
|
2
3
|
|
|
3
4
|
const PROMPT_COMPOSER_COMMAND_CHIP_CLASS = 'prompt-composer-command-chip';
|
|
4
5
|
function getPromptComposerTokenRangeBeforePos(editor, pos) {
|
|
@@ -70,7 +71,7 @@ function insertPromptComposerContentAtCaret(editor, content, options) {
|
|
|
70
71
|
function setPromptComposerEditorText(editor, text, options) {
|
|
71
72
|
if (editor.isDestroyed)
|
|
72
73
|
return;
|
|
73
|
-
editor.commands.setContent(
|
|
74
|
+
editor.commands.setContent(promptBlocksDoc(text));
|
|
74
75
|
if (options?.focus) {
|
|
75
76
|
editor.commands.focus(options.atEnd === false ? 'start' : 'end');
|
|
76
77
|
}
|
|
@@ -3,12 +3,14 @@ import { Placeholder } from '@tiptap/extensions';
|
|
|
3
3
|
import { useEditor } from '@tiptap/react';
|
|
4
4
|
import StarterKit from '@tiptap/starter-kit';
|
|
5
5
|
import S from './PromptComposer.styl.js';
|
|
6
|
+
import { createPromptBlocksExtension, isInPromptBlock } from '../../tiptap/prompt-blocks/createPromptBlocksExtension.js';
|
|
7
|
+
import { promptBlocksDoc } from '../../tiptap/prompt-blocks/promptBlocksText.js';
|
|
6
8
|
import { createLinkHighlightExtension } from '../../tiptap/link-highlight/createLinkHighlightExtension.js';
|
|
7
9
|
import { NonInclusiveLink } from '../../tiptap/link-highlight/nonInclusiveLink.js';
|
|
8
10
|
import { DEFAULT_CHAT_SLASH_ITEMS } from '../../tiptap/slash-mention/defaultChatSlashItems.js';
|
|
9
11
|
import { createSlashMentionExtension } from '../../tiptap/slash-mention/createSlashMentionExtension.js';
|
|
10
12
|
import { safePromptComposerEditorText, promptComposerSafeEditorDom, syncPromptComposerHeight } from './PromptComposer.helpers.js';
|
|
11
|
-
import { PROMPT_COMPOSER_EMPTY_DOC
|
|
13
|
+
import { PROMPT_COMPOSER_EMPTY_DOC } from './promptComposerDoc.js';
|
|
12
14
|
|
|
13
15
|
function buildMentionConfigs({ slashCommandItems, onSlashItemCommand, mentionConfigs, atMentionItems, atMentionGetItems, onAtItemCommand, }) {
|
|
14
16
|
const configs = [];
|
|
@@ -101,7 +103,7 @@ function usePromptComposerEditor({ disabled, placeholder, slashCommandItems, sla
|
|
|
101
103
|
heading: false,
|
|
102
104
|
bulletList: false,
|
|
103
105
|
orderedList: false,
|
|
104
|
-
blockquote:
|
|
106
|
+
blockquote: { HTMLAttributes: { class: S.promptComposerQuote } },
|
|
105
107
|
codeBlock: false,
|
|
106
108
|
horizontalRule: false,
|
|
107
109
|
link: false,
|
|
@@ -113,6 +115,10 @@ function usePromptComposerEditor({ disabled, placeholder, slashCommandItems, sla
|
|
|
113
115
|
emptyNodeClass: S.promptComposerEmptyNode,
|
|
114
116
|
}),
|
|
115
117
|
createLinkHighlightExtension(S.promptComposerLink),
|
|
118
|
+
...createPromptBlocksExtension({
|
|
119
|
+
quoteClassName: S.promptComposerQuote,
|
|
120
|
+
listClassName: S.promptComposerList,
|
|
121
|
+
}),
|
|
116
122
|
];
|
|
117
123
|
for (const cfg of resolvedMentionConfigs) {
|
|
118
124
|
const slashChar = cfg.slashChar;
|
|
@@ -180,7 +186,8 @@ function usePromptComposerEditor({ disabled, placeholder, slashCommandItems, sla
|
|
|
180
186
|
if (!activeEditor)
|
|
181
187
|
return false;
|
|
182
188
|
const currentText = activeEditor.getText();
|
|
183
|
-
|
|
189
|
+
// In a list or a quote being typed, Enter adds the next line.
|
|
190
|
+
const hasNewlines = currentText.includes('\n') || isInPromptBlock(activeEditor.state);
|
|
184
191
|
const isModEnter = event.metaKey || event.ctrlKey;
|
|
185
192
|
// Single line: Enter submits. Multiline: Enter inserts a line; Cmd/Ctrl+Enter submits.
|
|
186
193
|
if (hasNewlines ? !isModEnter : isModEnter)
|
|
@@ -249,13 +256,13 @@ function usePromptComposerEditor({ disabled, placeholder, slashCommandItems, sla
|
|
|
249
256
|
const next = prefillMessage;
|
|
250
257
|
if (current === next)
|
|
251
258
|
return;
|
|
259
|
+
// The app trims the prompt on blur, and a list or quote at the end always
|
|
260
|
+
// has an empty line after it. Resetting for that would refocus the editor
|
|
261
|
+
// and turn an empty last item into text.
|
|
262
|
+
if (next !== '' && next === current.trim())
|
|
263
|
+
return;
|
|
252
264
|
try {
|
|
253
|
-
|
|
254
|
-
editor.commands.setContent(PROMPT_COMPOSER_EMPTY_DOC);
|
|
255
|
-
}
|
|
256
|
-
else {
|
|
257
|
-
editor.commands.setContent(promptComposerParagraphDoc(next));
|
|
258
|
-
}
|
|
265
|
+
editor.commands.setContent(promptBlocksDoc(next));
|
|
259
266
|
}
|
|
260
267
|
catch {
|
|
261
268
|
return;
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { wrappingInputRule, Extension, Node, getTextBetween, getTextSerializersFromSchema } from '@tiptap/core';
|
|
2
|
+
import { BulletList } from '@tiptap/extension-list';
|
|
3
|
+
import { PluginKey, Plugin } from '@tiptap/pm/state';
|
|
4
|
+
import { promptQuoteMarkdown, PROMPT_QUOTE_NODE } from './promptBlocksText.js';
|
|
5
|
+
|
|
6
|
+
const serializersBySchema = new WeakMap();
|
|
7
|
+
function textSerializers(schema) {
|
|
8
|
+
let serializers = serializersBySchema.get(schema);
|
|
9
|
+
if (!serializers) {
|
|
10
|
+
serializers = getTextSerializersFromSchema(schema);
|
|
11
|
+
serializersBySchema.set(schema, serializers);
|
|
12
|
+
}
|
|
13
|
+
return serializers;
|
|
14
|
+
}
|
|
15
|
+
/** Text of a textblock, with mention chips and hard breaks as text. */
|
|
16
|
+
function inlineText(node, schema) {
|
|
17
|
+
return getTextBetween(node, { from: 0, to: node.content.size }, { blockSeparator: '\n', textSerializers: textSerializers(schema) });
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Lines of a (draft) blockquote: one per paragraph, a pasted quote's own
|
|
21
|
+
* lines, and list items as `- ` lines.
|
|
22
|
+
*/
|
|
23
|
+
function blockquoteLines(node, schema) {
|
|
24
|
+
const lines = [];
|
|
25
|
+
node.forEach(child => {
|
|
26
|
+
if (child.type.name === PROMPT_QUOTE_NODE) {
|
|
27
|
+
lines.push(String(child.attrs.text ?? ''));
|
|
28
|
+
}
|
|
29
|
+
else if (child.type.name === 'bulletList') {
|
|
30
|
+
lines.push(bulletListMarkdown(child, schema));
|
|
31
|
+
}
|
|
32
|
+
else if (child.isTextblock) {
|
|
33
|
+
lines.push(inlineText(child, schema));
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
lines.push(blockquoteLines(child, schema));
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
return lines.join('\n');
|
|
40
|
+
}
|
|
41
|
+
/** Text of one block: textblocks inline, others by their own serializer. */
|
|
42
|
+
function blockText(node, schema) {
|
|
43
|
+
if (node.isTextblock)
|
|
44
|
+
return inlineText(node, schema);
|
|
45
|
+
const toText = textSerializers(schema)[node.type.name];
|
|
46
|
+
return toText ? toText({ node }) : node.textContent;
|
|
47
|
+
}
|
|
48
|
+
function bulletListMarkdown(list, schema, indent = '') {
|
|
49
|
+
const lines = [];
|
|
50
|
+
list.forEach(item => {
|
|
51
|
+
const itemLines = [];
|
|
52
|
+
let hasText = false;
|
|
53
|
+
item.forEach(child => {
|
|
54
|
+
if (child.type.name === 'bulletList') {
|
|
55
|
+
const nested = bulletListMarkdown(child, schema, `${indent} `);
|
|
56
|
+
if (nested)
|
|
57
|
+
itemLines.push(nested);
|
|
58
|
+
hasText ||= Boolean(nested);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const text = blockText(child, schema);
|
|
62
|
+
hasText ||= Boolean(text.trim());
|
|
63
|
+
const prefix = itemLines.length ? `${indent} ` : `${indent}- `;
|
|
64
|
+
itemLines.push(text
|
|
65
|
+
.split('\n')
|
|
66
|
+
.map((line, i) => (i === 0 ? prefix + line : `${indent} ${line}`))
|
|
67
|
+
.join('\n'));
|
|
68
|
+
});
|
|
69
|
+
// An empty item is not text yet: `- ` alone must not count as a prompt.
|
|
70
|
+
if (hasText)
|
|
71
|
+
lines.push(...itemLines);
|
|
72
|
+
});
|
|
73
|
+
return lines.join('\n');
|
|
74
|
+
}
|
|
75
|
+
/** Only `- ` starts a list: `* ` and `+ ` stay text, as getText() writes `- `. */
|
|
76
|
+
const DashBulletList = BulletList.extend({
|
|
77
|
+
addInputRules() {
|
|
78
|
+
return [wrappingInputRule({ find: /^\s*-\s$/, type: this.type })];
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
/** The innermost of `names` the position is in, if any. */
|
|
82
|
+
function promptBlockAt($pos, names = ['listItem', 'blockquote']) {
|
|
83
|
+
for (let d = $pos.depth; d > 0; d -= 1) {
|
|
84
|
+
const name = $pos.node(d).type.name;
|
|
85
|
+
if (names.includes(name))
|
|
86
|
+
return name;
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
const inQuote = ($pos) => !!promptBlockAt($pos, ['blockquote']);
|
|
91
|
+
/**
|
|
92
|
+
* Locked quote: a read-only block holding the quoted text. It can be
|
|
93
|
+
* selected and deleted, not edited.
|
|
94
|
+
*/
|
|
95
|
+
function createPromptQuoteNode(className) {
|
|
96
|
+
return Node.create({
|
|
97
|
+
name: PROMPT_QUOTE_NODE,
|
|
98
|
+
group: 'block',
|
|
99
|
+
atom: true,
|
|
100
|
+
selectable: true,
|
|
101
|
+
draggable: false,
|
|
102
|
+
addAttributes() {
|
|
103
|
+
return {
|
|
104
|
+
text: {
|
|
105
|
+
default: '',
|
|
106
|
+
parseHTML: el => el.textContent ?? '',
|
|
107
|
+
rendered: false,
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
},
|
|
111
|
+
parseHTML() {
|
|
112
|
+
return [{ tag: 'div[data-prompt-quote]' }];
|
|
113
|
+
},
|
|
114
|
+
renderHTML({ node }) {
|
|
115
|
+
return [
|
|
116
|
+
'div',
|
|
117
|
+
{
|
|
118
|
+
'data-prompt-quote': '',
|
|
119
|
+
contenteditable: 'false',
|
|
120
|
+
...(className ? { class: className } : {}),
|
|
121
|
+
},
|
|
122
|
+
String(node.attrs.text ?? ''),
|
|
123
|
+
];
|
|
124
|
+
},
|
|
125
|
+
renderText({ node }) {
|
|
126
|
+
return promptQuoteMarkdown(String(node.attrs.text ?? ''));
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
const lockQuotesKey = new PluginKey('promptLockQuotes');
|
|
131
|
+
/**
|
|
132
|
+
* Markdown-style lines in the prompt composer:
|
|
133
|
+
* - `- ` at the start of a line starts a bullet list (StarterKit's list);
|
|
134
|
+
* - `> ` starts a quote that stays editable while the caret is in it and
|
|
135
|
+
* turns into a read-only quote block once the caret leaves.
|
|
136
|
+
*
|
|
137
|
+
* `getText()` gives both back as markdown (`- item`, `> line`).
|
|
138
|
+
* Brings its own bullet list: use StarterKit with `bulletList: false` and
|
|
139
|
+
* `listItem` and `blockquote` enabled.
|
|
140
|
+
*/
|
|
141
|
+
function createPromptBlocksExtension({ quoteClassName, listClassName, } = {}) {
|
|
142
|
+
const PromptBlocks = Extension.create({
|
|
143
|
+
name: 'promptBlocks',
|
|
144
|
+
extendNodeSchema(extension) {
|
|
145
|
+
if (extension.name === 'bulletList') {
|
|
146
|
+
return {
|
|
147
|
+
toText: ({ node }) => bulletListMarkdown(node, node.type.schema),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
if (extension.name === 'blockquote') {
|
|
151
|
+
return {
|
|
152
|
+
toText: ({ node }) => promptQuoteMarkdown(blockquoteLines(node, node.type.schema)),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
return {};
|
|
156
|
+
},
|
|
157
|
+
addProseMirrorPlugins() {
|
|
158
|
+
const { editor } = this;
|
|
159
|
+
return [
|
|
160
|
+
new Plugin({
|
|
161
|
+
key: lockQuotesKey,
|
|
162
|
+
appendTransaction: (transactions, oldState, state) => {
|
|
163
|
+
const docChanged = transactions.some(tr => tr.docChanged);
|
|
164
|
+
if (!docChanged && !transactions.some(tr => tr.selectionSet)) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
// A caret move can only leave a quote it was in.
|
|
168
|
+
if (!docChanged &&
|
|
169
|
+
!inQuote(oldState.selection.$from) &&
|
|
170
|
+
!inQuote(oldState.selection.$to)) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
const quoteType = state.schema.nodes[PROMPT_QUOTE_NODE];
|
|
174
|
+
const paragraphType = state.schema.nodes.paragraph;
|
|
175
|
+
if (!quoteType || !paragraphType)
|
|
176
|
+
return null;
|
|
177
|
+
const { from, to } = state.selection;
|
|
178
|
+
const drafts = [];
|
|
179
|
+
state.doc.descendants((node, pos) => {
|
|
180
|
+
// Quotes sit at the top level or in list items, never inline.
|
|
181
|
+
if (node.type.name !== 'blockquote')
|
|
182
|
+
return !node.isTextblock;
|
|
183
|
+
const end = pos + node.nodeSize;
|
|
184
|
+
// Only once the selection is fully outside: a range reaching
|
|
185
|
+
// into the quote (Shift+Arrow, drag, select all) keeps it.
|
|
186
|
+
if (to < pos || from > end)
|
|
187
|
+
drafts.push({ pos, node });
|
|
188
|
+
return false;
|
|
189
|
+
});
|
|
190
|
+
if (drafts.length === 0)
|
|
191
|
+
return null;
|
|
192
|
+
const tr = state.tr;
|
|
193
|
+
// Right to left, so earlier positions stay valid.
|
|
194
|
+
for (const { pos, node } of drafts.reverse()) {
|
|
195
|
+
const text = blockquoteLines(node, state.schema);
|
|
196
|
+
const replacement = text.trim()
|
|
197
|
+
? quoteType.create({ text })
|
|
198
|
+
: paragraphType.create();
|
|
199
|
+
tr.replaceWith(pos, pos + node.nodeSize, replacement);
|
|
200
|
+
}
|
|
201
|
+
// In undo history with the edit that left the quote, so undo
|
|
202
|
+
// takes back the typing and the lock together.
|
|
203
|
+
return tr;
|
|
204
|
+
},
|
|
205
|
+
props: {
|
|
206
|
+
// StarterKit's `- ` / `> ` rules need the start of a paragraph;
|
|
207
|
+
// this also takes a line started with Shift+Enter (a hard break).
|
|
208
|
+
handleTextInput: (view, from, to, text) => {
|
|
209
|
+
if (text !== ' ' || from !== to || view.composing)
|
|
210
|
+
return false;
|
|
211
|
+
const { doc } = view.state;
|
|
212
|
+
if (from < 2)
|
|
213
|
+
return false;
|
|
214
|
+
const marker = doc.textBetween(from - 1, from);
|
|
215
|
+
if (marker !== '>' && marker !== '-')
|
|
216
|
+
return false;
|
|
217
|
+
const $marker = doc.resolve(from - 1);
|
|
218
|
+
if ($marker.parent.type.name !== 'paragraph')
|
|
219
|
+
return false;
|
|
220
|
+
if ($marker.nodeBefore?.type.name !== 'hardBreak')
|
|
221
|
+
return false;
|
|
222
|
+
const inBlock = promptBlockAt($marker);
|
|
223
|
+
// `> ` in a quote or list stays text; `- ` in a list is the next
|
|
224
|
+
// item, in a quote it starts a list there.
|
|
225
|
+
if (inBlock && marker === '>')
|
|
226
|
+
return false;
|
|
227
|
+
const start = (chain) => {
|
|
228
|
+
const run = chain.deleteRange({ from: from - 2, to: from });
|
|
229
|
+
if (inBlock === 'listItem')
|
|
230
|
+
return run.splitListItem('listItem');
|
|
231
|
+
run.splitBlock();
|
|
232
|
+
return marker === '>'
|
|
233
|
+
? run.wrapIn('blockquote')
|
|
234
|
+
: run.toggleBulletList();
|
|
235
|
+
};
|
|
236
|
+
// Handled once the doc changed, even if a later step failed:
|
|
237
|
+
// the marker is gone, so the default insert would land wrong.
|
|
238
|
+
const before = view.state.doc;
|
|
239
|
+
start(editor.chain()).run();
|
|
240
|
+
return view.state.doc !== before;
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
}),
|
|
244
|
+
];
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
return [
|
|
248
|
+
createPromptQuoteNode(quoteClassName),
|
|
249
|
+
DashBulletList.configure({
|
|
250
|
+
HTMLAttributes: listClassName ? { class: listClassName } : {},
|
|
251
|
+
}),
|
|
252
|
+
PromptBlocks,
|
|
253
|
+
];
|
|
254
|
+
}
|
|
255
|
+
/** True when the caret is in a bullet list or a quote being typed. */
|
|
256
|
+
function isInPromptBlock(state) {
|
|
257
|
+
return promptBlockAt(state.selection.$from) !== null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export { createPromptBlocksExtension, isInPromptBlock };
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/** Node name of a locked (read-only) quote block. */
|
|
2
|
+
const PROMPT_QUOTE_NODE = 'promptQuote';
|
|
3
|
+
const QUOTE_LINE = /^>(?: (.*)|)$/;
|
|
4
|
+
const TOP_BULLET_LINE = /^- (.*)$/;
|
|
5
|
+
const BULLET_LINE = /^( *)- (.*)$/;
|
|
6
|
+
/** Indented line under a list item: a nested item or a continued line. */
|
|
7
|
+
const LIST_CHILD_LINE = /^ {2,}\S/;
|
|
8
|
+
/**
|
|
9
|
+
* Markdown quote of `text`: `> ` per line, `>` for a blank line, nothing for
|
|
10
|
+
* an empty quote (so `> ` alone does not count as a prompt).
|
|
11
|
+
*/
|
|
12
|
+
function promptQuoteMarkdown(text) {
|
|
13
|
+
if (!text.trim())
|
|
14
|
+
return '';
|
|
15
|
+
return text
|
|
16
|
+
.split('\n')
|
|
17
|
+
.map(line => (line.trim() ? `> ${line}` : '>'))
|
|
18
|
+
.join('\n');
|
|
19
|
+
}
|
|
20
|
+
const textContent = (text) => text ? [{ type: 'text', text }] : [];
|
|
21
|
+
const paragraph = (text) => ({
|
|
22
|
+
type: 'paragraph',
|
|
23
|
+
content: textContent(text),
|
|
24
|
+
});
|
|
25
|
+
/** Bullet groups keep their raw lines (indents included) for buildList. */
|
|
26
|
+
function groupLines(lines) {
|
|
27
|
+
const groups = [];
|
|
28
|
+
for (const line of lines) {
|
|
29
|
+
const last = groups[groups.length - 1];
|
|
30
|
+
const quote = line.match(QUOTE_LINE);
|
|
31
|
+
const inList = last?.kind === 'bullet';
|
|
32
|
+
const bullet = !quote &&
|
|
33
|
+
(TOP_BULLET_LINE.test(line) || (inList && LIST_CHILD_LINE.test(line)));
|
|
34
|
+
const kind = quote ? 'quote' : bullet ? 'bullet' : 'plain';
|
|
35
|
+
const value = quote ? (quote[1] ?? '') : line;
|
|
36
|
+
if (last && last.kind === kind)
|
|
37
|
+
last.lines.push(value);
|
|
38
|
+
else
|
|
39
|
+
groups.push({ kind, lines: [value] });
|
|
40
|
+
}
|
|
41
|
+
return groups;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Bullet list from `- item` lines at `indent` spaces. A `- ` line two
|
|
45
|
+
* spaces deeper starts a nested list; other deeper lines continue the
|
|
46
|
+
* item's text (what getText() writes for a multi-line item).
|
|
47
|
+
*/
|
|
48
|
+
function buildList(lines, indent = 0) {
|
|
49
|
+
const items = [];
|
|
50
|
+
for (const line of lines) {
|
|
51
|
+
const bullet = line.match(BULLET_LINE);
|
|
52
|
+
const item = items[items.length - 1];
|
|
53
|
+
if ((bullet && bullet[1].length === indent) || !item) {
|
|
54
|
+
items.push({ text: bullet ? bullet[2] : line.trim(), parts: [] });
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const lineIndent = line.match(/^ */)[0].length;
|
|
58
|
+
const last = item.parts[item.parts.length - 1];
|
|
59
|
+
const startsNested = bullet && lineIndent === indent + 2;
|
|
60
|
+
if (startsNested || (last && 'nested' in last && lineIndent > indent + 2)) {
|
|
61
|
+
if (last && 'nested' in last)
|
|
62
|
+
last.nested.push(line);
|
|
63
|
+
else
|
|
64
|
+
item.parts.push({ nested: [line] });
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const value = line.slice(Math.min(lineIndent, indent + 2));
|
|
68
|
+
// Text at the item's own indent continues it, after a nested list too.
|
|
69
|
+
if (!last)
|
|
70
|
+
item.text += `\n${value}`;
|
|
71
|
+
else if ('text' in last)
|
|
72
|
+
last.text += `\n${value}`;
|
|
73
|
+
else
|
|
74
|
+
item.parts.push({ text: value });
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
type: 'bulletList',
|
|
78
|
+
content: items.map(item => ({
|
|
79
|
+
type: 'listItem',
|
|
80
|
+
content: [
|
|
81
|
+
paragraph(item.text),
|
|
82
|
+
...item.parts.map(part => 'nested' in part
|
|
83
|
+
? buildList(part.nested, indent + 2)
|
|
84
|
+
: paragraph(part.text)),
|
|
85
|
+
],
|
|
86
|
+
})),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Blocks for plain `text` with `> ` lines as locked quotes and `- ` lines as
|
|
91
|
+
* a bullet list. `getText()` of the result gives `text` back (blocks are
|
|
92
|
+
* joined by a blank line, list items by a newline).
|
|
93
|
+
*
|
|
94
|
+
* Other lines stay in one paragraph with newlines, as the composer prefill
|
|
95
|
+
* always worked. A doc never ends on a quote or list: an empty line follows,
|
|
96
|
+
* so the caret can go there instead of selecting the read-only quote.
|
|
97
|
+
*/
|
|
98
|
+
function promptBlocksFromText(text) {
|
|
99
|
+
const groups = groupLines(text.replace(/\r\n?/g, '\n').split('\n'));
|
|
100
|
+
const blocks = [];
|
|
101
|
+
groups.forEach((group, i) => {
|
|
102
|
+
if (group.kind === 'quote') {
|
|
103
|
+
// A bare `>` quotes nothing: keep it as an empty line.
|
|
104
|
+
if (!group.lines.join('').trim()) {
|
|
105
|
+
blocks.push(paragraph(''));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
blocks.push({
|
|
109
|
+
type: PROMPT_QUOTE_NODE,
|
|
110
|
+
attrs: { text: group.lines.join('\n') },
|
|
111
|
+
});
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (group.kind === 'bullet') {
|
|
115
|
+
const list = buildList(group.lines);
|
|
116
|
+
// `- a`, blank line, `- b` is one list, as in markdown.
|
|
117
|
+
const prev = blocks[blocks.length - 1];
|
|
118
|
+
if (prev?.type === 'bulletList') {
|
|
119
|
+
prev.content = [...(prev.content ?? []), ...(list.content ?? [])];
|
|
120
|
+
}
|
|
121
|
+
else
|
|
122
|
+
blocks.push(list);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
// The blank line next to a quote or list is the block separator.
|
|
126
|
+
const lines = [...group.lines];
|
|
127
|
+
if (i > 0 && lines[0] === '')
|
|
128
|
+
lines.shift();
|
|
129
|
+
if (i < groups.length - 1 && lines[lines.length - 1] === '')
|
|
130
|
+
lines.pop();
|
|
131
|
+
if (lines.length === 0)
|
|
132
|
+
return;
|
|
133
|
+
blocks.push(paragraph(lines.join('\n')));
|
|
134
|
+
});
|
|
135
|
+
if (blocks[blocks.length - 1]?.type !== 'paragraph')
|
|
136
|
+
blocks.push(paragraph(''));
|
|
137
|
+
return blocks;
|
|
138
|
+
}
|
|
139
|
+
function promptBlocksDoc(text) {
|
|
140
|
+
return { type: 'doc', content: promptBlocksFromText(text) };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export { PROMPT_QUOTE_NODE, promptBlocksDoc, promptBlocksFromText, promptQuoteMarkdown };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Extension, Node } from '@tiptap/core';
|
|
2
|
+
import type { ResolvedPos } from '@tiptap/pm/model';
|
|
3
|
+
export type CreatePromptBlocksExtensionOptions = {
|
|
4
|
+
/** Class of a locked quote and of a quote being typed. */
|
|
5
|
+
quoteClassName?: string;
|
|
6
|
+
/** Class of a bullet list. */
|
|
7
|
+
listClassName?: string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Markdown-style lines in the prompt composer:
|
|
11
|
+
* - `- ` at the start of a line starts a bullet list (StarterKit's list);
|
|
12
|
+
* - `> ` starts a quote that stays editable while the caret is in it and
|
|
13
|
+
* turns into a read-only quote block once the caret leaves.
|
|
14
|
+
*
|
|
15
|
+
* `getText()` gives both back as markdown (`- item`, `> line`).
|
|
16
|
+
* Brings its own bullet list: use StarterKit with `bulletList: false` and
|
|
17
|
+
* `listItem` and `blockquote` enabled.
|
|
18
|
+
*/
|
|
19
|
+
export declare function createPromptBlocksExtension({ quoteClassName, listClassName, }?: CreatePromptBlocksExtensionOptions): (Node<any, any> | Extension<any, any>)[];
|
|
20
|
+
/** True when the caret is in a bullet list or a quote being typed. */
|
|
21
|
+
export declare function isInPromptBlock(state: {
|
|
22
|
+
selection: {
|
|
23
|
+
$from: ResolvedPos;
|
|
24
|
+
};
|
|
25
|
+
}): boolean;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { createPromptBlocksExtension, isInPromptBlock, } from './createPromptBlocksExtension';
|
|
2
|
+
export type { CreatePromptBlocksExtensionOptions } from './createPromptBlocksExtension';
|
|
3
|
+
export { PROMPT_QUOTE_NODE, promptBlocksDoc, promptBlocksFromText, promptQuoteMarkdown, } from './promptBlocksText';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { JSONContent } from '@tiptap/core';
|
|
2
|
+
/** Node name of a locked (read-only) quote block. */
|
|
3
|
+
export declare const PROMPT_QUOTE_NODE = "promptQuote";
|
|
4
|
+
/**
|
|
5
|
+
* Markdown quote of `text`: `> ` per line, `>` for a blank line, nothing for
|
|
6
|
+
* an empty quote (so `> ` alone does not count as a prompt).
|
|
7
|
+
*/
|
|
8
|
+
export declare function promptQuoteMarkdown(text: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Blocks for plain `text` with `> ` lines as locked quotes and `- ` lines as
|
|
11
|
+
* a bullet list. `getText()` of the result gives `text` back (blocks are
|
|
12
|
+
* joined by a blank line, list items by a newline).
|
|
13
|
+
*
|
|
14
|
+
* Other lines stay in one paragraph with newlines, as the composer prefill
|
|
15
|
+
* always worked. A doc never ends on a quote or list: an empty line follows,
|
|
16
|
+
* so the caret can go there instead of selecting the read-only quote.
|
|
17
|
+
*/
|
|
18
|
+
export declare function promptBlocksFromText(text: string): JSONContent[];
|
|
19
|
+
export declare function promptBlocksDoc(text: string): JSONContent;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homecode/ui",
|
|
3
|
-
"version": "5.20.
|
|
3
|
+
"version": "5.20.20",
|
|
4
4
|
"description": "React UI components library",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"tests": "jest",
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
"dependencies": {
|
|
62
62
|
"@tiptap/core": "^3.30.5",
|
|
63
63
|
"@tiptap/extension-link": "^3.30.5",
|
|
64
|
+
"@tiptap/extension-list": "^3.30.5",
|
|
64
65
|
"@tiptap/extension-mention": "^3.30.5",
|
|
65
66
|
"@tiptap/extensions": "^3.30.5",
|
|
66
67
|
"@tiptap/pm": "^3.30.5",
|