@sybilion/uilib 1.3.38 → 1.3.40

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 (26) hide show
  1. package/dist/esm/components/ui/Chat/Chat.types.js +1 -8
  2. package/dist/esm/components/ui/Chat/ChatChrome/ChatChrome.js +1 -1
  3. package/dist/esm/components/ui/Chat/ChatMessage/ChatMessage.js +3 -3
  4. package/dist/esm/components/ui/Chat/ChatSheet/ChatSheet.js +2 -2
  5. package/dist/esm/components/ui/Chat/ChatSheet/useChatPanelChromeModel.js +7 -54
  6. package/dist/esm/contexts/chat-context.js +1 -0
  7. package/dist/esm/index.js +2 -2
  8. package/dist/esm/types/src/components/ui/Chat/Chat.types.d.ts +3 -5
  9. package/dist/esm/types/src/components/ui/Chat/ChatMessage/ChatMessage.d.ts +1 -1
  10. package/dist/esm/types/src/components/ui/Chat/ChatSheet/ChatSheet.d.ts +1 -1
  11. package/dist/esm/types/src/components/ui/Chat/ChatSheet/useChatPanelChromeModel.d.ts +4 -4
  12. package/dist/esm/types/src/components/ui/Chat/index.d.ts +3 -3
  13. package/dist/esm/types/src/contexts/chat-context.d.ts +2 -0
  14. package/dist/esm/types/src/tiptap/slash-mention/types.d.ts +3 -2
  15. package/package.json +1 -1
  16. package/src/components/ui/Chat/Chat.types.ts +3 -10
  17. package/src/components/ui/Chat/ChatChrome/ChatChrome.tsx +1 -0
  18. package/src/components/ui/Chat/ChatMessage/ChatMessage.tsx +4 -11
  19. package/src/components/ui/Chat/ChatSheet/ChatSheet.tsx +2 -2
  20. package/src/components/ui/Chat/ChatSheet/useChatPanelChromeModel.tsx +11 -75
  21. package/src/components/ui/Chat/index.ts +10 -7
  22. package/src/contexts/chat-context.tsx +3 -0
  23. package/src/docs/pages/ChatSlashCommandsPage.tsx +37 -11
  24. package/src/tiptap/slash-mention/types.ts +3 -2
  25. package/dist/esm/types/src/components/ui/Chat/Chat.types.test.d.ts +0 -1
  26. package/src/components/ui/Chat/Chat.types.test.ts +0 -32
@@ -4,12 +4,5 @@ var MessageRole;
4
4
  MessageRole["ASSISTANT"] = "assistant";
5
5
  MessageRole["SYSTEM"] = "system";
6
6
  })(MessageRole || (MessageRole = {}));
7
- /** System placeholder while dashboard generation runs (must match ChatSheet `addMessage` text). */
8
- const GENERATING_DASHBOARD_SYSTEM_TEXT = 'Generating dashboard…';
9
- /** Slash command id that triggers dashboard generation when `onGenerateDashboard` is set. */
10
- const GENERATE_DASHBOARD_SLASH_COMMAND_ID = 'generate-dashboard';
11
- function isGenerateDashboardSlashMessage(message) {
12
- return message.trim() === `/${GENERATE_DASHBOARD_SLASH_COMMAND_ID}`;
13
- }
14
7
 
15
- export { GENERATE_DASHBOARD_SLASH_COMMAND_ID, GENERATING_DASHBOARD_SYSTEM_TEXT, MessageRole, isGenerateDashboardSlashMessage };
8
+ export { MessageRole };
@@ -68,7 +68,7 @@ function ChatChrome({ showResizeHandle, resizeHandle, onClose, isEmpty, renderPr
68
68
  }, [isEmpty, messages.length]);
