@sybilion/uilib 1.3.32 → 1.3.35

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.
Files changed (56) hide show
  1. package/dist/esm/components/ui/Chat/ChatChrome/ChatChrome.js +2 -2
  2. package/dist/esm/components/ui/Chat/ChatPrompt/ChatPrompt.helpers.js +18 -4
  3. package/dist/esm/components/ui/Chat/ChatPrompt/ChatPrompt.js +39 -52
  4. package/dist/esm/components/ui/Chat/ChatPrompt/ChatPrompt.styl.js +2 -2
  5. package/dist/esm/components/ui/Chat/ChatPrompt/ChatPromptComposer.js +25 -0
  6. package/dist/esm/components/ui/Chat/ChatPrompt/chatPromptDoc.js +17 -0
  7. package/dist/esm/components/ui/Chat/ChatPrompt/useChatPromptEditor.js +163 -0
  8. package/dist/esm/components/ui/Tooltip/Tooltip.js +1 -1
  9. package/dist/esm/components/ui/Tooltip/Tooltip.styl.js +1 -1
  10. package/dist/esm/index.js +2 -0
  11. package/dist/esm/tiptap/slash-mention/SlashSuggestionList.js +53 -0
  12. package/dist/esm/tiptap/slash-mention/SlashSuggestionList.styl.js +7 -0
  13. package/dist/esm/tiptap/slash-mention/createSlashMentionExtension.js +151 -0
  14. package/dist/esm/tiptap/slash-mention/defaultChatSlashItems.js +12 -0
  15. package/dist/esm/types/src/components/ui/Chat/Chat.types.d.ts +3 -0
  16. package/dist/esm/types/src/components/ui/Chat/ChatChrome/ChatChrome.d.ts +1 -1
  17. package/dist/esm/types/src/components/ui/Chat/ChatChrome/ChatChrome.types.d.ts +5 -0
  18. package/dist/esm/types/src/components/ui/Chat/ChatPrompt/ChatPrompt.d.ts +1 -1
  19. package/dist/esm/types/src/components/ui/Chat/ChatPrompt/ChatPrompt.helpers.d.ts +6 -0
  20. package/dist/esm/types/src/components/ui/Chat/ChatPrompt/ChatPromptComposer.d.ts +13 -0
  21. package/dist/esm/types/src/components/ui/Chat/ChatPrompt/chatPromptDoc.d.ts +3 -0
  22. package/dist/esm/types/src/components/ui/Chat/ChatPrompt/useChatPromptEditor.d.ts +20 -0
  23. package/dist/esm/types/src/components/ui/Chat/index.d.ts +1 -0
  24. package/dist/esm/types/src/components/ui/Input/Input.d.ts +1 -1
  25. package/dist/esm/types/src/docs/pages/ChatSlashCommandsPage.d.ts +1 -0
  26. package/dist/esm/types/src/index.d.ts +1 -0
  27. package/dist/esm/types/src/tiptap/slash-mention/SlashSuggestionList.d.ts +12 -0
  28. package/dist/esm/types/src/tiptap/slash-mention/createSlashMentionExtension.d.ts +21 -0
  29. package/dist/esm/types/src/tiptap/slash-mention/defaultChatSlashItems.d.ts +4 -0
  30. package/dist/esm/types/src/tiptap/slash-mention/index.d.ts +5 -0
  31. package/dist/esm/types/src/tiptap/slash-mention/types.d.ts +25 -0
  32. package/package.json +15 -1
  33. package/src/components/ui/Chat/Chat.types.ts +4 -0
  34. package/src/components/ui/Chat/ChatChrome/ChatChrome.tsx +4 -0
  35. package/src/components/ui/Chat/ChatChrome/ChatChrome.types.ts +5 -0
  36. package/src/components/ui/Chat/ChatPrompt/ChatPrompt.helpers.ts +43 -0
  37. package/src/components/ui/Chat/ChatPrompt/ChatPrompt.styl +33 -5
  38. package/src/components/ui/Chat/ChatPrompt/ChatPrompt.styl.d.ts +2 -1
  39. package/src/components/ui/Chat/ChatPrompt/ChatPrompt.tsx +50 -106
  40. package/src/components/ui/Chat/ChatPrompt/ChatPromptComposer.tsx +93 -0
  41. package/src/components/ui/Chat/ChatPrompt/chatPromptDoc.ts +18 -0
  42. package/src/components/ui/Chat/ChatPrompt/useChatPromptEditor.ts +214 -0
  43. package/src/components/ui/Chat/index.ts +1 -0
  44. package/src/components/ui/Tooltip/Tooltip.styl +1 -0
  45. package/src/components/ui/Tooltip/Tooltip.tsx +1 -1
  46. package/src/docs/pages/ChatSlashCommandsPage.tsx +139 -0
  47. package/src/docs/pages/TooltipPage.tsx +1 -1
  48. package/src/docs/registry.ts +6 -0
  49. package/src/index.ts +1 -0
  50. package/src/tiptap/slash-mention/SlashSuggestionList.styl +48 -0
  51. package/src/tiptap/slash-mention/SlashSuggestionList.styl.d.ts +11 -0
  52. package/src/tiptap/slash-mention/SlashSuggestionList.tsx +109 -0
  53. package/src/tiptap/slash-mention/createSlashMentionExtension.ts +217 -0
  54. package/src/tiptap/slash-mention/defaultChatSlashItems.ts +18 -0
  55. package/src/tiptap/slash-mention/index.ts +16 -0
  56. package/src/tiptap/slash-mention/types.ts +29 -0
