@sybilion/uilib 1.3.36 → 1.3.37
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/ChatPrompt/useChatPromptEditor.js +1 -0
- package/dist/esm/components/ui/Chat/ChatSheet/ChatSheet.js +2 -1
- package/dist/esm/components/ui/Chat/ChatSheet/useChatPanelChromeModel.js +2 -1
- package/dist/esm/tiptap/slash-mention/createSlashMentionExtension.js +24 -9
- 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 +4 -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 +7 -0
- package/package.json +1 -1
- package/src/components/ui/Chat/ChatPrompt/useChatPromptEditor.ts +1 -0
- package/src/components/ui/Chat/ChatSheet/ChatSheet.tsx +2 -0
- package/src/components/ui/Chat/ChatSheet/useChatPanelChromeModel.tsx +99 -90
- package/src/docs/docsHeaderActions.tsx +3 -2
- package/src/docs/pages/ChatSlashCommandsPage.tsx +3 -1
- package/src/tiptap/slash-mention/createSlashMentionExtension.ts +41 -6
- package/src/tiptap/slash-mention/index.ts +1 -0
- package/src/tiptap/slash-mention/types.ts +8 -0
|
@@ -48,6 +48,7 @@ function useChatPromptEditor({ disabled, placeholder, slashCommandItems, prefill
|
|
|
48
48
|
if (slashItemsStable.length > 0) {
|
|
49
49
|
exts.push(createSlashMentionExtension({
|
|
50
50
|
items: slashItemsStable,
|
|
51
|
+
suggestionPlacement: 'above',
|
|
51
52
|
onSuggestionUiActiveChange: suggestionActiveUpdater,
|
|
52
53
|
}));
|
|
53
54
|
}
|
|
@@ -4,7 +4,7 @@ import { Button } from '../../Button/Button.js';
|
|
|
4
4
|
import { ChatChrome } from '../ChatChrome/ChatChrome.js';
|
|
5
5
|
import { useChatPanelChromeModel } from './useChatPanelChromeModel.js';
|
|
6
6
|
|
|
7
|
-
function ChatSheet({ triggerLabel = 'Open Chat', triggerAriaLabel, actionsRef, renderTrigger, presets, scopeId, onMessage, onScriptComplete, onGenerateDashboard, renderMessageChart, emptyState, allowedAttachments, allowPdfAttachments, onAttachmentsDropped, inline = false, }) {
|
|
7
|
+
function ChatSheet({ triggerLabel = 'Open Chat', triggerAriaLabel, actionsRef, renderTrigger, presets, scopeId, onMessage, onScriptComplete, onGenerateDashboard, renderMessageChart, emptyState, allowedAttachments, allowPdfAttachments, onAttachmentsDropped, slashCommandItems, inline = false, }) {
|
|
8
8
|
const model = useChatPanelChromeModel({
|
|
9
9
|
embedAsPage: inline,
|
|
10
10
|
presets,
|
|
@@ -17,6 +17,7 @@ function ChatSheet({ triggerLabel = 'Open Chat', triggerAriaLabel, actionsRef, r
|
|
|
17
17
|
allowedAttachments,
|
|
18
18
|
allowPdfAttachments,
|
|
19
19
|
onAttachmentsDropped,
|
|
20
|
+
slashCommandItems,
|
|
20
21
|
});
|
|
21
22
|
if (actionsRef) {
|
|
22
23
|
actionsRef.current = {
|
|
@@ -22,7 +22,7 @@ const CHAT_NAV_COLLAPSE_BREAKPOINT_PX = 1400;
|
|
|
22
22
|
const CHAT_QUERY_PARAM = 'chat';
|
|
23
23
|
const CHAT_OPEN_VALUE = 'open';
|
|
24
24
|
const PROMPT_QUERY_PARAM = 'prompt';
|
|
25
|
-
function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onScriptComplete, onGenerateDashboard, renderMessageChart, emptyState, allowedAttachments, allowPdfAttachments, onAttachmentsDropped, }) {
|
|
25
|
+
function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onScriptComplete, onGenerateDashboard, renderMessageChart, emptyState, allowedAttachments, allowPdfAttachments, onAttachmentsDropped, slashCommandItems, }) {
|
|
26
26
|
const effectiveScopeId = scopeId ?? NO_SCOPE_FALLBACK;
|
|
27
27
|
const isMobile = useIsMobile();
|
|
28
28
|
const { chatPanelContainer, isOpen: sidebarNavOpen, setOpen: setSidebarNavOpen, chatWidthPx, setChatWidthPx, getShellWidth, setChatPanelOpen, } = useSidebar();
|
|
@@ -786,6 +786,7 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
|
|
|
786
786
|
allowedAttachments,
|
|
787
787
|
allowPdfAttachments,
|
|
788
788
|
onAttachmentsDropped,
|
|
789
|
+
slashCommandItems,
|
|
789
790
|
};
|
|
790
791
|
const toggleOpen = () => onOpenChange(!isOpen);
|
|
791
792
|
return {
|
|
@@ -4,17 +4,30 @@ import { ReactRenderer } from '@tiptap/react';
|
|
|
4
4
|
import { SlashSuggestionList } from './SlashSuggestionList.js';
|
|
5
5
|
import { filterSlashItems } from './defaultChatSlashItems.js';
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
const SUGGESTION_GAP_PX = 4;
|
|
8
|
+
function placeSlashSuggestionPopup(popupElement, clientRect, placement) {
|
|
9
|
+
if (!clientRect)
|
|
10
|
+
return;
|
|
11
|
+
const el = popupElement;
|
|
12
|
+
const popupHeight = el.getBoundingClientRect().height;
|
|
13
|
+
const spaceBelow = window.innerHeight - clientRect.bottom - SUGGESTION_GAP_PX;
|
|
14
|
+
const spaceAbove = clientRect.top - SUGGESTION_GAP_PX;
|
|
15
|
+
let showAbove = placement === 'above';
|
|
16
|
+
if (placement === 'auto') {
|
|
17
|
+
showAbove = spaceBelow < popupHeight && spaceAbove >= spaceBelow;
|
|
18
|
+
}
|
|
19
|
+
const top = showAbove
|
|
20
|
+
? Math.max(SUGGESTION_GAP_PX, clientRect.top - popupHeight - SUGGESTION_GAP_PX)
|
|
21
|
+
: clientRect.bottom + SUGGESTION_GAP_PX;
|
|
22
|
+
el.style.left = `${clientRect.left}px`;
|
|
23
|
+
el.style.top = `${top}px`;
|
|
24
|
+
}
|
|
25
|
+
function slashMentionSuggestionRender(uiRef, placement = 'below') {
|
|
8
26
|
let popup = null;
|
|
9
27
|
const place = (props) => {
|
|
10
28
|
if (!popup?.element)
|
|
11
29
|
return;
|
|
12
|
-
|
|
13
|
-
if (!rect)
|
|
14
|
-
return;
|
|
15
|
-
const el = popup.element;
|
|
16
|
-
el.style.left = `${rect.left}px`;
|
|
17
|
-
el.style.top = `${rect.bottom + 4}px`;
|
|
30
|
+
placeSlashSuggestionPopup(popup.element, props.clientRect?.() ?? null, placement);
|
|
18
31
|
};
|
|
19
32
|
return {
|
|
20
33
|
onStart: props => {
|
|
@@ -33,6 +46,7 @@ function slashMentionSuggestionRender(uiRef) {
|
|
|
33
46
|
popup.element.style.zIndex = '10002';
|
|
34
47
|
document.body.append(popup.element);
|
|
35
48
|
place(props);
|
|
49
|
+
requestAnimationFrame(() => place(props));
|
|
36
50
|
},
|
|
37
51
|
onUpdate: props => {
|
|
38
52
|
if (!popup)
|
|
@@ -43,6 +57,7 @@ function slashMentionSuggestionRender(uiRef) {
|
|
|
43
57
|
listHandleRef: uiRef,
|
|
44
58
|
});
|
|
45
59
|
place(props);
|
|
60
|
+
requestAnimationFrame(() => place(props));
|
|
46
61
|
},
|
|
47
62
|
onExit: () => {
|
|
48
63
|
popup?.destroy();
|
|
@@ -88,7 +103,7 @@ function allowSlashTrigger({ state, range, }) {
|
|
|
88
103
|
const before = state.doc.textBetween(range.from - 1, range.from);
|
|
89
104
|
return /\s/.test(before);
|
|
90
105
|
}
|
|
91
|
-
function createSlashMentionExtension({ items: resolvedItems, slashChar = '/', pluginKey, onItemCommand, onSuggestionUiActiveChange, }) {
|
|
106
|
+
function createSlashMentionExtension({ items: resolvedItems, slashChar = '/', pluginKey, onItemCommand, suggestionPlacement = 'below', onSuggestionUiActiveChange, }) {
|
|
92
107
|
const uiRef = {
|
|
93
108
|
current: null,
|
|
94
109
|
};
|
|
@@ -131,7 +146,7 @@ function createSlashMentionExtension({ items: resolvedItems, slashChar = '/', pl
|
|
|
131
146
|
return null;
|
|
132
147
|
},
|
|
133
148
|
render: () => {
|
|
134
|
-
const menu = slashMentionSuggestionRender(uiRef);
|
|
149
|
+
const menu = slashMentionSuggestionRender(uiRef, suggestionPlacement);
|
|
135
150
|
return {
|
|
136
151
|
...menu,
|
|
137
152
|
onStart: props => {
|
|
@@ -19,4 +19,4 @@ export interface ChatSheetProps extends Omit<UseChatPanelChromeModelInput, 'embe
|
|
|
19
19
|
*/
|
|
20
20
|
inline?: boolean;
|
|
21
21
|
}
|
|
22
|
-
export declare function ChatSheet({ triggerLabel, triggerAriaLabel, actionsRef, renderTrigger, presets, scopeId, onMessage, onScriptComplete, onGenerateDashboard, renderMessageChart, emptyState, allowedAttachments, allowPdfAttachments, onAttachmentsDropped, inline, }: ChatSheetProps): import("react/jsx-runtime").JSX.Element;
|
|
22
|
+
export declare function ChatSheet({ triggerLabel, triggerAriaLabel, actionsRef, renderTrigger, presets, scopeId, onMessage, onScriptComplete, onGenerateDashboard, renderMessageChart, emptyState, allowedAttachments, allowPdfAttachments, onAttachmentsDropped, slashCommandItems, inline, }: ChatSheetProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ChatPreset, type ScriptCompletePayload } from '#uilib/components/ui/Chat/Chat.types';
|
|
2
|
+
import type { SlashCommandItem } from '#uilib/tiptap/slash-mention/types';
|
|
2
3
|
import type { ChatChromeProps } from '../ChatChrome';
|
|
3
4
|
import type { ChatAttachmentDropItem } from '../ChatChrome/ChatChrome.types';
|
|
4
5
|
import type { ChatEmptyStateConfig } from '../ChatEmptyState/ChatEmptyState.types';
|
|
@@ -22,6 +23,8 @@ export type UseChatPanelChromeModelInput = {
|
|
|
22
23
|
/** When true, PDF drops are accepted and parsed to plain text. */
|
|
23
24
|
allowPdfAttachments?: boolean;
|
|
24
25
|
onAttachmentsDropped?: (items: ChatAttachmentDropItem[]) => void | Promise<void>;
|
|
26
|
+
/** Slash menu (`/`) in the composer; omit or pass empty to disable. */
|
|
27
|
+
slashCommandItems?: SlashCommandItem[];
|
|
25
28
|
};
|
|
26
29
|
export type UseChatPanelChromeModelResult = {
|
|
27
30
|
chromeProps: ChatChromeProps;
|
|
@@ -31,4 +34,4 @@ export type UseChatPanelChromeModelResult = {
|
|
|
31
34
|
newChat: () => void;
|
|
32
35
|
chatPanelContainer: HTMLElement | null;
|
|
33
36
|
};
|
|
34
|
-
export declare function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onScriptComplete, onGenerateDashboard, renderMessageChart, emptyState, allowedAttachments, allowPdfAttachments, onAttachmentsDropped, }: UseChatPanelChromeModelInput): UseChatPanelChromeModelResult;
|
|
37
|
+
export declare function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onScriptComplete, onGenerateDashboard, renderMessageChart, emptyState, allowedAttachments, allowPdfAttachments, onAttachmentsDropped, slashCommandItems, }: UseChatPanelChromeModelInput): UseChatPanelChromeModelResult;
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
import { ChatSheetProps } from '#uilib/components/ui/Chat';
|
|
2
|
+
export declare function DocsHeaderActions(props: ChatSheetProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { MutableRefObject } from 'react';
|
|
2
2
|
import type { SuggestionProps } from '@tiptap/suggestion';
|
|
3
3
|
import { type SlashSuggestionListHandle } from './SlashSuggestionList';
|
|
4
|
-
import type { CreateSlashMentionExtensionOptions, SlashCommandItem } from './types';
|
|
5
|
-
export declare function slashMentionSuggestionRender(uiRef: MutableRefObject<SlashSuggestionListHandle | null
|
|
4
|
+
import type { CreateSlashMentionExtensionOptions, SlashCommandItem, SlashSuggestionPlacement } from './types';
|
|
5
|
+
export declare function slashMentionSuggestionRender(uiRef: MutableRefObject<SlashSuggestionListHandle | null>, placement?: SlashSuggestionPlacement): {
|
|
6
6
|
onStart?: (props: SuggestionProps<SlashCommandItem, SlashCommandItem>) => void;
|
|
7
7
|
onUpdate?: (props: SuggestionProps<SlashCommandItem, SlashCommandItem>) => void;
|
|
8
8
|
onExit?: (props: SuggestionProps<SlashCommandItem, SlashCommandItem>) => void;
|
|
@@ -18,4 +18,4 @@ export declare function slashMentionSuggestionRender(uiRef: MutableRefObject<Sla
|
|
|
18
18
|
export type CreateSlashMentionExtensionConfiguredOptions = CreateSlashMentionExtensionOptions & {
|
|
19
19
|
onSuggestionUiActiveChange?: (active: boolean) => void;
|
|
20
20
|
};
|
|
21
|
-
export declare function createSlashMentionExtension({ items: resolvedItems, slashChar, pluginKey, onItemCommand, onSuggestionUiActiveChange, }: CreateSlashMentionExtensionConfiguredOptions): import("@tiptap/core").Node<import("@tiptap/extension-mention").MentionOptions<any, import("@tiptap/extension-mention").MentionNodeAttrs>, any>;
|
|
21
|
+
export declare function createSlashMentionExtension({ items: resolvedItems, slashChar, pluginKey, onItemCommand, suggestionPlacement, onSuggestionUiActiveChange, }: CreateSlashMentionExtensionConfiguredOptions): import("@tiptap/core").Node<import("@tiptap/extension-mention").MentionOptions<any, import("@tiptap/extension-mention").MentionNodeAttrs>, any>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type { SlashCommandItem, SlashOnItemCommand, SlashItemCommandContext, CreateSlashMentionExtensionOptions, } from './types';
|
|
1
|
+
export type { SlashCommandItem, SlashOnItemCommand, SlashItemCommandContext, SlashSuggestionPlacement, CreateSlashMentionExtensionOptions, } from './types';
|
|
2
2
|
export { DEFAULT_CHAT_SLASH_ITEMS, filterSlashItems, } from './defaultChatSlashItems';
|
|
3
3
|
export { createSlashMentionExtension, slashMentionSuggestionRender, } from './createSlashMentionExtension';
|
|
4
4
|
export type { CreateSlashMentionExtensionConfiguredOptions } from './createSlashMentionExtension';
|
|
@@ -13,6 +13,8 @@ export type SlashItemCommandContext = {
|
|
|
13
13
|
* If provided, run before default mention insertion. Return true to skip inserting a mention node.
|
|
14
14
|
*/
|
|
15
15
|
export type SlashOnItemCommand = (ctx: SlashItemCommandContext) => boolean;
|
|
16
|
+
/** Where the slash palette opens relative to the caret. */
|
|
17
|
+
export type SlashSuggestionPlacement = 'below' | 'above' | 'auto';
|
|
16
18
|
export type CreateSlashMentionExtensionOptions = {
|
|
17
19
|
/** Items shown in the slash menu (filtered by query after `/`). */
|
|
18
20
|
items: SlashCommandItem[];
|
|
@@ -22,4 +24,9 @@ export type CreateSlashMentionExtensionOptions = {
|
|
|
22
24
|
pluginKey?: import('@tiptap/pm/state').PluginKey;
|
|
23
25
|
/** Custom handler (e.g. insert a block node instead of a mention). */
|
|
24
26
|
onItemCommand?: SlashOnItemCommand;
|
|
27
|
+
/**
|
|
28
|
+
* Palette position vs caret. Default `below`.
|
|
29
|
+
* Use `above` for bottom-anchored composers (chat prompt); `auto` flips by viewport space.
|
|
30
|
+
*/
|
|
31
|
+
suggestionPlacement?: SlashSuggestionPlacement;
|
|
25
32
|
};
|
package/package.json
CHANGED
|
@@ -47,6 +47,7 @@ export function ChatSheet({
|
|
|
47
47
|
allowedAttachments,
|
|
48
48
|
allowPdfAttachments,
|
|
49
49
|
onAttachmentsDropped,
|
|
50
|
+
slashCommandItems,
|
|
50
51
|
inline = false,
|
|
51
52
|
}: ChatSheetProps) {
|
|
52
53
|
const model = useChatPanelChromeModel({
|
|
@@ -61,6 +62,7 @@ export function ChatSheet({
|
|
|
61
62
|
allowedAttachments,
|
|
62
63
|
allowPdfAttachments,
|
|
63
64
|
onAttachmentsDropped,
|
|
65
|
+
slashCommandItems,
|
|
64
66
|
});
|
|
65
67
|
|
|
66
68
|
if (actionsRef) {
|
|
@@ -38,6 +38,7 @@ import useEvent from '#uilib/hooks/useEvent';
|
|
|
38
38
|
import { useIsMobile } from '#uilib/hooks/useIsMobile';
|
|
39
39
|
import { useQueryParams } from '#uilib/hooks/useQueryParams';
|
|
40
40
|
import logger from '#uilib/lib/logger';
|
|
41
|
+
import type { SlashCommandItem } from '#uilib/tiptap/slash-mention/types';
|
|
41
42
|
import { mergePresetFreeformDefaults } from '#uilib/utils/chatPresetMerge';
|
|
42
43
|
import { ScrollRef } from '@homecode/ui';
|
|
43
44
|
|
|
@@ -70,6 +71,8 @@ export type UseChatPanelChromeModelInput = {
|
|
|
70
71
|
onAttachmentsDropped?: (
|
|
71
72
|
items: ChatAttachmentDropItem[],
|
|
72
73
|
) => void | Promise<void>;
|
|
74
|
+
/** Slash menu (`/`) in the composer; omit or pass empty to disable. */
|
|
75
|
+
slashCommandItems?: SlashCommandItem[];
|
|
73
76
|
};
|
|
74
77
|
|
|
75
78
|
export type UseChatPanelChromeModelResult = {
|
|
@@ -118,6 +121,7 @@ export function useChatPanelChromeModel({
|
|
|
118
121
|
allowedAttachments,
|
|
119
122
|
allowPdfAttachments,
|
|
120
123
|
onAttachmentsDropped,
|
|
124
|
+
slashCommandItems,
|
|
121
125
|
}: UseChatPanelChromeModelInput): UseChatPanelChromeModelResult {
|
|
122
126
|
const effectiveScopeId = scopeId ?? NO_SCOPE_FALLBACK;
|
|
123
127
|
const isMobile = useIsMobile();
|
|
@@ -616,103 +620,107 @@ export function useChatPanelChromeModel({
|
|
|
616
620
|
],
|
|
617
621
|
);
|
|
618
622
|
|
|
619
|
-
const submitPreset = useCallback(
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
623
|
+
const submitPreset = useCallback(
|
|
624
|
+
async (preset: ChatPreset) => {
|
|
625
|
+
const script = preset.script;
|
|
626
|
+
const scriptGraph = isPresetScriptGraph(script);
|
|
627
|
+
const hasLinearScript = Array.isArray(script) && script.length > 0;
|
|
628
|
+
const hasReplies =
|
|
629
|
+
preset.replies && Object.keys(preset.replies).length > 0;
|
|
630
|
+
const isLocalDemo =
|
|
631
|
+
hasLinearScript ||
|
|
632
|
+
scriptGraph ||
|
|
633
|
+
Boolean(preset.answer?.trim()) ||
|
|
634
|
+
Boolean(hasReplies);
|
|
635
|
+
|
|
636
|
+
if (!isLocalDemo) {
|
|
637
|
+
if (!currentChatId) return;
|
|
638
|
+
endLocalDemoFlow(currentChatId);
|
|
639
|
+
await handlePromptSubmit(preset.text);
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
636
642
|
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
delete next[currentChatId];
|
|
643
|
-
return next;
|
|
644
|
-
});
|
|
645
|
-
addMessage(currentChatId, MessageRole.USER, preset.text);
|
|
646
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
647
|
-
const firstAssistant = scriptGraph
|
|
648
|
-
? script.initialMessage
|
|
649
|
-
: (preset.answer ?? '');
|
|
650
|
-
addMessage(currentChatId, MessageRole.ASSISTANT, firstAssistant);
|
|
651
|
-
|
|
652
|
-
setScriptByChatId(prev => {
|
|
653
|
-
const next = { ...prev };
|
|
654
|
-
if (hasLinearScript) {
|
|
655
|
-
next[currentChatId] = {
|
|
656
|
-
lines: script,
|
|
657
|
-
nextIndex: 0,
|
|
658
|
-
presetId: preset.id,
|
|
659
|
-
};
|
|
660
|
-
} else {
|
|
643
|
+
setLocalUiBusy(true);
|
|
644
|
+
try {
|
|
645
|
+
if (!currentChatId) return;
|
|
646
|
+
setScriptCompleteByChatId(prev => {
|
|
647
|
+
const next = { ...prev };
|
|
661
648
|
delete next[currentChatId];
|
|
649
|
+
return next;
|
|
650
|
+
});
|
|
651
|
+
addMessage(currentChatId, MessageRole.USER, preset.text);
|
|
652
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
653
|
+
const firstAssistant = scriptGraph
|
|
654
|
+
? script.initialMessage
|
|
655
|
+
: (preset.answer ?? '');
|
|
656
|
+
addMessage(currentChatId, MessageRole.ASSISTANT, firstAssistant);
|
|
657
|
+
|
|
658
|
+
setScriptByChatId(prev => {
|
|
659
|
+
const next = { ...prev };
|
|
660
|
+
if (hasLinearScript) {
|
|
661
|
+
next[currentChatId] = {
|
|
662
|
+
lines: script,
|
|
663
|
+
nextIndex: 0,
|
|
664
|
+
presetId: preset.id,
|
|
665
|
+
};
|
|
666
|
+
} else {
|
|
667
|
+
delete next[currentChatId];
|
|
668
|
+
}
|
|
669
|
+
return next;
|
|
670
|
+
});
|
|
671
|
+
setQuickReplyBranchesByChat(prev => {
|
|
672
|
+
const next = { ...prev };
|
|
673
|
+
if (scriptGraph) {
|
|
674
|
+
next[currentChatId] = branchesFromPresetScriptGraph(script);
|
|
675
|
+
} else if (hasReplies && !hasLinearScript) {
|
|
676
|
+
next[currentChatId] = preset.replies!;
|
|
677
|
+
} else {
|
|
678
|
+
delete next[currentChatId];
|
|
679
|
+
}
|
|
680
|
+
return next;
|
|
681
|
+
});
|
|
682
|
+
if (scriptGraph || (hasReplies && !hasLinearScript)) {
|
|
683
|
+
setUsedScriptBranchKeysByChat(prev => ({
|
|
684
|
+
...prev,
|
|
685
|
+
[currentChatId]: [],
|
|
686
|
+
}));
|
|
662
687
|
}
|
|
663
|
-
return next;
|
|
664
|
-
});
|
|
665
|
-
setQuickReplyBranchesByChat(prev => {
|
|
666
|
-
const next = { ...prev };
|
|
667
688
|
if (scriptGraph) {
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
...prev,
|
|
679
|
-
[currentChatId]: [],
|
|
680
|
-
}));
|
|
681
|
-
}
|
|
682
|
-
if (scriptGraph) {
|
|
683
|
-
const fromMerged = presetsWithFreeform.find(p => p.id === preset.id);
|
|
684
|
-
let freeformNext = {
|
|
685
|
-
...(fromMerged?.freeformNext ?? {}),
|
|
686
|
-
...(preset.freeformNext ?? {}),
|
|
687
|
-
};
|
|
688
|
-
if (Object.keys(freeformNext).length === 0) {
|
|
689
|
-
const ks = Object.keys(branchesFromPresetScriptGraph(script));
|
|
690
|
-
if (ks.length === 1) {
|
|
691
|
-
freeformNext = { initial: ks[0] };
|
|
689
|
+
const fromMerged = presetsWithFreeform.find(p => p.id === preset.id);
|
|
690
|
+
let freeformNext = {
|
|
691
|
+
...(fromMerged?.freeformNext ?? {}),
|
|
692
|
+
...(preset.freeformNext ?? {}),
|
|
693
|
+
};
|
|
694
|
+
if (Object.keys(freeformNext).length === 0) {
|
|
695
|
+
const ks = Object.keys(branchesFromPresetScriptGraph(script));
|
|
696
|
+
if (ks.length === 1) {
|
|
697
|
+
freeformNext = { initial: ks[0] };
|
|
698
|
+
}
|
|
692
699
|
}
|
|
700
|
+
setIntakeByChatId(prev => ({
|
|
701
|
+
...prev,
|
|
702
|
+
[currentChatId!]: {
|
|
703
|
+
presetId: preset.id,
|
|
704
|
+
freeformNext,
|
|
705
|
+
scriptStepId: 'initial',
|
|
706
|
+
answers: {},
|
|
707
|
+
},
|
|
708
|
+
}));
|
|
693
709
|
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
freeformNext,
|
|
699
|
-
scriptStepId: 'initial',
|
|
700
|
-
answers: {},
|
|
701
|
-
},
|
|
702
|
-
}));
|
|
710
|
+
} catch (error) {
|
|
711
|
+
logger.error('Error sending chat message:', error);
|
|
712
|
+
} finally {
|
|
713
|
+
setLocalUiBusy(false);
|
|
703
714
|
}
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
addMessage,
|
|
714
|
-
presetsWithFreeform,
|
|
715
|
-
]);
|
|
715
|
+
},
|
|
716
|
+
[
|
|
717
|
+
currentChatId,
|
|
718
|
+
endLocalDemoFlow,
|
|
719
|
+
handlePromptSubmit,
|
|
720
|
+
addMessage,
|
|
721
|
+
presetsWithFreeform,
|
|
722
|
+
],
|
|
723
|
+
);
|
|
716
724
|
|
|
717
725
|
const resolvedEmptyState = useMemo((): ChatEmptyStateProps | undefined => {
|
|
718
726
|
if (!emptyState) return undefined;
|
|
@@ -1040,6 +1048,7 @@ export function useChatPanelChromeModel({
|
|
|
1040
1048
|
allowedAttachments,
|
|
1041
1049
|
allowPdfAttachments,
|
|
1042
1050
|
onAttachmentsDropped,
|
|
1051
|
+
slashCommandItems,
|
|
1043
1052
|
};
|
|
1044
1053
|
|
|
1045
1054
|
const toggleOpen = () => onOpenChange(!isOpen);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { ChatSheet } from '#uilib/components/ui/Chat';
|
|
1
|
+
import { ChatSheet, ChatSheetProps } from '#uilib/components/ui/Chat';
|
|
2
2
|
import { MessageSquare } from 'lucide-react';
|
|
3
3
|
|
|
4
4
|
import { DOCS_CHAT_SCOPE_ID } from './docsConstants';
|
|
5
5
|
|
|
6
|
-
export function DocsHeaderActions() {
|
|
6
|
+
export function DocsHeaderActions(props: ChatSheetProps) {
|
|
7
7
|
return (
|
|
8
8
|
<ChatSheet
|
|
9
9
|
triggerLabel={
|
|
@@ -21,6 +21,7 @@ export function DocsHeaderActions() {
|
|
|
21
21
|
<p>Optional empty-state slot via additionalContent.</p>
|
|
22
22
|
),
|
|
23
23
|
}}
|
|
24
|
+
{...props}
|
|
24
25
|
/>
|
|
25
26
|
);
|
|
26
27
|
}
|
|
@@ -85,7 +85,9 @@ export default function ChatSlashCommandsPage() {
|
|
|
85
85
|
breadcrumbs={[{ label: 'Chat' }, { label: 'Chat slash commands' }]}
|
|
86
86
|
title="Chat slash commands"
|
|
87
87
|
subheader={`Slash palette uses TipTap Mention with "/" as the trigger. Library default item list is empty — pass slashCommandItems from the app (this page uses a sample item for the demo).`}
|
|
88
|
-
actions={
|
|
88
|
+
actions={
|
|
89
|
+
<DocsHeaderActions slashCommandItems={DOCS_SAMPLE_SLASH_ITEMS} />
|
|
90
|
+
}
|
|
89
91
|
/>
|
|
90
92
|
<PageContentSection>
|
|
91
93
|
<p style={{ marginBottom: 16, fontSize: 14, lineHeight: 1.5 }}>
|
|
@@ -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();
|
|
@@ -149,6 +183,7 @@ export function createSlashMentionExtension({
|
|
|
149
183
|
slashChar = '/',
|
|
150
184
|
pluginKey,
|
|
151
185
|
onItemCommand,
|
|
186
|
+
suggestionPlacement = 'below',
|
|
152
187
|
onSuggestionUiActiveChange,
|
|
153
188
|
}: CreateSlashMentionExtensionConfiguredOptions) {
|
|
154
189
|
const uiRef: MutableRefObject<SlashSuggestionListHandle | null> = {
|
|
@@ -199,7 +234,7 @@ export function createSlashMentionExtension({
|
|
|
199
234
|
return null;
|
|
200
235
|
},
|
|
201
236
|
render: () => {
|
|
202
|
-
const menu = slashMentionSuggestionRender(uiRef);
|
|
237
|
+
const menu = slashMentionSuggestionRender(uiRef, suggestionPlacement);
|
|
203
238
|
return {
|
|
204
239
|
...menu,
|
|
205
240
|
onStart: props => {
|
|
@@ -17,6 +17,9 @@ export type SlashItemCommandContext = {
|
|
|
17
17
|
*/
|
|
18
18
|
export type SlashOnItemCommand = (ctx: SlashItemCommandContext) => boolean;
|
|
19
19
|
|
|
20
|
+
/** Where the slash palette opens relative to the caret. */
|
|
21
|
+
export type SlashSuggestionPlacement = 'below' | 'above' | 'auto';
|
|
22
|
+
|
|
20
23
|
export type CreateSlashMentionExtensionOptions = {
|
|
21
24
|
/** Items shown in the slash menu (filtered by query after `/`). */
|
|
22
25
|
items: SlashCommandItem[];
|
|
@@ -26,4 +29,9 @@ export type CreateSlashMentionExtensionOptions = {
|
|
|
26
29
|
pluginKey?: import('@tiptap/pm/state').PluginKey;
|
|
27
30
|
/** Custom handler (e.g. insert a block node instead of a mention). */
|
|
28
31
|
onItemCommand?: SlashOnItemCommand;
|
|
32
|
+
/**
|
|
33
|
+
* Palette position vs caret. Default `below`.
|
|
34
|
+
* Use `above` for bottom-anchored composers (chat prompt); `auto` flips by viewport space.
|
|
35
|
+
*/
|
|
36
|
+
suggestionPlacement?: SlashSuggestionPlacement;
|
|
29
37
|
};
|