@homecode/ui 5.20.19 → 5.20.21
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/index.js +1 -1
- package/dist/esm/src/components/Autocomplete/Autocomplete.js +1 -0
- package/dist/esm/src/components/FormattedText/FormattedText.js +31 -5
- package/dist/esm/src/components/FormattedText/FormattedText.styl.js +2 -2
- 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/promptComposerPlaceholder.js +10 -0
- package/dist/esm/src/components/PromptComposer/usePromptComposerEditor.js +18 -10
- 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/FormattedText/FormattedText.d.ts +2 -1
- package/dist/esm/types/src/components/FormattedText/FormattedText.types.d.ts +10 -0
- package/dist/esm/types/src/components/FormattedText/index.d.ts +2 -2
- package/dist/esm/types/src/components/PromptComposer/promptComposerDoc.d.ts +0 -1
- package/dist/esm/types/src/components/PromptComposer/promptComposerPlaceholder.d.ts +9 -0
- package/dist/esm/types/src/components/PromptComposer/promptComposerPlaceholder.test.d.ts +1 -0
- 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
package/dist/esm/index.js
CHANGED
|
@@ -24,7 +24,7 @@ export { Flex } from './src/components/Flex/Flex.js';
|
|
|
24
24
|
export { FoldedLine } from './src/components/FoldedLine/FoldedLine.js';
|
|
25
25
|
export { FoldedLineAction, FoldedLineActionsProvider, useFoldedLineActionsRegister } from './src/components/FoldedLine/FoldedLineActionsContext.js';
|
|
26
26
|
export { Form } from './src/components/Form/Form.js';
|
|
27
|
-
export { FormattedText } from './src/components/FormattedText/FormattedText.js';
|
|
27
|
+
export { FormattedText, findJsonEndExclusive } from './src/components/FormattedText/FormattedText.js';
|
|
28
28
|
export { Gap } from './src/components/Gap/Gap.js';
|
|
29
29
|
export { Gallery, normalizeGalleryItem } from './src/components/Gallery/Gallery.js';
|
|
30
30
|
export { GridLayout } from './src/components/GridLayout/GridLayout.js';
|
|
@@ -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';
|
|
@@ -108,7 +108,24 @@ function applyInjectors(text, injectors) {
|
|
|
108
108
|
});
|
|
109
109
|
return result;
|
|
110
110
|
}
|
|
111
|
-
function FormattedText({ text, className, onButtonClick, }) {
|
|
111
|
+
function FormattedText({ text, className, onButtonClick, quotes, injectors, breaks = true, }) {
|
|
112
|
+
// Without `<br>`s the text keeps its own newlines (pre-wrap), so a block, such
|
|
113
|
+
// as a table, swallows the blank lines after it instead of showing them.
|
|
114
|
+
const blankLinesAfter = (content, from) => breaks ? 0 : content.slice(from).match(/^\s*/)[0].length;
|
|
115
|
+
/** A run of `> ` lines, grayed with a bar down its left side. */
|
|
116
|
+
const injectQuote = (content) => {
|
|
117
|
+
const matches = content.match(/(?:^|\n)(>[^\n]*(?:\n>[^\n]*)*)(?:\n|$)/);
|
|
118
|
+
if (!matches)
|
|
119
|
+
return null;
|
|
120
|
+
const lines = matches[1]
|
|
121
|
+
.split('\n')
|
|
122
|
+
.map(line => line.replace(/^>[ \t]?/, ''));
|
|
123
|
+
return {
|
|
124
|
+
elem: (jsx("div", { className: S.quote, "data-quote": true, children: lines.join('\n') })),
|
|
125
|
+
index: matches.index,
|
|
126
|
+
length: matches[0].length,
|
|
127
|
+
};
|
|
128
|
+
};
|
|
112
129
|
const injectLines = (content) => {
|
|
113
130
|
const matches = content.match(/(^|\n)---[ \t]*(?:\n|$)/);
|
|
114
131
|
if (!matches)
|
|
@@ -266,6 +283,7 @@ function FormattedText({ text, className, onButtonClick, }) {
|
|
|
266
283
|
const headingTags = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
|
|
267
284
|
const processCellContent = (value, depth = 0) => applyInjectors(value, [
|
|
268
285
|
injectCodeBlock,
|
|
286
|
+
...(injectors ?? []),
|
|
269
287
|
injectPrettyJson,
|
|
270
288
|
...(depth === 0
|
|
271
289
|
? [
|
|
@@ -291,7 +309,10 @@ function FormattedText({ text, className, onButtonClick, }) {
|
|
|
291
309
|
injectButtons,
|
|
292
310
|
]);
|
|
293
311
|
const injectHeading = (content) => {
|
|
294
|
-
|
|
312
|
+
// Without `<br>`s the text keeps its own newlines (pre-wrap), so a heading,
|
|
313
|
+
// which is a block, swallows the blank lines around it instead of leaving
|
|
314
|
+
// an extra empty line.
|
|
315
|
+
const matches = content.match(breaks ? /(^|\n)(#{1,6})\s+([^\n]+)/ : /(^|\n+)(#{1,6})[ \t]+([^\n]+)\n*/);
|
|
295
316
|
if (!matches)
|
|
296
317
|
return null;
|
|
297
318
|
const Tag = headingTags[Math.min(matches[2].length, 6) - 1];
|
|
@@ -350,13 +371,18 @@ function FormattedText({ text, className, onButtonClick, }) {
|
|
|
350
371
|
return {
|
|
351
372
|
elem: (jsx(Table, { size: "s", variant: "plain", columns: columns, data: tableData, className: S.table })),
|
|
352
373
|
index: matches.index + (matches[1]?.length || 0),
|
|
353
|
-
length: matches[0].length -
|
|
374
|
+
length: matches[0].length -
|
|
375
|
+
(matches[1]?.length || 0) +
|
|
376
|
+
blankLinesAfter(content, matches.index + matches[0].length),
|
|
354
377
|
};
|
|
355
378
|
};
|
|
356
379
|
return (jsx("div", { className: className, children: applyInjectors(text, [
|
|
357
380
|
injectTables,
|
|
358
381
|
injectCodeBlock,
|
|
382
|
+
...(injectors ?? []),
|
|
359
383
|
injectPrettyJson,
|
|
384
|
+
// After code, so a `> ` line inside a code block stays code.
|
|
385
|
+
...(quotes ? [injectQuote] : []),
|
|
360
386
|
injectHeading,
|
|
361
387
|
injectLines,
|
|
362
388
|
injectLinks,
|
|
@@ -365,8 +391,8 @@ function FormattedText({ text, className, onButtonClick, }) {
|
|
|
365
391
|
injectBullet,
|
|
366
392
|
injectNumbered,
|
|
367
393
|
injectButtons,
|
|
368
|
-
injectBreaks,
|
|
394
|
+
...(breaks ? [injectBreaks] : []),
|
|
369
395
|
]) }));
|
|
370
396
|
}
|
|
371
397
|
|
|
372
|
-
export { FormattedText, hrefForFormattedLink };
|
|
398
|
+
export { FormattedText, findJsonEndExclusive, hrefForFormattedLink };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
var css_248z = ".FormattedText_bullet__vbqlq{display:inline-block;margin-bottom:2px;margin-left:var(--p-2);margin-right:2px}.FormattedText_bullet__vbqlq:before{background-color:var(--accent-color);border-radius:50%;content:\"\";display:block;height:6px;margin:0 4px;width:6px}.FormattedText_numberedIndex__rZ6Kt{font-feature-settings:\"tnum\";color:var(--accent-color);display:inline-block;font-variant-numeric:tabular-nums;margin-left:var(--p-2);margin-right:6px;min-width:1.25em}.FormattedText_line__c2bmE{background-color:var(--accent-color-alpha-200);border-radius:2px;height:2px;margin:var(--p-2) 0;width:100%}.FormattedText_codeBlock__Xrbvn,.FormattedText_codeInline__jCbNL{background-color:var(--decent-color-alpha-400);border-radius:var(--p-2);box-shadow:0 1px 1px var(--accent-color-alpha-50),inset 0 1px 1px var(--decent-color-alpha-500);font-family:monospace;-webkit-user-select:text;-moz-user-select:text;user-select:text}.FormattedText_codeInline__jCbNL{border-radius:3px;display:inline;font-size:90%;padding:0 4px}.FormattedText_codeBlock__Xrbvn,.FormattedText_jsonPre__qOGmo{display:block;font-size:90%;margin:var(--p-2) 0;overflow-x:auto;padding:var(--p-2);white-space:pre-wrap}.FormattedText_jsonPre__qOGmo{background-color:var(--decent-color-alpha-300);border-radius:var(--p-2);box-shadow:0 1px 1px var(--accent-color-alpha-50),inset 0 1px 1px var(--accent-color-alpha-50);font-family:monospace;max-width:100%;-webkit-user-select:text;-moz-user-select:text;user-select:text}.FormattedText_table__ipMMS{margin-bottom:10px}";
|
|
2
|
-
var S = {"bullet":"FormattedText_bullet__vbqlq","numberedIndex":"FormattedText_numberedIndex__rZ6Kt","line":"FormattedText_line__c2bmE","codeInline":"FormattedText_codeInline__jCbNL","codeBlock":"FormattedText_codeBlock__Xrbvn","jsonPre":"FormattedText_jsonPre__qOGmo","table":"FormattedText_table__ipMMS"};
|
|
1
|
+
var css_248z = ".FormattedText_bullet__vbqlq{display:inline-block;margin-bottom:2px;margin-left:var(--p-2);margin-right:2px}.FormattedText_bullet__vbqlq:before{background-color:var(--accent-color);border-radius:50%;content:\"\";display:block;height:6px;margin:0 4px;width:6px}.FormattedText_numberedIndex__rZ6Kt{font-feature-settings:\"tnum\";color:var(--accent-color);display:inline-block;font-variant-numeric:tabular-nums;margin-left:var(--p-2);margin-right:6px;min-width:1.25em}.FormattedText_quote__BBm-M{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}.FormattedText_quote__BBm-M:before{background-color:var(--accent-color);border-radius:2px;bottom:2px;content:\"\";left:2px;opacity:.3;position:absolute;top:2px;width:4px}.FormattedText_line__c2bmE{background-color:var(--accent-color-alpha-200);border-radius:2px;height:2px;margin:var(--p-2) 0;width:100%}.FormattedText_codeBlock__Xrbvn,.FormattedText_codeInline__jCbNL{background-color:var(--decent-color-alpha-400);border-radius:var(--p-2);box-shadow:0 1px 1px var(--accent-color-alpha-50),inset 0 1px 1px var(--decent-color-alpha-500);font-family:monospace;-webkit-user-select:text;-moz-user-select:text;user-select:text}.FormattedText_codeInline__jCbNL{border-radius:3px;display:inline;font-size:90%;padding:0 4px}.FormattedText_codeBlock__Xrbvn,.FormattedText_jsonPre__qOGmo{display:block;font-size:90%;margin:var(--p-2) 0;overflow-x:auto;padding:var(--p-2);white-space:pre-wrap}.FormattedText_jsonPre__qOGmo{background-color:var(--decent-color-alpha-300);border-radius:var(--p-2);box-shadow:0 1px 1px var(--accent-color-alpha-50),inset 0 1px 1px var(--accent-color-alpha-50);font-family:monospace;max-width:100%;-webkit-user-select:text;-moz-user-select:text;user-select:text}.FormattedText_table__ipMMS{margin-bottom:10px}";
|
|
2
|
+
var S = {"bullet":"FormattedText_bullet__vbqlq","numberedIndex":"FormattedText_numberedIndex__rZ6Kt","quote":"FormattedText_quote__BBm-M","line":"FormattedText_line__c2bmE","codeInline":"FormattedText_codeInline__jCbNL","codeBlock":"FormattedText_codeBlock__Xrbvn","jsonPre":"FormattedText_jsonPre__qOGmo","table":"FormattedText_table__ipMMS"};
|
|
3
3
|
if (typeof document !== 'undefined') {
|
|
4
4
|
var __uilibHead = document.head || document.getElementsByTagName('head')[0];
|
|
5
5
|
if (__uilibHead) {
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Placeholder text for the composer's empty paragraphs. Only a wholly empty
|
|
3
|
+
* composer shows it: a quote or list with an empty line after it already is
|
|
4
|
+
* content, so it keeps no hint.
|
|
5
|
+
*/
|
|
6
|
+
function promptComposerPlaceholder(text) {
|
|
7
|
+
return ({ editor }) => (editor.isEmpty ? text : '');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export { promptComposerPlaceholder };
|
|
@@ -3,12 +3,15 @@ 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 { promptComposerPlaceholder } from './promptComposerPlaceholder.js';
|
|
7
|
+
import { createPromptBlocksExtension, isInPromptBlock } from '../../tiptap/prompt-blocks/createPromptBlocksExtension.js';
|
|
8
|
+
import { promptBlocksDoc } from '../../tiptap/prompt-blocks/promptBlocksText.js';
|
|
6
9
|
import { createLinkHighlightExtension } from '../../tiptap/link-highlight/createLinkHighlightExtension.js';
|
|
7
10
|
import { NonInclusiveLink } from '../../tiptap/link-highlight/nonInclusiveLink.js';
|
|
8
11
|
import { DEFAULT_CHAT_SLASH_ITEMS } from '../../tiptap/slash-mention/defaultChatSlashItems.js';
|
|
9
12
|
import { createSlashMentionExtension } from '../../tiptap/slash-mention/createSlashMentionExtension.js';
|
|
10
13
|
import { safePromptComposerEditorText, promptComposerSafeEditorDom, syncPromptComposerHeight } from './PromptComposer.helpers.js';
|
|
11
|
-
import { PROMPT_COMPOSER_EMPTY_DOC
|
|
14
|
+
import { PROMPT_COMPOSER_EMPTY_DOC } from './promptComposerDoc.js';
|
|
12
15
|
|
|
13
16
|
function buildMentionConfigs({ slashCommandItems, onSlashItemCommand, mentionConfigs, atMentionItems, atMentionGetItems, onAtItemCommand, }) {
|
|
14
17
|
const configs = [];
|
|
@@ -101,18 +104,22 @@ function usePromptComposerEditor({ disabled, placeholder, slashCommandItems, sla
|
|
|
101
104
|
heading: false,
|
|
102
105
|
bulletList: false,
|
|
103
106
|
orderedList: false,
|
|
104
|
-
blockquote:
|
|
107
|
+
blockquote: { HTMLAttributes: { class: S.promptComposerQuote } },
|
|
105
108
|
codeBlock: false,
|
|
106
109
|
horizontalRule: false,
|
|
107
110
|
link: false,
|
|
108
111
|
}),
|
|
109
112
|
NonInclusiveLink,
|
|
110
113
|
Placeholder.configure({
|
|
111
|
-
placeholder: placeholderText,
|
|
114
|
+
placeholder: promptComposerPlaceholder(placeholderText),
|
|
112
115
|
showOnlyWhenEditable: true,
|
|
113
116
|
emptyNodeClass: S.promptComposerEmptyNode,
|
|
114
117
|
}),
|
|
115
118
|
createLinkHighlightExtension(S.promptComposerLink),
|
|
119
|
+
...createPromptBlocksExtension({
|
|
120
|
+
quoteClassName: S.promptComposerQuote,
|
|
121
|
+
listClassName: S.promptComposerList,
|
|
122
|
+
}),
|
|
116
123
|
];
|
|
117
124
|
for (const cfg of resolvedMentionConfigs) {
|
|
118
125
|
const slashChar = cfg.slashChar;
|
|
@@ -180,7 +187,8 @@ function usePromptComposerEditor({ disabled, placeholder, slashCommandItems, sla
|
|
|
180
187
|
if (!activeEditor)
|
|
181
188
|
return false;
|
|
182
189
|
const currentText = activeEditor.getText();
|
|
183
|
-
|
|
190
|
+
// In a list or a quote being typed, Enter adds the next line.
|
|
191
|
+
const hasNewlines = currentText.includes('\n') || isInPromptBlock(activeEditor.state);
|
|
184
192
|
const isModEnter = event.metaKey || event.ctrlKey;
|
|
185
193
|
// Single line: Enter submits. Multiline: Enter inserts a line; Cmd/Ctrl+Enter submits.
|
|
186
194
|
if (hasNewlines ? !isModEnter : isModEnter)
|
|
@@ -249,13 +257,13 @@ function usePromptComposerEditor({ disabled, placeholder, slashCommandItems, sla
|
|
|
249
257
|
const next = prefillMessage;
|
|
250
258
|
if (current === next)
|
|
251
259
|
return;
|
|
260
|
+
// The app trims the prompt on blur, and a list or quote at the end always
|
|
261
|
+
// has an empty line after it. Resetting for that would refocus the editor
|
|
262
|
+
// and turn an empty last item into text.
|
|
263
|
+
if (next !== '' && next === current.trim())
|
|
264
|
+
return;
|
|
252
265
|
try {
|
|
253
|
-
|
|
254
|
-
editor.commands.setContent(PROMPT_COMPOSER_EMPTY_DOC);
|
|
255
|
-
}
|
|
256
|
-
else {
|
|
257
|
-
editor.commands.setContent(promptComposerParagraphDoc(next));
|
|
258
|
-
}
|
|
266
|
+
editor.commands.setContent(promptBlocksDoc(next));
|
|
259
267
|
}
|
|
260
268
|
catch {
|
|
261
269
|
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 };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import type { FormattedTextProps } from './FormattedText.types';
|
|
3
3
|
export declare function hrefForFormattedLink(url: string): string;
|
|
4
|
-
export declare function
|
|
4
|
+
export declare function findJsonEndExclusive(content: string, start: number): number;
|
|
5
|
+
export declare function FormattedText({ text, className, onButtonClick, quotes, injectors, breaks, }: FormattedTextProps): React.JSX.Element;
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
export type InjectionResult = {
|
|
3
|
+
elem: React.ReactNode;
|
|
4
|
+
index: number;
|
|
5
|
+
length: number;
|
|
6
|
+
} | null;
|
|
7
|
+
export type Injector = (content: string) => InjectionResult;
|
|
1
8
|
export type FormattedTextProps = {
|
|
2
9
|
text: string;
|
|
3
10
|
className?: string;
|
|
@@ -5,4 +12,7 @@ export type FormattedTextProps = {
|
|
|
5
12
|
text: string;
|
|
6
13
|
[name: string]: string;
|
|
7
14
|
}) => void;
|
|
15
|
+
quotes?: boolean;
|
|
16
|
+
injectors?: Injector[];
|
|
17
|
+
breaks?: boolean;
|
|
8
18
|
};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { FormattedText } from './FormattedText';
|
|
2
|
-
export type { FormattedTextProps } from './FormattedText.types';
|
|
1
|
+
export { FormattedText, findJsonEndExclusive } from './FormattedText';
|
|
2
|
+
export type { FormattedTextProps, InjectionResult, Injector, } from './FormattedText.types';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Editor } from '@tiptap/core';
|
|
2
|
+
/**
|
|
3
|
+
* Placeholder text for the composer's empty paragraphs. Only a wholly empty
|
|
4
|
+
* composer shows it: a quote or list with an empty line after it already is
|
|
5
|
+
* content, so it keeps no hint.
|
|
6
|
+
*/
|
|
7
|
+
export declare function promptComposerPlaceholder(text: string): ({ editor }: {
|
|
8
|
+
editor: Editor;
|
|
9
|
+
}) => string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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.21",
|
|
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",
|