@@ -109,6 +109,12 @@ export const DOC_REGISTRY: DocEntry[] = [
109
109
  section: 'Chat',
110
110
  load: () => import('./pages/ChatPage'),
111
111
  },
112
+ {
113
+ slug: 'chat-slash-commands',
114
+ title: 'Chat slash commands',
115
+ section: 'Chat',
116
+ load: () => import('./pages/ChatSlashCommandsPage'),
117
+ },
112
118
  {
113
119
  slug: 'chat-user-csv-attachment',
114
120
  title: 'Chat user CSV attachment',
package/src/index.ts CHANGED
@@ -11,6 +11,7 @@ export { DEFAULT_THEME_ACTIVE_COLOR } from './docs/lib/theme';
11
11
  export * from './sybilion-auth';
12
12
  export * from './types/sybilionDatasetSnapshots';
13
13
  export * from './contexts/chat-context';
14
+ export * from './tiptap/slash-mention';
14
15
  export * from './types/chat-api.types';
15
16
  export * from './components/ui/AnalysesSelector';
16
17
  export * from './components/ui/AnalysisLineIcon';
@@ -0,0 +1,48 @@
1
+ @import 'lib/theme.styl';
2
+
3
+ .root
4
+ display flex
5
+ flex-direction column
6
+ min-width 220px
7
+ max-width min(340px, 90vw)
8
+ max-height min(260px, 40vh)
9
+ overflow-y auto
10
+ padding var(--p-1)
11
+ margin 0
12
+ list-style none
13
+ background var(--popover, var(--card))
14
+ color var(--foreground)
15
+ border 1px solid var(--border)
16
+ border-radius var(--radius-lg, 8px)
17
+ box-shadow 0 8px 24px rgba(0, 0, 0, 0.12)
18
+ font-size var(--text-sm)
19
+
20
+ :global(.dark) &
21
+ box-shadow 0 8px 24px rgba(0, 0, 0, 0.5)
22
+
23
+ .item
24
+ display flex
25
+ flex-direction column
26
+ align-items flex-start
27
+ width 100%
28
+ padding var(--p-1) var(--p-2)
29
+ margin 0
30
+ border none
31
+ border-radius var(--radius-md, 6px)
32
+ background transparent
33
+ color inherit
34
+ text-align left
35
+ cursor pointer
36
+
37
+ &:hover
38
+ background var(--muted)
39
+
40
+ .itemHighlighted
41
+ background var(--muted)
42
+
43
+ .itemLabel
44
+ font-weight 600
45
+
46
+ .itemDesc
47
+ font-size var(--text-xs)
48
+ color var(--muted-foreground)
@@ -0,0 +1,11 @@
1
+ // This file is automatically generated.
2
+ // Please do not change this file!
3
+ interface CssExports {
4
+ 'item': string;
5
+ 'itemDesc': string;
6
+ 'itemHighlighted': string;
7
+ 'itemLabel': string;
8
+ 'root': string;
9
+ }
10
+ export const cssExports: CssExports;
11
+ export default cssExports;
@@ -0,0 +1,109 @@
1
+ import cn from 'classnames';
2
+ import type { RefObject } from 'react';
3
+ import { useCallback, useEffect, useImperativeHandle, useState } from 'react';
4
+
5
+ import S from './SlashSuggestionList.styl';
6
+ import type { SlashCommandItem } from './types';
7
+
8
+ export type SlashSuggestionListHandle = {
9
+ onKeyboardEvent: (event: KeyboardEvent) => boolean;
10
+ };
11
+
12
+ type SlashSuggestionListInnerProps = {
13
+ items: SlashCommandItem[];
14
+ command: (item: SlashCommandItem) => void;
15
+ listHandleRef: RefObject<SlashSuggestionListHandle | null>;
16
+ };
17
+
18
+ function SlashSuggestionListInner({
19
+ items,
20
+ command,
21
+ listHandleRef,
22
+ }: SlashSuggestionListInnerProps) {
23
+ const [selected, setSelected] = useState(0);
24
+
25
+ useEffect(() => {
26
+ setSelected(s => Math.min(s, Math.max(items.length - 1, 0)));
27
+ }, [items.length]);
28
+
29
+ const safeSel = Math.min(selected, Math.max(items.length - 1, 0));
30
+
31
+ const onPick = useCallback(
32
+ (index: number) => {
33
+ const item = items[index];
34
+ if (!item) return;
35
+ command(item);
36
+ },
37
+ [command, items],
38
+ );
39
+
40
+ const onKeyboardEvent = useCallback(
41
+ (event: KeyboardEvent): boolean => {
42
+ if (items.length === 0) return false;
43
+ if (event.key === 'ArrowDown') {
44
+ event.preventDefault();
45
+ setSelected(s => Math.min(s + 1, items.length - 1));
46
+ return true;
47
+ }
48
+ if (event.key === 'ArrowUp') {
49
+ event.preventDefault();
50
+ setSelected(s => Math.max(s - 1, 0));
51
+ return true;
52
+ }
53
+ if (event.key === 'Enter') {
54
+ event.preventDefault();
55
+ const max = Math.max(items.length - 1, 0);
56
+ const idx = Math.min(selected, max);
57
+ const item = items[idx];
58
+ if (item) command(item);
59
+ return true;
60
+ }
61
+ return false;
62
+ },
63
+ [command, items, selected],
64
+ );
65
+
66
+ useImperativeHandle(listHandleRef, () => ({ onKeyboardEvent }), [
67
+ onKeyboardEvent,
68
+ ]);
69
+
70
+ return (
71
+ <div className={S.root} role="listbox" aria-label="Slash commands">
72
+ {items.map((item, idx) => (
73
+ <button
74
+ key={`${item.id}-${idx}`}
75
+ type="button"
76
+ role="option"
77
+ aria-selected={idx === safeSel}
78
+ className={cn(S.item, idx === safeSel && S.itemHighlighted)}
79
+ onMouseDown={e => e.preventDefault()}
80
+ onMouseEnter={() => setSelected(idx)}
81
+ onClick={() => onPick(idx)}
82
+ >
83
+ <span className={S.itemLabel}>/{item.label}</span>
84
+ {item.description ? (
85
+ <span className={S.itemDesc}>{item.description}</span>
86
+ ) : null}
87
+ </button>
88
+ ))}
89
+ </div>
90
+ );
91
+ }
92
+
93
+ /** Props forwarded from Tiptap suggestion renderer + our wiring. */
94
+ export type SlashSuggestionListProps = {
95
+ items: SlashCommandItem[];
96
+ command: (item: SlashCommandItem) => void;
97
+ listHandleRef: RefObject<SlashSuggestionListHandle | null>;
98
+ };
99
+
100
+ export function SlashSuggestionList(props: SlashSuggestionListProps) {
101
+ if (props.items.length === 0) return null;
102
+ return (
103
+ <SlashSuggestionListInner
104
+ items={props.items}
105
+ command={props.command}
106
+ listHandleRef={props.listHandleRef}
107
+ />
108
+ );
109
+ }
@@ -0,0 +1,217 @@
1
+ import type { MutableRefObject } from 'react';
2
+
3
+ import { type Editor, mergeAttributes } from '@tiptap/core';
4
+ import Mention from '@tiptap/extension-mention';
5
+ import type { EditorState } from '@tiptap/pm/state';
6
+ import { ReactRenderer } from '@tiptap/react';
7
+ import type { SuggestionProps } from '@tiptap/suggestion';
8
+
9
+ import {
10
+ SlashSuggestionList,
11
+ type SlashSuggestionListHandle,
12
+ } from './SlashSuggestionList';
13
+ import { filterSlashItems } from './defaultChatSlashItems';
14
+ import type {
15
+ CreateSlashMentionExtensionOptions,
16
+ SlashCommandItem,
17
+ } from './types';
18
+
19
+ export function slashMentionSuggestionRender(
20
+ uiRef: MutableRefObject<SlashSuggestionListHandle | null>,
21
+ ): {
22
+ onStart?: (
23
+ props: SuggestionProps<SlashCommandItem, SlashCommandItem>,
24
+ ) => void;
25
+ onUpdate?: (
26
+ props: SuggestionProps<SlashCommandItem, SlashCommandItem>,
27
+ ) => void;
28
+ onExit?: (props: SuggestionProps<SlashCommandItem, SlashCommandItem>) => void;
29
+ onKeyDown?: (p: {
30
+ view: unknown;
31
+ event: KeyboardEvent;
32
+ range: { from: number; to: number };
33
+ }) => boolean;
34
+ } {
35
+ let popup: ReactRenderer<{
36
+ items: SlashCommandItem[];
37
+ command: (item: SlashCommandItem) => void;
38
+ listHandleRef: MutableRefObject<SlashSuggestionListHandle | null>;
39
+ }> | null = null;
40
+
41
+ const place = (
42
+ props: Pick<
43
+ SuggestionProps<SlashCommandItem, SlashCommandItem>,
44
+ 'clientRect'
45
+ >,
46
+ ) => {
47
+ if (!popup?.element) return;
48
+ const rect = props.clientRect?.();
49
+ if (!rect) return;
50
+ const el = popup.element;
51
+ el.style.left = `${rect.left}px`;
52
+ el.style.top = `${rect.bottom + 4}px`;
53
+ };
54
+
55
+ return {
56
+ onStart: props => {
57
+ uiRef.current = null;
58
+ popup?.destroy?.();
59
+ popup = new ReactRenderer(SlashSuggestionList, {
60
+ editor: props.editor,
61
+ props: {
62
+ items: props.items,
63
+ command: props.command,
64
+ listHandleRef: uiRef,
65
+ },
66
+ });
67
+ popup.element.style.position = 'fixed';
68
+ popup.element.style.margin = '0';
69
+ popup.element.style.zIndex = '10002';
70
+ document.body.append(popup.element);
71
+ place(props);
72
+ },
73
+ onUpdate: props => {
74
+ if (!popup) return;
75
+ popup.updateProps({
76
+ items: props.items,
77
+ command: props.command,
78
+ listHandleRef: uiRef,
79
+ });
80
+ place(props);
81
+ },
82
+ onExit: () => {
83
+ popup?.destroy();
84
+ popup?.element?.remove?.();
85
+ popup = null;
86
+ uiRef.current = null;
87
+ },
88
+ onKeyDown: ({ event }) =>
89
+ uiRef.current?.onKeyboardEvent(event as KeyboardEvent) ?? false,
90
+ };
91
+ }
92
+
93
+ function insertDefaultMention(
94
+ editor: Editor,
95
+ range: { from: number; to: number },
96
+ props: SlashCommandItem,
97
+ slashChar: string,
98
+ ): void {
99
+ const nodeAfter = editor.view.state.selection.$to.nodeAfter;
100
+ const extend = nodeAfter?.text?.startsWith(' ') ? 1 : 0;
101
+ const adjusted = extend ? { ...range, to: range.to + extend } : range;
102
+
103
+ editor
104
+ .chain()
105
+ .focus()
106
+ .insertContentAt(adjusted, [
107
+ {
108
+ type: 'mention',
109
+ attrs: {
110
+ id: props.id,
111
+ label: props.label,
112
+ mentionSuggestionChar: slashChar,
113
+ },
114
+ },
115
+ { type: 'text', text: ' ' },
116
+ ])
117
+ .run();
118
+
119
+ queueMicrotask(() => {
120
+ editor.view.dom.ownerDocument?.defaultView
121
+ ?.getSelection?.()
122
+ ?.collapseToEnd();
123
+ });
124
+ }
125
+
126
+ function allowSlashTrigger({
127
+ state,
128
+ range,
129
+ }: {
130
+ editor: Editor;
131
+ state: EditorState;
132
+ range: { from: number; to: number };
133
+ }) {
134
+ const type = state.schema.nodes['mention'];
135
+ const $from = state.doc.resolve(range.from);
136
+ if (!type || !$from.parent.type.contentMatch.matchType(type)) return false;
137
+ if ($from.parentOffset === 0) return true;
138
+ const before = state.doc.textBetween(range.from - 1, range.from);
139
+ return /\s/.test(before);
140
+ }
141
+
142
+ export type CreateSlashMentionExtensionConfiguredOptions =
143
+ CreateSlashMentionExtensionOptions & {
144
+ onSuggestionUiActiveChange?: (active: boolean) => void;
145
+ };
146
+
147
+ export function createSlashMentionExtension({
148
+ items: resolvedItems,
149
+ slashChar = '/',
150
+ pluginKey,
151
+ onItemCommand,
152
+ onSuggestionUiActiveChange,
153
+ }: CreateSlashMentionExtensionConfiguredOptions) {
154
+ const uiRef: MutableRefObject<SlashSuggestionListHandle | null> = {
155
+ current: null,
156
+ };
157
+
158
+ return Mention.configure({
159
+ renderText({ node }) {
160
+ return `/${node.attrs.id as string}`;
161
+ },
162
+ renderHTML({ options, node, suggestion }) {
163
+ const suggestionChar = suggestion?.char ?? slashChar;
164
+ const id =
165
+ typeof node.attrs.id === 'string'
166
+ ? node.attrs.id
167
+ : String(node.attrs.id ?? '');
168
+ return [
169
+ 'span',
170
+ mergeAttributes(
171
+ {
172
+ 'data-type': 'mention',
173
+ 'data-slash-command': id,
174
+ class: 'slash-mention',
175
+ },
176
+ options.HTMLAttributes,
177
+ ),
178
+ `${suggestionChar}${id}`,
179
+ ];
180
+ },
181
+ suggestion: {
182
+ char: slashChar,
183
+ pluginKey,
184
+ allowedPrefixes: [' ', '\n', '\t', '\r'],
185
+ allow: props => allowSlashTrigger(props),
186
+ items: ({ query }) =>
187
+ Promise.resolve(filterSlashItems(resolvedItems, query)),
188
+ command: ({ editor, range, props }) => {
189
+ const item = props as SlashCommandItem;
190
+ if (onItemCommand?.({ editor, range, item }) === true) {
191
+ queueMicrotask(() => {
192
+ editor.view.dom.ownerDocument?.defaultView
193
+ ?.getSelection?.()
194
+ ?.collapseToEnd();
195
+ });
196
+ return null;
197
+ }
198
+ insertDefaultMention(editor, range, item, slashChar);
199
+ return null;
200
+ },
201
+ render: () => {
202
+ const menu = slashMentionSuggestionRender(uiRef);
203
+ return {
204
+ ...menu,
205
+ onStart: props => {
206
+ onSuggestionUiActiveChange?.(true);
207
+ menu.onStart?.(props);
208
+ },
209
+ onExit: props => {
210
+ menu.onExit?.(props);
211
+ onSuggestionUiActiveChange?.(false);
212
+ },
213
+ };
214
+ },
215
+ },
216
+ });
217
+ }
@@ -0,0 +1,18 @@
1
+ import type { SlashCommandItem } from './types';
2
+
3
+ /** Empty default: pass `slashCommandItems` from the app to enable `/` palette. */
4
+ export const DEFAULT_CHAT_SLASH_ITEMS: SlashCommandItem[] = [];
5
+
6
+ export function filterSlashItems(
7
+ items: SlashCommandItem[],
8
+ query: string,
9
+ ): SlashCommandItem[] {
10
+ const q = query.trim().toLowerCase();
11
+ if (!q) return items;
12
+ return items.filter(
13
+ item =>
14
+ item.id.toLowerCase().includes(q) ||
15
+ item.label.toLowerCase().includes(q) ||
16
+ (item.description?.toLowerCase().includes(q) ?? false),
17
+ );
18
+ }
@@ -0,0 +1,16 @@
1
+ export type {
2
+ SlashCommandItem,
3
+ SlashOnItemCommand,
4
+ SlashItemCommandContext,
5
+ CreateSlashMentionExtensionOptions,
6
+ } from './types';
7
+ export {
8
+ DEFAULT_CHAT_SLASH_ITEMS,
9
+ filterSlashItems,
10
+ } from './defaultChatSlashItems';
11
+ export {
12
+ createSlashMentionExtension,
13
+ slashMentionSuggestionRender,
14
+ } from './createSlashMentionExtension';
15
+ export type { CreateSlashMentionExtensionConfiguredOptions } from './createSlashMentionExtension';
16
+ export type { SlashSuggestionListHandle } from './SlashSuggestionList';
@@ -0,0 +1,29 @@
1
+ import type { Editor, Range } from '@tiptap/core';
2
+
3
+ export type SlashCommandItem = {
4
+ id: string;
5
+ label: string;
6
+ description?: string;
7
+ };
8
+
9
+ export type SlashItemCommandContext = {
10
+ editor: Editor;
11
+ range: Range;
12
+ item: SlashCommandItem;
13
+ };
14
+
15
+ /**
16
+ * If provided, run before default mention insertion. Return true to skip inserting a mention node.
17
+ */
18
+ export type SlashOnItemCommand = (ctx: SlashItemCommandContext) => boolean;
19
+
20
+ export type CreateSlashMentionExtensionOptions = {
21
+ /** Items shown in the slash menu (filtered by query after `/`). */
22
+ items: SlashCommandItem[];
23
+ /** Trigger character; use `/` for slash commands. */
24
+ slashChar?: string;
25
+ /** Optional custom plugin key when multiple slash editors exist. */
26
+ pluginKey?: import('@tiptap/pm/state').PluginKey;
27
+ /** Custom handler (e.g. insert a block node instead of a mention). */
28
+ onItemCommand?: SlashOnItemCommand;
29
+ };