69
69
  return (jsxs("div", { className: S.root, children: [showResizeHandle && resizeHandle ? (jsx(PanelResizeHandle, { edge: "leading", isActive: resizeHandle.isActive, startWidthPx: resizeHandle.startWidthPx, getShellWidth: resizeHandle.getShellWidth, onDragWidth: resizeHandle.onDragWidth, onDragComplete: resizeHandle.onDragComplete, className: cn(SidebarStem.sidebarResizeHandle, S.chatResizeHandle) })) : null, jsx("div", { className: S.panelHeader, children: onClose ? (jsx(Button, { type: "button", variant: "ghost", icon: true, className: S.panelClose, "aria-label": "Close chat", onClick: onClose, children: jsx(X, { className: "size-4" }) })) : null }), jsxs("div", { className: S.content, children: [attachmentsDropzoneEnabled ? (jsx(DropZone, { accept: attachmentAccept, label: "Drop text files to attach", multiple: true, ghost: true, overlayScope: "container", disabled: promptBusy, className: S.attachmentDropzone, onFiles: handleAttachmentFiles })) : null, jsxs(Chat, { isEmpty: isEmpty, scopeId: effectiveScopeId, onChatDeleted: onChatDeleted, children: [isEmpty ? (jsxs(Fragment, { children: [jsx(Chat.EmptyState, { ...emptyState }), renderPresets('fixed')] })) : (jsx("div", { className: S.scrollWrapper, children: jsxs(Scroll, { y: true, yScrollbarClassName: S.scrollbar, className: S.scroll, innerClassName: S.scrollInner, offset: { y: { before: 56, after: 180 } }, fadeSize: "m", autoHide: true, ref: scrollRef, children: [messages.map((msg, index, arr) => {
70
70
  const isLast = index === arr.length - 1;
71
- return (jsx(Chat.Message, { role: msg.role, text: msg.text, userTextFileAttachments: msg.userTextFileAttachments, onQuickReply: onQuickReply, suppressedQuickReplyKeys: suppressedQuickReplyKeys, quickReplyDisabled: isLoading, isLastMessage: isLast, scriptContinue: isLast && scriptContinueLabel
71
+ return (jsx(Chat.Message, { role: msg.role, text: msg.text, inProgress: msg.inProgress, userTextFileAttachments: msg.userTextFileAttachments, onQuickReply: onQuickReply, suppressedQuickReplyKeys: suppressedQuickReplyKeys, quickReplyDisabled: isLoading, isLastMessage: isLast, scriptContinue: isLast && scriptContinueLabel
72
72
  ? { label: scriptContinueLabel }
73
73
  : undefined, onScriptContinue: isLast && scriptContinueLabel
74
74
  ? onScriptContinue
@@ -4,19 +4,19 @@ import { InteractiveContent } from '../../InteractiveContent/InteractiveContent.
4
4
  import 'lucide-react';
5
5
  import '../../InteractiveContent/InteractiveContent.styl.js';
6
6
  import { TextShimmer } from '../../TextShimmer/TextShimmer.js';
7
- import { MessageRole, GENERATING_DASHBOARD_SYSTEM_TEXT } from '../Chat.types.js';
7
+ import { MessageRole } from '../Chat.types.js';
8
8
  import { userTextFileAttachmentsFromMessage } from '../userTextFileAttachments.js';
9
9
  import { AgentMessageContent } from './AgentMessageContent.js';
10
10
  import S from './ChatMessage.styl.js';
11
11
  import { UserTextFileAttachmentBubble } from './UserTextFileAttachmentBubble.js';
12
12
 
13
- function ChatMessage({ role, text, userTextFileAttachments, onQuickReply, suppressedQuickReplyKeys, quickReplyDisabled, isLastMessage = true, scriptContinue, onScriptContinue, renderMessageChart, }) {
13
+ function ChatMessage({ role, text, inProgress, userTextFileAttachments, onQuickReply, suppressedQuickReplyKeys, quickReplyDisabled, isLastMessage = true, scriptContinue, onScriptContinue, renderMessageChart, }) {
14
14
  const fileAttachments = userTextFileAttachmentsFromMessage({
15
15
  userTextFileAttachments,
16
16
  });
17
17
  const isAssistant = role === MessageRole.ASSISTANT;
18
18
  const isSystem = role === MessageRole.SYSTEM;
19
- return (jsx("div", { className: cn(S.root, S[`role-${role}`]), children: isSystem ? (jsx("div", { className: S.text, children: text === GENERATING_DASHBOARD_SYSTEM_TEXT ? (jsx(TextShimmer, { as: "span", children: text })) : (text) })) : isAssistant ? (jsx(AgentMessageContent, { text: text, onQuickReply: onQuickReply, suppressedQuickReplyKeys: suppressedQuickReplyKeys, quickReplyDisabled: quickReplyDisabled, isLastMessage: isLastMessage, scriptContinue: scriptContinue, onScriptContinue: onScriptContinue, renderMessageChart: renderMessageChart })) : (jsxs("div", { className: S.userColumn, children: [jsx("div", { className: S.text, children: jsx(InteractiveContent, { text: text }) }), fileAttachments.map(attachment => (jsx(UserTextFileAttachmentBubble, { attachment: attachment }, `${attachment.displayName}:${attachment.filename}`)))] })) }));
19
+ return (jsx("div", { className: cn(S.root, S[`role-${role}`]), children: isSystem ? (jsx("div", { className: S.text, children: inProgress ? jsx(TextShimmer, { as: "span", children: text }) : text })) : isAssistant ? (jsx(AgentMessageContent, { text: text, onQuickReply: onQuickReply, suppressedQuickReplyKeys: suppressedQuickReplyKeys, quickReplyDisabled: quickReplyDisabled, isLastMessage: isLastMessage, scriptContinue: scriptContinue, onScriptContinue: onScriptContinue, renderMessageChart: renderMessageChart })) : (jsxs("div", { className: S.userColumn, children: [jsx("div", { className: S.text, children: jsx(InteractiveContent, { text: text }) }), fileAttachments.map(attachment => (jsx(UserTextFileAttachmentBubble, { attachment: attachment }, `${attachment.displayName}:${attachment.filename}`)))] })) }));
20
20
  }
21
21
 
22
22
  export { ChatMessage };
@@ -4,20 +4,20 @@ 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, slashCommandItems, inline = false, }) {
7
+ function ChatSheet({ triggerLabel = 'Open Chat', triggerAriaLabel, actionsRef, renderTrigger, presets, scopeId, onMessage, onScriptComplete, renderMessageChart, emptyState, allowedAttachments, allowPdfAttachments, onAttachmentsDropped, slashCommandItems, onSlashItemCommand, inline = false, }) {
8
8
  const model = useChatPanelChromeModel({
9
9
  embedAsPage: inline,
10
10
  presets,
11
11
  scopeId,
12
12
  onMessage,
13
13
  onScriptComplete,
14
- onGenerateDashboard,
15
14
  renderMessageChart,
16
15
  emptyState,
17
16
  allowedAttachments,
18
17
  allowPdfAttachments,
19
18
  onAttachmentsDropped,
20
19
  slashCommandItems,
20
+ onSlashItemCommand,
21
21
  });
22
22
  if (actionsRef) {
23
23
  actionsRef.current = {
@@ -1,9 +1,9 @@
1
1
  import { jsx } from 'react/jsx-runtime';
2
2
  import { useState, useRef, useEffect, useMemo, useCallback } from 'react';
3
- import { MessageRole, GENERATING_DASHBOARD_SYSTEM_TEXT, GENERATE_DASHBOARD_SLASH_COMMAND_ID, isGenerateDashboardSlashMessage } from '../Chat.types.js';
3
+ import { MessageRole } from '../Chat.types.js';
4
4
  import { isGraphIntakeAssistantStepComplete, matchUserTextToQuickReply, isPresetScriptGraph, branchesFromPresetScriptGraph, parseScriptLine, textHasQuickReplyMarkers, branchKeysUsedFromChatHistory, branchKeysUsedByUserMessages, extractQuickReplyLabelKeyPairsFromText, entryBranchKeyBeforeLastAssistant } from '../ChatMessage/presetScript.js';
5
5
  import { buildChatSendMessagePayload, displayTextFromSendPayload } from '../buildChatSendMessagePayload.js';
6
- import { usedPresetIdsFromMessages, formatChatTranscript } from '../chat-preset-utils.js';
6
+ import { usedPresetIdsFromMessages } from '../chat-preset-utils.js';
7
7
  import { useChatsForScopeId, useChat, useChatOutboundPending, useSyncChatPanelBusy, isChatEmpty } from '../../../../contexts/chat-context.js';
8
8
  import useEvent from '../../../../hooks/useEvent.js';
9
9
  import { useIsMobile } from '../../../../hooks/useIsMobile.js';
@@ -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, slashCommandItems, }) {
25
+ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onScriptComplete, renderMessageChart, emptyState, allowedAttachments, allowPdfAttachments, onAttachmentsDropped, slashCommandItems, onSlashItemCommand, }) {
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();
@@ -60,7 +60,6 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
60
60
  const [intakeByChatId, setIntakeByChatId] = useState({});
61
61
  /** Preset intake finished for this session (e.g. `onScriptComplete` callback). */
62
62
  const [scriptCompleteByChatId, setScriptCompleteByChatId] = useState({});
63
- const [generatingDashboard, setGeneratingDashboard] = useState(false);
64
63
  const scriptAdvanceLockRef = useRef(false);
65
64
  const quickReplyLockRef = useRef(false);
66
65
  const scrollRef = useRef(null);
@@ -712,60 +711,14 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
712
711
  lastMsg?.role === MessageRole.ASSISTANT &&
713
712
  !linearScriptActive &&
714
713
  (!graphActive || (!lastHasQuickMarkers && unusedBranchKeys.length === 0));
715
- const handleGenerateDashboard = useCallback(async () => {
716
- if (!currentChatId || !onGenerateDashboard)
717
- return;
718
- const transcript = formatChatTranscript((chat?.messages ?? []).filter(m => m.role !== MessageRole.SYSTEM));
719
- setGeneratingDashboard(true);
720
- const systemPlaceholderId = addMessage(currentChatId, MessageRole.SYSTEM, GENERATING_DASHBOARD_SYSTEM_TEXT);
721
- try {
722
- await Promise.resolve(onGenerateDashboard(transcript));
723
- setScriptCompleteByChatId(prev => {
724
- const next = { ...prev };
725
- delete next[currentChatId];
726
- return next;
727
- });
728
- }
729
- finally {
730
- if (systemPlaceholderId) {
731
- removeMessageById(currentChatId, systemPlaceholderId);
732
- }
733
- setGeneratingDashboard(false);
734
- }
735
- }, [
736
- currentChatId,
737
- onGenerateDashboard,
738
- chat?.messages,
739
- addMessage,
740
- removeMessageById,
741
- ]);
742
- const onSlashItemCommand = useCallback(({ item }) => {
743
- if (item.id !== GENERATE_DASHBOARD_SLASH_COMMAND_ID ||
744
- !onGenerateDashboard) {
745
- return false;
746
- }
747
- if (generatingDashboard) {
748
- return true;
749
- }
750
- queueMicrotask(() => {
751
- void handleGenerateDashboard();
752
- });
753
- return true;
754
- }, [onGenerateDashboard, generatingDashboard, handleGenerateDashboard]);
755
714
  const onPromptSubmitWithSlashCommands = useCallback(async (message, attachments) => {
756
- if (isGenerateDashboardSlashMessage(message) &&
757
- onGenerateDashboard &&
758
- !generatingDashboard) {
759
- void handleGenerateDashboard();
715
+ const trimmed = message.trim();
716
+ const slashItem = slashCommandItems?.find(item => trimmed === `/${item.id}`);
717
+ if (slashItem && onSlashItemCommand?.({ item: slashItem }) === true) {
760
718
  return;
761
719
  }
762
720
  return handlePromptSubmit(message, attachments);
763
- }, [
764
- handlePromptSubmit,
765
- onGenerateDashboard,
766
- generatingDashboard,
767
- handleGenerateDashboard,
768
- ]);
721
+ }, [handlePromptSubmit, slashCommandItems, onSlashItemCommand]);
769
722
  const onDragChatWidth = useCallback((rawPx) => setChatWidthPx(rawPx, { persist: false }), [setChatWidthPx]);
770
723
  const onDragChatComplete = useCallback((finalRawPx) => setChatWidthPx(finalRawPx, { persist: true }), [setChatWidthPx]);
771
724
  const chromeProps = {
@@ -200,6 +200,7 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
200
200
  ...(attachments?.length
201
201
  ? { userTextFileAttachments: attachments }
202
202
  : {}),
203
+ ...(options?.inProgress ? { inProgress: true } : {}),
203
204
  };
204
205
  setChats(prev => {
205
206
  const scopeChats = prev[scopeId] ?? [];
package/dist/esm/index.js CHANGED
@@ -22,7 +22,7 @@ export { ChartLegend, ChartTooltip } from './components/ui/Chart/Chart.js';
22
22
  export { THEMES } from './components/ui/Chart/Chart.types.js';
23
23
  export { ChartAreaInteractive, chartConfig } from './components/ui/ChartAreaInteractive/ChartAreaInteractive.js';
24
24
  export { Chat } from './components/ui/Chat/Chat.js';
25
- export { usedPresetIdsFromMessages } from './components/ui/Chat/chat-preset-utils.js';
25
+ export { formatChatTranscript, usedPresetIdsFromMessages } from './components/ui/Chat/chat-preset-utils.js';
26
26
  export { ChatChrome } from './components/ui/Chat/ChatChrome/ChatChrome.js';
27
27
  export { TEXT_ATTACHMENT_ACCEPT_PARTS, filterToTextAttachments } from './components/ui/Chat/chatAttachmentAccept.js';
28
28
  export { buildChatSendMessagePayload, displayTextFromSendPayload, normalizeUserTextFileAttachments } from './components/ui/Chat/buildChatSendMessagePayload.js';
@@ -32,7 +32,7 @@ export { useChatPanelChromeModel } from './components/ui/Chat/ChatSheet/useChatP
32
32
  export { ChatMessage } from './components/ui/Chat/ChatMessage/ChatMessage.js';
33
33
  export { ChatPrompt } from './components/ui/Chat/ChatPrompt/ChatPrompt.js';
34
34
  export { ChatPresets } from './components/ui/Chat/ChatPresets/ChatPresets.js';
35
- export { GENERATE_DASHBOARD_SLASH_COMMAND_ID, MessageRole, isGenerateDashboardSlashMessage } from './components/ui/Chat/Chat.types.js';
35
+ export { MessageRole } from './components/ui/Chat/Chat.types.js';
36
36
  export { CsvIcon } from './components/icons/CsvIcon/CsvIcon.js';
37
37
  export { Checkbox } from './components/ui/Checkbox/Checkbox.js';
38
38
  export { Confirm } from './components/ui/Confirm/Confirm.js';
@@ -6,11 +6,6 @@ export declare enum MessageRole {
6
6
  ASSISTANT = "assistant",
7
7
  SYSTEM = "system"
8
8
  }
9
- /** System placeholder while dashboard generation runs (must match ChatSheet `addMessage` text). */
10
- export declare const GENERATING_DASHBOARD_SYSTEM_TEXT = "Generating dashboard\u2026";
11
- /** Slash command id that triggers dashboard generation when `onGenerateDashboard` is set. */
12
- export declare const GENERATE_DASHBOARD_SLASH_COMMAND_ID = "generate-dashboard";
13
- export declare function isGenerateDashboardSlashMessage(message: string): boolean;
14
9
  /** USER-only: text file attached to a message; shown as downloadable file row(s). */
15
10
  export type UserTextFileAttachment = {
16
11
  displayName: string;
@@ -29,6 +24,8 @@ export interface Message {
29
24
  text: string;
30
25
  timestamp: number;
31
26
  userTextFileAttachments?: UserTextFileAttachment[];
27
+ /** SYSTEM-only: transient progress placeholder while work is in flight. */
28
+ inProgress?: boolean;
32
29
  }
33
30
  export interface Chat {
34
31
  session_id: string;
@@ -93,6 +90,7 @@ export interface ChatPromptProps {
93
90
  export interface ChatMessageProps {
94
91
  role: MessageRole;
95
92
  text: string;
93
+ inProgress?: boolean;
96
94
  userTextFileAttachments?: UserTextFileAttachment[];
97
95
  onQuickReply?: (branchKey: string, displayLabel: string) => void;
98
96
  /** Branch keys already taken (e.g. from chat history); hide quick-reply buttons for these. */
@@ -1,2 +1,2 @@
1
1
  import { type ChatMessageProps } from '../Chat.types';
2
- export declare function ChatMessage({ role, text, userTextFileAttachments, onQuickReply, suppressedQuickReplyKeys, quickReplyDisabled, isLastMessage, scriptContinue, onScriptContinue, renderMessageChart, }: ChatMessageProps): import("react/jsx-runtime").JSX.Element;
2
+ export declare function ChatMessage({ role, text, inProgress, userTextFileAttachments, onQuickReply, suppressedQuickReplyKeys, quickReplyDisabled, isLastMessage, scriptContinue, onScriptContinue, renderMessageChart, }: ChatMessageProps): import("react/jsx-runtime").JSX.Element;
@@ -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, slashCommandItems, inline, }: ChatSheetProps): import("react/jsx-runtime").JSX.Element;
22
+ export declare function ChatSheet({ triggerLabel, triggerAriaLabel, actionsRef, renderTrigger, presets, scopeId, onMessage, onScriptComplete, renderMessageChart, emptyState, allowedAttachments, allowPdfAttachments, onAttachmentsDropped, slashCommandItems, onSlashItemCommand, inline, }: ChatSheetProps): import("react/jsx-runtime").JSX.Element;
@@ -1,5 +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
+ import type { SlashCommandItem, SlashOnItemCommand } from '#uilib/tiptap/slash-mention/types';
3
3
  import type { ChatChromeProps } from '../ChatChrome';
4
4
  import type { ChatAttachmentDropItem } from '../ChatChrome/ChatChrome.types';
5
5
  import type { ChatEmptyStateConfig } from '../ChatEmptyState/ChatEmptyState.types';
@@ -12,8 +12,6 @@ export type UseChatPanelChromeModelInput = {
12
12
  onMessage?: (message: string) => void;
13
13
  /** Fires when a preset script has no further `[Label|branchKey]` steps (graph leaf or linear script end). */
14
14
  onScriptComplete?: (payload: ScriptCompletePayload) => void;
15
- /** Generate dashboard from chat transcript (e.g. via `/generate-dashboard` slash command). */
16
- onGenerateDashboard?: (transcript: string) => void | Promise<void>;
17
15
  /** Renders `[CHART]` tokens in assistant messages. */
18
16
  renderMessageChart?: () => React.ReactNode;
19
17
  /** Forwarded to `ChatChrome` when the thread is empty. */
@@ -25,6 +23,8 @@ export type UseChatPanelChromeModelInput = {
25
23
  onAttachmentsDropped?: (items: ChatAttachmentDropItem[]) => void | Promise<void>;
26
24
  /** Slash menu (`/`) in the composer; omit or pass empty to disable. */
27
25
  slashCommandItems?: SlashCommandItem[];
26
+ /** Custom slash command handler (palette pick or Enter on `/id`). */
27
+ onSlashItemCommand?: SlashOnItemCommand;
28
28
  };
29
29
  export type UseChatPanelChromeModelResult = {
30
30
  chromeProps: ChatChromeProps;
@@ -34,4 +34,4 @@ export type UseChatPanelChromeModelResult = {
34
34
  newChat: () => void;
35
35
  chatPanelContainer: HTMLElement | null;
36
36
  };
37
- export declare function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onScriptComplete, onGenerateDashboard, renderMessageChart, emptyState, allowedAttachments, allowPdfAttachments, onAttachmentsDropped, slashCommandItems, }: UseChatPanelChromeModelInput): UseChatPanelChromeModelResult;
37
+ export declare function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onScriptComplete, renderMessageChart, emptyState, allowedAttachments, allowPdfAttachments, onAttachmentsDropped, slashCommandItems, onSlashItemCommand, }: UseChatPanelChromeModelInput): UseChatPanelChromeModelResult;
@@ -1,5 +1,5 @@
1
1
  export { Chat } from './Chat';
2
- export { usedPresetIdsFromMessages } from './chat-preset-utils';
2
+ export { formatChatTranscript, usedPresetIdsFromMessages, } from './chat-preset-utils';
3
3
  export { ChatChrome } from './ChatChrome';
4
4
  export type { ChatChromeProps, ChatChromeResizeHandleConfig, } from './ChatChrome';
5
5
  export { TEXT_ATTACHMENT_ACCEPT_PARTS, filterToTextAttachments, } from './chatAttachmentAccept';
@@ -14,6 +14,6 @@ export { ChatPrompt } from './ChatPrompt';
14
14
  export { ChatPresets } from './ChatPresets';
15
15
  export type { ChatEmptyStateConfig, ChatEmptyStateContext, ChatEmptyStateProps, } from './ChatEmptyState/ChatEmptyState.types';
16
16
  export type { Chat as ChatType, ChatAttachmentDropItem, ChatSendMessagePayload, ChatProps, ChatPreset as ChatPresetType, Message, UserTextFileAttachment, } from './Chat.types';
17
- export { GENERATE_DASHBOARD_SLASH_COMMAND_ID, isGenerateDashboardSlashMessage, MessageRole, } from './Chat.types';
18
- export type { SlashCommandItem } from '#uilib/tiptap/slash-mention/types';
17
+ export { MessageRole } from './Chat.types';
18
+ export type { SlashCommandItem, SlashItemCommandContext, SlashOnItemCommand, } from '#uilib/tiptap/slash-mention/types';
19
19
  export { CsvIcon } from '../../icons/CsvIcon/CsvIcon';
@@ -5,6 +5,8 @@ export type SendChatMessageFn = (message: string, targetChatId: string) => Promi
5
5
  export type { ChatSendMessagePayload, UserTextFileAttachment, } from '#uilib/components/ui/Chat/Chat.types';
6
6
  export type AddChatMessageOptions = {
7
7
  userTextFileAttachments?: UserTextFileAttachment[];
8
+ /** SYSTEM-only: mark as transient progress placeholder (shimmer until removed). */
9
+ inProgress?: boolean;
8
10
  };
9
11
  export interface ChatContextType {
10
12
  /** Returns the new session id, or undefined if no user / not created. */
@@ -5,9 +5,10 @@ export type SlashCommandItem = {
5
5
  description?: string;
6
6
  };
7
7
  export type SlashItemCommandContext = {
8
- editor: Editor;
9
- range: Range;
10
8
  item: SlashCommandItem;
9
+ /** Present when picked from the slash palette; omitted on Enter submit of `/id`. */
10
+ editor?: Editor;
11
+ range?: Range;
11
12
  };
12
13
  /**
13
14
  * If provided, run when a slash item is picked. Return true to skip mention insert
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sybilion/uilib",
3
- "version": "1.3.38",
3
+ "version": "1.3.40",
4
4
  "description": "Sybilion Design System — React UI components (Webpack + Stylus)",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -13,16 +13,6 @@ export enum MessageRole {
13
13
  SYSTEM = 'system',
14
14
  }
15
15
 
16
- /** System placeholder while dashboard generation runs (must match ChatSheet `addMessage` text). */
17
- export const GENERATING_DASHBOARD_SYSTEM_TEXT = 'Generating dashboard…';
18
-
19
- /** Slash command id that triggers dashboard generation when `onGenerateDashboard` is set. */
20
- export const GENERATE_DASHBOARD_SLASH_COMMAND_ID = 'generate-dashboard';
21
-
22
- export function isGenerateDashboardSlashMessage(message: string): boolean {
23
- return message.trim() === `/${GENERATE_DASHBOARD_SLASH_COMMAND_ID}`;
24
- }
25
-
26
16
  /** USER-only: text file attached to a message; shown as downloadable file row(s). */
27
17
  export type UserTextFileAttachment = {
28
18
  displayName: string;
@@ -43,6 +33,8 @@ export interface Message {
43
33
  text: string;
44
34
  timestamp: number;
45
35
  userTextFileAttachments?: UserTextFileAttachment[];
36
+ /** SYSTEM-only: transient progress placeholder while work is in flight. */
37
+ inProgress?: boolean;
46
38
  }
47
39
 
48
40
  export interface Chat {
@@ -113,6 +105,7 @@ export interface ChatPromptProps {
113
105
  export interface ChatMessageProps {
114
106
  role: MessageRole;
115
107
  text: string;
108
+ inProgress?: boolean;
116
109
  userTextFileAttachments?: UserTextFileAttachment[];
117
110
  onQuickReply?: (branchKey: string, displayLabel: string) => void;
118
111
  /** Branch keys already taken (e.g. from chat history); hide quick-reply buttons for these. */
@@ -199,6 +199,7 @@ export function ChatChrome({
199
199
  key={msg.id}
200
200
  role={msg.role}
201
201
  text={msg.text}
202
+ inProgress={msg.inProgress}
202
203
  userTextFileAttachments={msg.userTextFileAttachments}
203
204
  onQuickReply={onQuickReply}
204
205
  suppressedQuickReplyKeys={suppressedQuickReplyKeys}
@@ -1,13 +1,9 @@
1
1
  import cn from 'classnames';
2
2
 
3
3
  import { InteractiveContent } from '#uilib/components/ui/InteractiveContent';
4
+ import { TextShimmer } from '#uilib/components/ui/TextShimmer';
4
5
 
5
- import { TextShimmer } from '../../TextShimmer';
6
- import {
7
- type ChatMessageProps,
8
- GENERATING_DASHBOARD_SYSTEM_TEXT,
9
- MessageRole,
10
- } from '../Chat.types';
6
+ import { type ChatMessageProps, MessageRole } from '../Chat.types';
11
7
  import { userTextFileAttachmentsFromMessage } from '../userTextFileAttachments';
12
8
  import { AgentMessageContent } from './AgentMessageContent';
13
9
  import S from './ChatMessage.styl';
@@ -16,6 +12,7 @@ import { UserTextFileAttachmentBubble } from './UserTextFileAttachmentBubble';
16
12
  export function ChatMessage({
17
13
  role,
18
14
  text,
15
+ inProgress,
19
16
  userTextFileAttachments,
20
17
  onQuickReply,
21
18
  suppressedQuickReplyKeys,
@@ -35,11 +32,7 @@ export function ChatMessage({
35
32
  <div className={cn(S.root, S[`role-${role}`])}>
36
33
  {isSystem ? (
37
34
  <div className={S.text}>
38
- {text === GENERATING_DASHBOARD_SYSTEM_TEXT ? (
39
- <TextShimmer as="span">{text}</TextShimmer>
40
- ) : (
41
- text
42
- )}
35
+ {inProgress ? <TextShimmer as="span">{text}</TextShimmer> : text}
43
36
  </div>
44
37
  ) : isAssistant ? (
45
38
  <AgentMessageContent
@@ -41,13 +41,13 @@ export function ChatSheet({
41
41
  scopeId,
42
42
  onMessage,
43
43
  onScriptComplete,
44
- onGenerateDashboard,
45
44
  renderMessageChart,
46
45
  emptyState,
47
46
  allowedAttachments,
48
47
  allowPdfAttachments,
49
48
  onAttachmentsDropped,
50
49
  slashCommandItems,
50
+ onSlashItemCommand,
51
51
  inline = false,
52
52
  }: ChatSheetProps) {
53
53
  const model = useChatPanelChromeModel({
@@ -56,13 +56,13 @@ export function ChatSheet({
56
56
  scopeId,
57
57
  onMessage,
58
58
  onScriptComplete,
59
- onGenerateDashboard,
60
59
  renderMessageChart,
61
60
  emptyState,
62
61
  allowedAttachments,
63
62
  allowPdfAttachments,
64
63
  onAttachmentsDropped,
65
64
  slashCommandItems,
65
+ onSlashItemCommand,
66
66
  });
67
67
 
68
68
  if (actionsRef) {
@@ -2,11 +2,8 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
2
 
3
3
  import {
4
4
  ChatPreset,
5
- GENERATE_DASHBOARD_SLASH_COMMAND_ID,
6
- GENERATING_DASHBOARD_SYSTEM_TEXT,
7
5
  MessageRole,
8
6
  type ScriptCompletePayload,
9
- isGenerateDashboardSlashMessage,
10
7
  } from '#uilib/components/ui/Chat/Chat.types';
11
8
  import {
12
9
  branchKeysUsedByUserMessages,
@@ -25,10 +22,7 @@ import {
25
22
  buildChatSendMessagePayload,
26
23
  displayTextFromSendPayload,
27
24
  } from '#uilib/components/ui/Chat/buildChatSendMessagePayload';
28
- import {
29
- formatChatTranscript,
30
- usedPresetIdsFromMessages,
31
- } from '#uilib/components/ui/Chat/chat-preset-utils';
25
+ import { usedPresetIdsFromMessages } from '#uilib/components/ui/Chat/chat-preset-utils';
32
26
  import {
33
27
  isChatEmpty,
34
28
  useChat,
@@ -42,7 +36,7 @@ import { useQueryParams } from '#uilib/hooks/useQueryParams';
42
36
  import logger from '#uilib/lib/logger';
43
37
  import type {
44
38
  SlashCommandItem,
45
- SlashItemCommandContext,
39
+ SlashOnItemCommand,
46
40
  } from '#uilib/tiptap/slash-mention/types';
47
41
  import { mergePresetFreeformDefaults } from '#uilib/utils/chatPresetMerge';
48
42
  import { ScrollRef } from '@homecode/ui';
@@ -63,8 +57,6 @@ export type UseChatPanelChromeModelInput = {
63
57
  onMessage?: (message: string) => void;
64
58
  /** Fires when a preset script has no further `[Label|branchKey]` steps (graph leaf or linear script end). */
65
59
  onScriptComplete?: (payload: ScriptCompletePayload) => void;
66
- /** Generate dashboard from chat transcript (e.g. via `/generate-dashboard` slash command). */
67
- onGenerateDashboard?: (transcript: string) => void | Promise<void>;
68
60
  /** Renders `[CHART]` tokens in assistant messages. */
69
61
  renderMessageChart?: () => React.ReactNode;
70
62
  /** Forwarded to `ChatChrome` when the thread is empty. */
@@ -78,6 +70,8 @@ export type UseChatPanelChromeModelInput = {
78
70
  ) => void | Promise<void>;
79
71
  /** Slash menu (`/`) in the composer; omit or pass empty to disable. */
80
72
  slashCommandItems?: SlashCommandItem[];
73
+ /** Custom slash command handler (palette pick or Enter on `/id`). */
74
+ onSlashItemCommand?: SlashOnItemCommand;
81
75
  };
82
76
 
83
77
  export type UseChatPanelChromeModelResult = {
@@ -120,13 +114,13 @@ export function useChatPanelChromeModel({
120
114
  scopeId,
121
115
  onMessage,
122
116
  onScriptComplete,
123
- onGenerateDashboard,
124
117
  renderMessageChart,
125
118
  emptyState,
126
119
  allowedAttachments,
127
120
  allowPdfAttachments,
128
121
  onAttachmentsDropped,
129
122
  slashCommandItems,
123
+ onSlashItemCommand,
130
124
  }: UseChatPanelChromeModelInput): UseChatPanelChromeModelResult {
131
125
  const effectiveScopeId = scopeId ?? NO_SCOPE_FALLBACK;
132
126
  const isMobile = useIsMobile();
@@ -202,7 +196,6 @@ export function useChatPanelChromeModel({
202
196
  const [scriptCompleteByChatId, setScriptCompleteByChatId] = useState<
203
197
  Record<string, boolean>
204
198
  >({});
205
- const [generatingDashboard, setGeneratingDashboard] = useState(false);
206
199
  const scriptAdvanceLockRef = useRef(false);
207
200
  const quickReplyLockRef = useRef(false);
208
201
  const scrollRef = useRef<ScrollRef>(null);
@@ -961,75 +954,18 @@ export function useChatPanelChromeModel({
961
954
  !linearScriptActive &&
962
955
  (!graphActive || (!lastHasQuickMarkers && unusedBranchKeys.length === 0));
963
956
 
964
- const handleGenerateDashboard = useCallback(async () => {
965
- if (!currentChatId || !onGenerateDashboard) return;
966
- const transcript = formatChatTranscript(
967
- (chat?.messages ?? []).filter(m => m.role !== MessageRole.SYSTEM),
968
- );
969
- setGeneratingDashboard(true);
970
- const systemPlaceholderId = addMessage(
971
- currentChatId,
972
- MessageRole.SYSTEM,
973
- GENERATING_DASHBOARD_SYSTEM_TEXT,
974
- );
975
- try {
976
- await Promise.resolve(onGenerateDashboard(transcript));
977
- setScriptCompleteByChatId(prev => {
978
- const next = { ...prev };
979
- delete next[currentChatId];
980
- return next;
981
- });
982
- } finally {
983
- if (systemPlaceholderId) {
984
- removeMessageById(currentChatId, systemPlaceholderId);
985
- }
986
- setGeneratingDashboard(false);
987
- }
988
- }, [
989
- currentChatId,
990
- onGenerateDashboard,
991
- chat?.messages,
992
- addMessage,
993
- removeMessageById,
994
- ]);
995
-
996
- const onSlashItemCommand = useCallback(
997
- ({ item }: SlashItemCommandContext) => {
998
- if (
999
- item.id !== GENERATE_DASHBOARD_SLASH_COMMAND_ID ||
1000
- !onGenerateDashboard
1001
- ) {
1002
- return false;
1003
- }
1004
- if (generatingDashboard) {
1005
- return true;
1006
- }
1007
- queueMicrotask(() => {
1008
- void handleGenerateDashboard();
1009
- });
1010
- return true;
1011
- },
1012
- [onGenerateDashboard, generatingDashboard, handleGenerateDashboard],
1013
- );
1014
-
1015
957
  const onPromptSubmitWithSlashCommands = useCallback(
1016
958
  async (message: string, attachments?: ChatAttachmentDropItem[]) => {
1017
- if (
1018
- isGenerateDashboardSlashMessage(message) &&
1019
- onGenerateDashboard &&
1020
- !generatingDashboard
1021
- ) {
1022
- void handleGenerateDashboard();
959
+ const trimmed = message.trim();
960
+ const slashItem = slashCommandItems?.find(
961
+ item => trimmed === `/${item.id}`,
962
+ );
963
+ if (slashItem && onSlashItemCommand?.({ item: slashItem }) === true) {
1023
964
  return;
1024
965
  }
1025
966
  return handlePromptSubmit(message, attachments);
1026
967
  },
1027
- [
1028
- handlePromptSubmit,
1029
- onGenerateDashboard,
1030
- generatingDashboard,
1031
- handleGenerateDashboard,
1032
- ],
968
+ [handlePromptSubmit, slashCommandItems, onSlashItemCommand],
1033
969
  );
1034
970
 
1035
971
  const onDragChatWidth = useCallback(
@@ -1,5 +1,8 @@
1
1
  export { Chat } from './Chat';
2
- export { usedPresetIdsFromMessages } from './chat-preset-utils';
2
+ export {
3
+ formatChatTranscript,
4
+ usedPresetIdsFromMessages,
5
+ } from './chat-preset-utils';
3
6
  export { ChatChrome } from './ChatChrome';
4
7
  export type {
5
8
  ChatChromeProps,
@@ -39,10 +42,10 @@ export type {
39
42
  Message,
40
43
  UserTextFileAttachment,
41
44
  } from './Chat.types';
42
- export {
43
- GENERATE_DASHBOARD_SLASH_COMMAND_ID,
44
- isGenerateDashboardSlashMessage,
45
- MessageRole,
46
- } from './Chat.types';
47
- export type { SlashCommandItem } from '#uilib/tiptap/slash-mention/types';
45
+ export { MessageRole } from './Chat.types';
46
+ export type {
47
+ SlashCommandItem,
48
+ SlashItemCommandContext,
49
+ SlashOnItemCommand,
50
+ } from '#uilib/tiptap/slash-mention/types';
48
51
  export { CsvIcon } from '../../icons/CsvIcon/CsvIcon';
@@ -35,6 +35,8 @@ const CHAT_SCOPE_IDS_REGISTRY_KEY = 'chat-scope-ids';
35
35
 
36
36
  export type AddChatMessageOptions = {
37
37
  userTextFileAttachments?: UserTextFileAttachment[];
38
+ /** SYSTEM-only: mark as transient progress placeholder (shimmer until removed). */
39
+ inProgress?: boolean;
38
40
  };
39
41
 
40
42
  export interface ChatContextType {
@@ -326,6 +328,7 @@ export function ChatProvider({
326
328
  ...(attachments?.length
327
329
  ? { userTextFileAttachments: attachments }
328
330
  : {}),
331
+ ...(options?.inProgress ? { inProgress: true } : {}),
329
332
  };
330
333
 
331
334
  setChats(prev => {
@@ -31,18 +31,25 @@ const DOCS_SAMPLE_SLASH_ITEMS: SlashCommandItem[] = [
31
31
  const SAMPLE_COMMAND_REPLY_TEXT =
32
32
  'Sample command ran via `onSlashItemCommand` — composer cleared, no mention inserted.';
33
33
 
34
- const ASSISTANT_REPLY_TEXT =
35
- 'Demo reply for a normal message. Slash picks with a custom handler skip mention insert.';
34
+ const SAMPLE_COMMAND_PROGRESS_TEXT = 'Running sample command…';
36
35
 
37
- function makeMessage(role: MessageRole, text: string): Message {
36
+ function makeMessage(
37
+ role: MessageRole,
38
+ text: string,
39
+ options?: { inProgress?: boolean },
40
+ ): Message {
38
41
  return {
39
42
  id: crypto.randomUUID(),
40
43
  role,
41
44
  text,
42
45
  timestamp: Date.now(),
46
+ ...(options?.inProgress ? { inProgress: true } : {}),
43
47
  };
44
48
  }
45
49
 
50
+ const ASSISTANT_REPLY_TEXT =
51
+ 'Demo reply for a normal message. Slash picks with a custom handler skip mention insert.';
52
+
46
53
  export default function ChatSlashCommandsPage() {
47
54
  const [messages, setMessages] = useState<Message[]>([]);
48
55
  const [isLoading, setIsLoading] = useState(false);
@@ -63,10 +70,30 @@ export default function ChatSlashCommandsPage() {
63
70
  messages[messages.length - 1]?.role === MessageRole.USER;
64
71
 
65
72
  const runSampleCommand = useCallback(() => {
73
+ const progressId = crypto.randomUUID();
74
+ setIsLoading(true);
66
75
  setMessages(prev => [
67
76
  ...prev,
68
- makeMessage(MessageRole.ASSISTANT, SAMPLE_COMMAND_REPLY_TEXT),
77
+ {
78
+ id: progressId,
79
+ role: MessageRole.SYSTEM,
80
+ text: SAMPLE_COMMAND_PROGRESS_TEXT,
81
+ timestamp: Date.now(),
82
+ inProgress: true,
83
+ },
69
84
  ]);
85
+
86
+ if (replyTimeoutRef.current != null) {
87
+ clearTimeout(replyTimeoutRef.current);
88
+ }
89
+ replyTimeoutRef.current = setTimeout(() => {
90
+ replyTimeoutRef.current = null;
91
+ setMessages(prev => [
92
+ ...prev.filter(m => m.id !== progressId),
93
+ makeMessage(MessageRole.ASSISTANT, SAMPLE_COMMAND_REPLY_TEXT),
94
+ ]);
95
+ setIsLoading(false);
96
+ }, 1200);
70
97
  }, []);
71
98
 
72
99
  const onSlashItemCommand = useCallback(
@@ -113,7 +140,7 @@ export default function ChatSlashCommandsPage() {
113
140
  <AppPageHeader
114
141
  breadcrumbs={[{ label: 'Chat' }, { label: 'Chat slash commands' }]}
115
142
  title="Chat slash commands"
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.`}
143
+ 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. Long-running handlers can append a SYSTEM message with inProgress while work is in flight, then remove it when done.`}
117
144
  actions={
118
145
  <DocsHeaderActions slashCommandItems={DOCS_SAMPLE_SLASH_ITEMS} />
119
146
  }
@@ -127,8 +154,10 @@ export default function ChatSlashCommandsPage() {
127
154
  shell below: scrolling history, empty state, disclaimer, composer.
128
155
  Type <kbd className="font-mono">/</kbd> at line start or after a
129
156
  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.
157
+ custom handler (shows a transient{' '}
158
+ <kbd className="font-mono">inProgress</kbd> shimmer while the demo
159
+ action runs), or send <kbd className="font-mono">/sample-command</kbd>{' '}
160
+ with Enter.
132
161
  </p>
133
162
  <ChatChrome
134
163
  showResizeHandle={false}
@@ -156,10 +185,7 @@ export default function ChatSlashCommandsPage() {
156
185
  emptyState={{
157
186
  title: 'Try a slash command',
158
187
  description:
159
- 'Pick sample-command from the palette or send /sample-command — onSlashItemCommand clears the composer and runs the demo action.',
160
- additionalContent: (
161
- <p>Optional empty-state slot via additionalContent.</p>
162
- ),
188
+ 'Pick sample-command from the palette or send /sample-command — onSlashItemCommand clears the composer, shows an inProgress placeholder, then posts the demo reply.',
163
189
  }}
164
190
  />
165
191
  </PageContentSection>
@@ -7,9 +7,10 @@ export type SlashCommandItem = {
7
7
  };
8
8
 
9
9
  export type SlashItemCommandContext = {
10
- editor: Editor;
11
- range: Range;
12
10
  item: SlashCommandItem;
11
+ /** Present when picked from the slash palette; omitted on Enter submit of `/id`. */
12
+ editor?: Editor;
13
+ range?: Range;
13
14
  };
14
15
 
15
16
  /**
@@ -1,32 +0,0 @@
1
- import {
2
- GENERATE_DASHBOARD_SLASH_COMMAND_ID,
3
- isGenerateDashboardSlashMessage,
4
- } from './Chat.types';
5
-
6
- describe('isGenerateDashboardSlashMessage', () => {
7
- it('matches exact slash command', () => {
8
- expect(
9
- isGenerateDashboardSlashMessage(
10
- `/${GENERATE_DASHBOARD_SLASH_COMMAND_ID}`,
11
- ),
12
- ).toBe(true);
13
- });
14
-
15
- it('trims surrounding whitespace', () => {
16
- expect(
17
- isGenerateDashboardSlashMessage(
18
- ` /${GENERATE_DASHBOARD_SLASH_COMMAND_ID} `,
19
- ),
20
- ).toBe(true);
21
- });
22
-
23
- it('rejects plain text and partial commands', () => {
24
- expect(isGenerateDashboardSlashMessage('generate-dashboard')).toBe(false);
25
- expect(isGenerateDashboardSlashMessage('/generate')).toBe(false);
26
- expect(
27
- isGenerateDashboardSlashMessage(
28
- `/ ${GENERATE_DASHBOARD_SLASH_COMMAND_ID}`,
29
- ),
30
- ).toBe(false);
31
- });
32
- });