@sybilion/uilib 1.3.36 → 1.3.38
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/components/ui/Chat/Chat.types.js +6 -1
- package/dist/esm/components/ui/Chat/ChatChrome/ChatChrome.js +7 -10
- package/dist/esm/components/ui/Chat/ChatPrompt/ChatPrompt.js +2 -1
- package/dist/esm/components/ui/Chat/ChatPrompt/useChatPromptEditor.js +6 -2
- package/dist/esm/components/ui/Chat/ChatSheet/ChatSheet.js +2 -1
- package/dist/esm/components/ui/Chat/ChatSheet/useChatPanelChromeModel.js +33 -15
- package/dist/esm/index.js +1 -1
- package/dist/esm/tiptap/slash-mention/createSlashMentionExtension.js +48 -14
- package/dist/esm/types/src/components/ui/Chat/Chat.types.d.ts +6 -1
- package/dist/esm/types/src/components/ui/Chat/Chat.types.test.d.ts +1 -0
- package/dist/esm/types/src/components/ui/Chat/ChatChrome/ChatChrome.d.ts +1 -1
- package/dist/esm/types/src/components/ui/Chat/ChatChrome/ChatChrome.types.d.ts +3 -6
- package/dist/esm/types/src/components/ui/Chat/ChatPrompt/ChatPrompt.d.ts +1 -1
- package/dist/esm/types/src/components/ui/Chat/ChatPrompt/useChatPromptEditor.d.ts +3 -2
- package/dist/esm/types/src/components/ui/Chat/ChatSheet/ChatSheet.d.ts +1 -1
- package/dist/esm/types/src/components/ui/Chat/ChatSheet/useChatPanelChromeModel.d.ts +5 -2
- package/dist/esm/types/src/components/ui/Chat/index.d.ts +1 -1
- package/dist/esm/types/src/docs/docsHeaderActions.d.ts +2 -1
- package/dist/esm/types/src/tiptap/slash-mention/createSlashMentionExtension.d.ts +3 -3
- package/dist/esm/types/src/tiptap/slash-mention/index.d.ts +1 -1
- package/dist/esm/types/src/tiptap/slash-mention/types.d.ts +9 -1
- package/package.json +1 -1
- package/src/components/ui/Chat/Chat.types.test.ts +32 -0
- package/src/components/ui/Chat/Chat.types.ts +13 -1
- package/src/components/ui/Chat/ChatChrome/ChatChrome.tsx +24 -46
- package/src/components/ui/Chat/ChatChrome/ChatChrome.types.ts +6 -8
- package/src/components/ui/Chat/ChatPrompt/ChatPrompt.tsx +2 -0
- package/src/components/ui/Chat/ChatPrompt/useChatPromptEditor.ts +11 -2
- package/src/components/ui/Chat/ChatSheet/ChatSheet.tsx +2 -0
- package/src/components/ui/Chat/ChatSheet/useChatPanelChromeModel.tsx +147 -109
- package/src/components/ui/Chat/index.ts +5 -1
- package/src/docs/docsHeaderActions.tsx +3 -2
- package/src/docs/pages/ChatAttachmentsDropzonePage.tsx +0 -5
- package/src/docs/pages/ChatPage.tsx +0 -5
- package/src/docs/pages/ChatSlashCommandsPage.tsx +43 -14
- package/src/docs/pages/ChatUserCsvAttachmentPage.tsx +0 -5
- package/src/tiptap/slash-mention/createSlashMentionExtension.ts +65 -11
- package/src/tiptap/slash-mention/index.ts +1 -0
- package/src/tiptap/slash-mention/types.ts +10 -1
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
type SlashCommandItem,
|
|
9
9
|
} from '#uilib/components/ui/Chat';
|
|
10
10
|
import { PageContentSection } from '#uilib/components/ui/Page';
|
|
11
|
+
import type { SlashItemCommandContext } from '#uilib/tiptap/slash-mention/types';
|
|
11
12
|
import { ScrollRef } from '@homecode/ui';
|
|
12
13
|
|
|
13
14
|
import { AppPageHeader } from '../components/AppPageHeader/AppPageHeader';
|
|
@@ -15,18 +16,23 @@ import { DocsHeaderActions } from '../docsHeaderActions';
|
|
|
15
16
|
|
|
16
17
|
const NO_QUICK_REPLY_KEYS: ReadonlySet<string> = new Set();
|
|
17
18
|
|
|
19
|
+
const DOCS_SAMPLE_COMMAND_ID = 'sample-command';
|
|
20
|
+
|
|
18
21
|
/** Sample items so the docs demo still shows a `/` palette (`DEFAULT_CHAT_SLASH_ITEMS` is empty). */
|
|
19
22
|
const DOCS_SAMPLE_SLASH_ITEMS: SlashCommandItem[] = [
|
|
20
23
|
{
|
|
21
|
-
id:
|
|
22
|
-
label:
|
|
24
|
+
id: DOCS_SAMPLE_COMMAND_ID,
|
|
25
|
+
label: DOCS_SAMPLE_COMMAND_ID,
|
|
23
26
|
description:
|
|
24
|
-
'Demo
|
|
27
|
+
'Demo handler — clears composer and runs `onSlashItemCommand` (no mention insert).',
|
|
25
28
|
},
|
|
26
29
|
];
|
|
27
30
|
|
|
31
|
+
const SAMPLE_COMMAND_REPLY_TEXT =
|
|
32
|
+
'Sample command ran via `onSlashItemCommand` — composer cleared, no mention inserted.';
|
|
33
|
+
|
|
28
34
|
const ASSISTANT_REPLY_TEXT =
|
|
29
|
-
'Demo reply
|
|
35
|
+
'Demo reply for a normal message. Slash picks with a custom handler skip mention insert.';
|
|
30
36
|
|
|
31
37
|
function makeMessage(role: MessageRole, text: string): Message {
|
|
32
38
|
return {
|
|
@@ -56,11 +62,34 @@ export default function ChatSlashCommandsPage() {
|
|
|
56
62
|
messages.length > 0 &&
|
|
57
63
|
messages[messages.length - 1]?.role === MessageRole.USER;
|
|
58
64
|
|
|
65
|
+
const runSampleCommand = useCallback(() => {
|
|
66
|
+
setMessages(prev => [
|
|
67
|
+
...prev,
|
|
68
|
+
makeMessage(MessageRole.ASSISTANT, SAMPLE_COMMAND_REPLY_TEXT),
|
|
69
|
+
]);
|
|
70
|
+
}, []);
|
|
71
|
+
|
|
72
|
+
const onSlashItemCommand = useCallback(
|
|
73
|
+
({ item }: SlashItemCommandContext) => {
|
|
74
|
+
if (item.id !== DOCS_SAMPLE_COMMAND_ID) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
queueMicrotask(() => runSampleCommand());
|
|
78
|
+
return true;
|
|
79
|
+
},
|
|
80
|
+
[runSampleCommand],
|
|
81
|
+
);
|
|
82
|
+
|
|
59
83
|
const onSubmit = useCallback(
|
|
60
84
|
(raw: string) => {
|
|
61
85
|
const text = raw.trim();
|
|
62
86
|
if (!text || isLoading) return;
|
|
63
87
|
|
|
88
|
+
if (text === `/${DOCS_SAMPLE_COMMAND_ID}`) {
|
|
89
|
+
runSampleCommand();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
64
93
|
setMessages(prev => [...prev, makeMessage(MessageRole.USER, text)]);
|
|
65
94
|
setIsLoading(true);
|
|
66
95
|
|
|
@@ -76,7 +105,7 @@ export default function ChatSlashCommandsPage() {
|
|
|
76
105
|
setIsLoading(false);
|
|
77
106
|
}, 900);
|
|
78
107
|
},
|
|
79
|
-
[isLoading],
|
|
108
|
+
[isLoading, runSampleCommand],
|
|
80
109
|
);
|
|
81
110
|
|
|
82
111
|
return (
|
|
@@ -84,8 +113,10 @@ export default function ChatSlashCommandsPage() {
|
|
|
84
113
|
<AppPageHeader
|
|
85
114
|
breadcrumbs={[{ label: 'Chat' }, { label: 'Chat slash commands' }]}
|
|
86
115
|
title="Chat slash commands"
|
|
87
|
-
subheader={`Slash palette uses TipTap Mention with "/" as the trigger.
|
|
88
|
-
actions={
|
|
116
|
+
subheader={`Slash palette uses TipTap Mention with "/" as the trigger. Pass slashCommandItems from the app; optional onSlashItemCommand can clear the composer and run an action instead of inserting a mention.`}
|
|
117
|
+
actions={
|
|
118
|
+
<DocsHeaderActions slashCommandItems={DOCS_SAMPLE_SLASH_ITEMS} />
|
|
119
|
+
}
|
|
89
120
|
/>
|
|
90
121
|
<PageContentSection>
|
|
91
122
|
<p style={{ marginBottom: 16, fontSize: 14, lineHeight: 1.5 }}>
|
|
@@ -95,7 +126,9 @@ export default function ChatSlashCommandsPage() {
|
|
|
95
126
|
</Link>{' '}
|
|
96
127
|
shell below: scrolling history, empty state, disclaimer, composer.
|
|
97
128
|
Type <kbd className="font-mono">/</kbd> at line start or after a
|
|
98
|
-
space; pick
|
|
129
|
+
space; pick <kbd className="font-mono">sample-command</kbd> to run the
|
|
130
|
+
custom handler, or send{' '}
|
|
131
|
+
<kbd className="font-mono">/sample-command</kbd> with Enter.
|
|
99
132
|
</p>
|
|
100
133
|
<ChatChrome
|
|
101
134
|
showResizeHandle={false}
|
|
@@ -109,13 +142,8 @@ export default function ChatSlashCommandsPage() {
|
|
|
109
142
|
isLoading={isLoading}
|
|
110
143
|
scriptContinueLabel={undefined}
|
|
111
144
|
onScriptContinue={undefined}
|
|
112
|
-
showBranchActionsRow={false}
|
|
113
145
|
showSyntheticBranchButtons={false}
|
|
114
146
|
unusedBranchKeys={[]}
|
|
115
|
-
isScriptComplete={false}
|
|
116
|
-
onGenerateDashboard={undefined}
|
|
117
|
-
generatingDashboard={false}
|
|
118
|
-
onGenerateDashboardClick={() => {}}
|
|
119
147
|
showInlinePresets={false}
|
|
120
148
|
isLastMessageFromUser={isLastMessageFromUser}
|
|
121
149
|
scrollRef={scrollRef}
|
|
@@ -123,11 +151,12 @@ export default function ChatSlashCommandsPage() {
|
|
|
123
151
|
onPromptSubmit={onSubmit}
|
|
124
152
|
onChatDeleted={() => {}}
|
|
125
153
|
slashCommandItems={DOCS_SAMPLE_SLASH_ITEMS}
|
|
154
|
+
onSlashItemCommand={onSlashItemCommand}
|
|
126
155
|
promptPlaceholder='Ask something or type "/" for demo commands…'
|
|
127
156
|
emptyState={{
|
|
128
157
|
title: 'Try a slash command',
|
|
129
158
|
description:
|
|
130
|
-
'
|
|
159
|
+
'Pick sample-command from the palette or send /sample-command — onSlashItemCommand clears the composer and runs the demo action.',
|
|
131
160
|
additionalContent: (
|
|
132
161
|
<p>Optional empty-state slot via additionalContent.</p>
|
|
133
162
|
),
|
|
@@ -148,13 +148,8 @@ export default function ChatUserCsvAttachmentPage() {
|
|
|
148
148
|
isLoading={isLoading}
|
|
149
149
|
scriptContinueLabel={undefined}
|
|
150
150
|
onScriptContinue={undefined}
|
|
151
|
-
showBranchActionsRow={false}
|
|
152
151
|
showSyntheticBranchButtons={false}
|
|
153
152
|
unusedBranchKeys={[]}
|
|
154
|
-
isScriptComplete={false}
|
|
155
|
-
onGenerateDashboard={undefined}
|
|
156
|
-
generatingDashboard={false}
|
|
157
|
-
onGenerateDashboardClick={() => {}}
|
|
158
153
|
showInlinePresets={false}
|
|
159
154
|
isLastMessageFromUser={isLastMessageFromUser}
|
|
160
155
|
scrollRef={scrollRef}
|
|
@@ -14,10 +14,42 @@ import { filterSlashItems } from './defaultChatSlashItems';
|
|
|
14
14
|
import type {
|
|
15
15
|
CreateSlashMentionExtensionOptions,
|
|
16
16
|
SlashCommandItem,
|
|
17
|
+
SlashSuggestionPlacement,
|
|
17
18
|
} from './types';
|
|
18
19
|
|
|
20
|
+
const SUGGESTION_GAP_PX = 4;
|
|
21
|
+
|
|
22
|
+
function placeSlashSuggestionPopup(
|
|
23
|
+
popupElement: HTMLElement,
|
|
24
|
+
clientRect: DOMRect | null | undefined,
|
|
25
|
+
placement: SlashSuggestionPlacement,
|
|
26
|
+
): void {
|
|
27
|
+
if (!clientRect) return;
|
|
28
|
+
|
|
29
|
+
const el = popupElement;
|
|
30
|
+
const popupHeight = el.getBoundingClientRect().height;
|
|
31
|
+
const spaceBelow = window.innerHeight - clientRect.bottom - SUGGESTION_GAP_PX;
|
|
32
|
+
const spaceAbove = clientRect.top - SUGGESTION_GAP_PX;
|
|
33
|
+
|
|
34
|
+
let showAbove = placement === 'above';
|
|
35
|
+
if (placement === 'auto') {
|
|
36
|
+
showAbove = spaceBelow < popupHeight && spaceAbove >= spaceBelow;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const top = showAbove
|
|
40
|
+
? Math.max(
|
|
41
|
+
SUGGESTION_GAP_PX,
|
|
42
|
+
clientRect.top - popupHeight - SUGGESTION_GAP_PX,
|
|
43
|
+
)
|
|
44
|
+
: clientRect.bottom + SUGGESTION_GAP_PX;
|
|
45
|
+
|
|
46
|
+
el.style.left = `${clientRect.left}px`;
|
|
47
|
+
el.style.top = `${top}px`;
|
|
48
|
+
}
|
|
49
|
+
|
|
19
50
|
export function slashMentionSuggestionRender(
|
|
20
51
|
uiRef: MutableRefObject<SlashSuggestionListHandle | null>,
|
|
52
|
+
placement: SlashSuggestionPlacement = 'below',
|
|
21
53
|
): {
|
|
22
54
|
onStart?: (
|
|
23
55
|
props: SuggestionProps<SlashCommandItem, SlashCommandItem>,
|
|
@@ -45,11 +77,11 @@ export function slashMentionSuggestionRender(
|
|
|
45
77
|
>,
|
|
46
78
|
) => {
|
|
47
79
|
if (!popup?.element) return;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
80
|
+
placeSlashSuggestionPopup(
|
|
81
|
+
popup.element,
|
|
82
|
+
props.clientRect?.() ?? null,
|
|
83
|
+
placement,
|
|
84
|
+
);
|
|
53
85
|
};
|
|
54
86
|
|
|
55
87
|
return {
|
|
@@ -69,6 +101,7 @@ export function slashMentionSuggestionRender(
|
|
|
69
101
|
popup.element.style.zIndex = '10002';
|
|
70
102
|
document.body.append(popup.element);
|
|
71
103
|
place(props);
|
|
104
|
+
requestAnimationFrame(() => place(props));
|
|
72
105
|
},
|
|
73
106
|
onUpdate: props => {
|
|
74
107
|
if (!popup) return;
|
|
@@ -78,6 +111,7 @@ export function slashMentionSuggestionRender(
|
|
|
78
111
|
listHandleRef: uiRef,
|
|
79
112
|
});
|
|
80
113
|
place(props);
|
|
114
|
+
requestAnimationFrame(() => place(props));
|
|
81
115
|
},
|
|
82
116
|
onExit: () => {
|
|
83
117
|
popup?.destroy();
|
|
@@ -90,6 +124,28 @@ export function slashMentionSuggestionRender(
|
|
|
90
124
|
};
|
|
91
125
|
}
|
|
92
126
|
|
|
127
|
+
function clearSlashTriggerEditor(
|
|
128
|
+
editor: Editor,
|
|
129
|
+
range: { from: number; to: number },
|
|
130
|
+
): void {
|
|
131
|
+
if (editor.isDestroyed) return;
|
|
132
|
+
try {
|
|
133
|
+
editor.chain().focus().deleteRange(range).clearContent().run();
|
|
134
|
+
} catch {
|
|
135
|
+
// Editor view may be tearing down during suggestion exit.
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function collapseEditorSelectionEnd(editor: Editor): void {
|
|
140
|
+
if (editor.isDestroyed) return;
|
|
141
|
+
try {
|
|
142
|
+
editor.view?.dom?.ownerDocument?.defaultView
|
|
143
|
+
?.getSelection?.()
|
|
144
|
+
?.collapseToEnd();
|
|
145
|
+
} catch {
|
|
146
|
+
// view.dom throws when editor is not mounted
|
|
147
|
+
}
|
|
148
|
+
}
|
|
93
149
|
function insertDefaultMention(
|
|
94
150
|
editor: Editor,
|
|
95
151
|
range: { from: number; to: number },
|
|
@@ -149,6 +205,7 @@ export function createSlashMentionExtension({
|
|
|
149
205
|
slashChar = '/',
|
|
150
206
|
pluginKey,
|
|
151
207
|
onItemCommand,
|
|
208
|
+
suggestionPlacement = 'below',
|
|
152
209
|
onSuggestionUiActiveChange,
|
|
153
210
|
}: CreateSlashMentionExtensionConfiguredOptions) {
|
|
154
211
|
const uiRef: MutableRefObject<SlashSuggestionListHandle | null> = {
|
|
@@ -188,18 +245,15 @@ export function createSlashMentionExtension({
|
|
|
188
245
|
command: ({ editor, range, props }) => {
|
|
189
246
|
const item = props as SlashCommandItem;
|
|
190
247
|
if (onItemCommand?.({ editor, range, item }) === true) {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
?.getSelection?.()
|
|
194
|
-
?.collapseToEnd();
|
|
195
|
-
});
|
|
248
|
+
clearSlashTriggerEditor(editor, range);
|
|
249
|
+
queueMicrotask(() => collapseEditorSelectionEnd(editor));
|
|
196
250
|
return null;
|
|
197
251
|
}
|
|
198
252
|
insertDefaultMention(editor, range, item, slashChar);
|
|
199
253
|
return null;
|
|
200
254
|
},
|
|
201
255
|
render: () => {
|
|
202
|
-
const menu = slashMentionSuggestionRender(uiRef);
|
|
256
|
+
const menu = slashMentionSuggestionRender(uiRef, suggestionPlacement);
|
|
203
257
|
return {
|
|
204
258
|
...menu,
|
|
205
259
|
onStart: props => {
|
|
@@ -13,10 +13,14 @@ export type SlashItemCommandContext = {
|
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* If provided, run
|
|
16
|
+
* If provided, run when a slash item is picked. Return true to skip mention insert
|
|
17
|
+
* (extension clears the trigger text from the composer).
|
|
17
18
|
*/
|
|
18
19
|
export type SlashOnItemCommand = (ctx: SlashItemCommandContext) => boolean;
|
|
19
20
|
|
|
21
|
+
/** Where the slash palette opens relative to the caret. */
|
|
22
|
+
export type SlashSuggestionPlacement = 'below' | 'above' | 'auto';
|
|
23
|
+
|
|
20
24
|
export type CreateSlashMentionExtensionOptions = {
|
|
21
25
|
/** Items shown in the slash menu (filtered by query after `/`). */
|
|
22
26
|
items: SlashCommandItem[];
|
|
@@ -26,4 +30,9 @@ export type CreateSlashMentionExtensionOptions = {
|
|
|
26
30
|
pluginKey?: import('@tiptap/pm/state').PluginKey;
|
|
27
31
|
/** Custom handler (e.g. insert a block node instead of a mention). */
|
|
28
32
|
onItemCommand?: SlashOnItemCommand;
|
|
33
|
+
/**
|
|
34
|
+
* Palette position vs caret. Default `below`.
|
|
35
|
+
* Use `above` for bottom-anchored composers (chat prompt); `auto` flips by viewport space.
|
|
36
|
+
*/
|
|
37
|
+
suggestionPlacement?: SlashSuggestionPlacement;
|
|
29
38
|
};
|