@sybilion/uilib 1.3.33 → 1.3.36
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/ChatChrome/ChatChrome.js +2 -2
- package/dist/esm/components/ui/Chat/ChatPrompt/ChatPrompt.helpers.js +18 -4
- package/dist/esm/components/ui/Chat/ChatPrompt/ChatPrompt.js +39 -52
- package/dist/esm/components/ui/Chat/ChatPrompt/ChatPrompt.styl.js +2 -2
- package/dist/esm/components/ui/Chat/ChatPrompt/ChatPromptComposer.js +25 -0
- package/dist/esm/components/ui/Chat/ChatPrompt/chatPromptDoc.js +17 -0
- package/dist/esm/components/ui/Chat/ChatPrompt/useChatPromptEditor.js +163 -0
- package/dist/esm/components/ui/Tooltip/Tooltip.styl.js +1 -1
- package/dist/esm/index.js +2 -0
- package/dist/esm/tiptap/slash-mention/SlashSuggestionList.js +53 -0
- package/dist/esm/tiptap/slash-mention/SlashSuggestionList.styl.js +7 -0
- package/dist/esm/tiptap/slash-mention/createSlashMentionExtension.js +151 -0
- package/dist/esm/tiptap/slash-mention/defaultChatSlashItems.js +12 -0
- package/dist/esm/types/src/components/ui/Chat/Chat.types.d.ts +3 -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 +5 -0
- package/dist/esm/types/src/components/ui/Chat/ChatPrompt/ChatPrompt.d.ts +1 -1
- package/dist/esm/types/src/components/ui/Chat/ChatPrompt/ChatPrompt.helpers.d.ts +6 -0
- package/dist/esm/types/src/components/ui/Chat/ChatPrompt/ChatPromptComposer.d.ts +13 -0
- package/dist/esm/types/src/components/ui/Chat/ChatPrompt/chatPromptDoc.d.ts +3 -0
- package/dist/esm/types/src/components/ui/Chat/ChatPrompt/useChatPromptEditor.d.ts +20 -0
- package/dist/esm/types/src/components/ui/Chat/index.d.ts +1 -0
- package/dist/esm/types/src/components/ui/Input/Input.d.ts +1 -1
- package/dist/esm/types/src/docs/pages/ChatSlashCommandsPage.d.ts +1 -0
- package/dist/esm/types/src/index.d.ts +1 -0
- package/dist/esm/types/src/tiptap/slash-mention/SlashSuggestionList.d.ts +12 -0
- package/dist/esm/types/src/tiptap/slash-mention/createSlashMentionExtension.d.ts +21 -0
- package/dist/esm/types/src/tiptap/slash-mention/defaultChatSlashItems.d.ts +4 -0
- package/dist/esm/types/src/tiptap/slash-mention/index.d.ts +5 -0
- package/dist/esm/types/src/tiptap/slash-mention/types.d.ts +25 -0
- package/package.json +15 -1
- package/src/components/ui/Chat/Chat.types.ts +4 -0
- package/src/components/ui/Chat/ChatChrome/ChatChrome.tsx +4 -0
- package/src/components/ui/Chat/ChatChrome/ChatChrome.types.ts +5 -0
- package/src/components/ui/Chat/ChatPrompt/ChatPrompt.helpers.ts +43 -0
- package/src/components/ui/Chat/ChatPrompt/ChatPrompt.styl +33 -5
- package/src/components/ui/Chat/ChatPrompt/ChatPrompt.styl.d.ts +2 -1
- package/src/components/ui/Chat/ChatPrompt/ChatPrompt.tsx +50 -106
- package/src/components/ui/Chat/ChatPrompt/ChatPromptComposer.tsx +93 -0
- package/src/components/ui/Chat/ChatPrompt/chatPromptDoc.ts +18 -0
- package/src/components/ui/Chat/ChatPrompt/useChatPromptEditor.ts +214 -0
- package/src/components/ui/Chat/index.ts +1 -0
- package/src/components/ui/Tooltip/Tooltip.styl +1 -0
- package/src/docs/pages/ChatSlashCommandsPage.tsx +139 -0
- package/src/docs/pages/TooltipPage.tsx +1 -1
- package/src/docs/registry.ts +6 -0
- package/src/index.ts +1 -0
- package/src/tiptap/slash-mention/SlashSuggestionList.styl +48 -0
- package/src/tiptap/slash-mention/SlashSuggestionList.styl.d.ts +11 -0
- package/src/tiptap/slash-mention/SlashSuggestionList.tsx +109 -0
- package/src/tiptap/slash-mention/createSlashMentionExtension.ts +217 -0
- package/src/tiptap/slash-mention/defaultChatSlashItems.ts +18 -0
- package/src/tiptap/slash-mention/index.ts +16 -0
- package/src/tiptap/slash-mention/types.ts +29 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Empty default: pass `slashCommandItems` from the app to enable `/` palette. */
|
|
2
|
+
const DEFAULT_CHAT_SLASH_ITEMS = [];
|
|
3
|
+
function filterSlashItems(items, query) {
|
|
4
|
+
const q = query.trim().toLowerCase();
|
|
5
|
+
if (!q)
|
|
6
|
+
return items;
|
|
7
|
+
return items.filter(item => item.id.toLowerCase().includes(q) ||
|
|
8
|
+
item.label.toLowerCase().includes(q) ||
|
|
9
|
+
(item.description?.toLowerCase().includes(q) ?? false));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export { DEFAULT_CHAT_SLASH_ITEMS, filterSlashItems };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { HTMLAttributes, ReactNode } from 'react';
|
|
2
|
+
import type { SlashCommandItem } from '#uilib/tiptap/slash-mention/types';
|
|
2
3
|
import type { PresetScriptGraph } from './ChatMessage/presetScript';
|
|
3
4
|
export declare enum MessageRole {
|
|
4
5
|
USER = "user",
|
|
@@ -81,6 +82,8 @@ export interface ChatPromptProps {
|
|
|
81
82
|
attachmentAccept?: string;
|
|
82
83
|
/** Called when the user picks files via the attach button. */
|
|
83
84
|
onAttachmentFiles?: (files: File[]) => void;
|
|
85
|
+
/** Slash menu (`/`); omit or pass empty to disable Mention trigger (no default items). */
|
|
86
|
+
slashCommandItems?: SlashCommandItem[];
|
|
84
87
|
}
|
|
85
88
|
export interface ChatMessageProps {
|
|
86
89
|
role: MessageRole;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { ChatChromeProps } from './ChatChrome.types';
|
|
2
|
-
export declare function ChatChrome({ showResizeHandle, resizeHandle, onClose, isEmpty, renderPresets, messages, onQuickReply, suppressedQuickReplyKeys, isLoading, scriptContinueLabel, onScriptContinue, renderMessageChart, showBranchActionsRow, showSyntheticBranchButtons, unusedBranchKeys, isScriptComplete, onGenerateDashboard, generatingDashboard, onGenerateDashboardClick, showInlinePresets, isLastMessageFromUser, scrollRef, effectiveScopeId, onPromptSubmit, onChatDeleted, promptPrefill, footerClassName, emptyState, allowedAttachments, allowPdfAttachments, onAttachmentsDropped, }: ChatChromeProps): import("react/jsx-runtime").JSX.Element;
|
|
2
|
+
export declare function ChatChrome({ showResizeHandle, resizeHandle, onClose, isEmpty, renderPresets, messages, onQuickReply, suppressedQuickReplyKeys, isLoading, scriptContinueLabel, onScriptContinue, renderMessageChart, showBranchActionsRow, showSyntheticBranchButtons, unusedBranchKeys, isScriptComplete, onGenerateDashboard, generatingDashboard, onGenerateDashboardClick, showInlinePresets, isLastMessageFromUser, scrollRef, effectiveScopeId, onPromptSubmit, onChatDeleted, promptPrefill, footerClassName, emptyState, allowedAttachments, allowPdfAttachments, onAttachmentsDropped, slashCommandItems, promptPlaceholder, }: ChatChromeProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -2,6 +2,7 @@ import type { RefObject } from 'react';
|
|
|
2
2
|
import type { ChatAttachmentDropItem, Message } from '#uilib/components/ui/Chat/Chat.types';
|
|
3
3
|
import type { ChatEmptyStateProps } from '#uilib/components/ui/Chat/ChatEmptyState/ChatEmptyState.types';
|
|
4
4
|
import type { ChatPresetsLayout } from '#uilib/components/ui/Chat/ChatPresets';
|
|
5
|
+
import type { SlashCommandItem } from '#uilib/tiptap/slash-mention/types';
|
|
5
6
|
import type { ScrollRef } from '@homecode/ui';
|
|
6
7
|
export type ChatChromeResizeHandleConfig = {
|
|
7
8
|
isActive: boolean;
|
|
@@ -48,4 +49,8 @@ export interface ChatChromeProps {
|
|
|
48
49
|
allowPdfAttachments?: boolean;
|
|
49
50
|
/** Optional hook when attachments are sent with a message. */
|
|
50
51
|
onAttachmentsDropped?: (items: ChatAttachmentDropItem[]) => void | Promise<void>;
|
|
52
|
+
/** Slash menu (`/`), forwarded to `Chat.Prompt`; omit or pass empty list to disable slash palette. */
|
|
53
|
+
slashCommandItems?: SlashCommandItem[];
|
|
54
|
+
/** Composer placeholder forwarded to `Chat.Prompt`. */
|
|
55
|
+
promptPlaceholder?: string;
|
|
51
56
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { ChatPromptProps } from '../Chat.types';
|
|
2
|
-
export declare function ChatPrompt({ onSubmit, placeholder, className, footer, prefillMessage, attachments, onRemoveAttachment, disabled, attachmentAccept, onAttachmentFiles, }: ChatPromptProps): import("react/jsx-runtime").JSX.Element;
|
|
2
|
+
export declare function ChatPrompt({ onSubmit, placeholder, className, footer, prefillMessage, slashCommandItems, attachments, onRemoveAttachment, disabled, attachmentAccept, onAttachmentFiles, }: ChatPromptProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
/** Keep in sync with `ChatPrompt.styl` `INPUT_MAX_HEIGHT` / `min-height`. */
|
|
2
2
|
export declare const PROMPT_INPUT_MAX_HEIGHT_PX = 200;
|
|
3
3
|
export declare const PROMPT_INPUT_MIN_HEIGHT_PX = 40;
|
|
4
|
+
/** ProseMirror `view.dom` exists only after TipTap mounts `<EditorContent />`; accessing `.view` can throw earlier. */
|
|
5
|
+
export declare function chatPromptSafeEditorDom(editor: {
|
|
6
|
+
readonly isDestroyed?: boolean;
|
|
7
|
+
} | null | undefined): HTMLElement | null;
|
|
4
8
|
export declare function chatPromptTextareaFloorPx(cs: CSSStyleDeclaration): number;
|
|
5
9
|
/** Autosizing textarea inside flex shells: empty → floor only; typed → collapse measure + clamp. */
|
|
6
10
|
export declare function syncChatPromptTextareaHeight(el: HTMLTextAreaElement, message: string): void;
|
|
11
|
+
/** Same sizing rules for TipTap `.ProseMirror` root (`text` mirrors doc plain text length for empty-state). */
|
|
12
|
+
export declare function syncChatPromptComposerHeight(el: HTMLElement, text: string): void;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type KeyboardEvent as ReactKeyboardEvent } from 'react';
|
|
2
|
+
import type { Editor } from '@tiptap/core';
|
|
3
|
+
import type { ChatAttachmentDropItem } from '../Chat.types';
|
|
4
|
+
export type ChatPromptComposerProps = {
|
|
5
|
+
editor: Editor;
|
|
6
|
+
disabled: boolean;
|
|
7
|
+
trimmedMessage: string;
|
|
8
|
+
attachments: ChatAttachmentDropItem[];
|
|
9
|
+
attachmentAccept?: string;
|
|
10
|
+
onAttachmentFiles?: (files: File[]) => void;
|
|
11
|
+
onComposerKeyDown: (event: ReactKeyboardEvent) => void;
|
|
12
|
+
};
|
|
13
|
+
export declare function ChatPromptComposer({ editor, disabled, trimmedMessage, attachments, attachmentAccept, onAttachmentFiles, onComposerKeyDown, }: ChatPromptComposerProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type KeyboardEvent as ReactKeyboardEvent } from 'react';
|
|
2
|
+
import type { SlashCommandItem } from '#uilib/tiptap/slash-mention/types';
|
|
3
|
+
import type { Editor } from '@tiptap/core';
|
|
4
|
+
export type UseChatPromptEditorOptions = {
|
|
5
|
+
disabled: boolean;
|
|
6
|
+
placeholder?: string;
|
|
7
|
+
slashCommandItems?: SlashCommandItem[];
|
|
8
|
+
prefillMessage?: string | null;
|
|
9
|
+
/** Staged attachment count — Enter-to-send when text empty but files present. */
|
|
10
|
+
attachmentsCount?: number;
|
|
11
|
+
/** Called when user presses Enter to send (after guards). */
|
|
12
|
+
onEnterSubmit: () => void;
|
|
13
|
+
};
|
|
14
|
+
export type UseChatPromptEditorResult = {
|
|
15
|
+
editor: Editor | null;
|
|
16
|
+
trimmedMessage: string;
|
|
17
|
+
resetAfterSend: () => void;
|
|
18
|
+
handleComposerKeyDown: (event: ReactKeyboardEvent) => void;
|
|
19
|
+
};
|
|
20
|
+
export declare function useChatPromptEditor({ disabled, placeholder, slashCommandItems, prefillMessage, attachmentsCount, onEnterSubmit, }: UseChatPromptEditorOptions): UseChatPromptEditorResult;
|
|
@@ -15,4 +15,5 @@ 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
17
|
export { MessageRole } from './Chat.types';
|
|
18
|
+
export type { SlashCommandItem } from '#uilib/tiptap/slash-mention/types';
|
|
18
19
|
export { CsvIcon } from '../../icons/CsvIcon/CsvIcon';
|
|
@@ -3,7 +3,7 @@ declare const Input: React.ForwardRefExoticComponent<(Omit<Omit<React.DetailedHT
|
|
|
3
3
|
type?: React.ComponentProps<"input">["type"];
|
|
4
4
|
size?: "sm" | "md" | "lg";
|
|
5
5
|
variant?: "default" | "clean";
|
|
6
|
-
}, "ref"> | Omit<Omit<React.DetailedHTMLProps<React.TextareaHTMLAttributes<HTMLTextAreaElement>, HTMLTextAreaElement>, "
|
|
6
|
+
}, "ref"> | Omit<Omit<React.DetailedHTMLProps<React.TextareaHTMLAttributes<HTMLTextAreaElement>, HTMLTextAreaElement>, "type" | "size"> & {
|
|
7
7
|
type: "textarea";
|
|
8
8
|
size?: "sm" | "md" | "lg";
|
|
9
9
|
variant?: "default" | "clean";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function ChatSlashCommandsPage(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -6,6 +6,7 @@ export { DEFAULT_THEME_ACTIVE_COLOR } from './docs/lib/theme';
|
|
|
6
6
|
export * from './sybilion-auth';
|
|
7
7
|
export * from './types/sybilionDatasetSnapshots';
|
|
8
8
|
export * from './contexts/chat-context';
|
|
9
|
+
export * from './tiptap/slash-mention';
|
|
9
10
|
export * from './types/chat-api.types';
|
|
10
11
|
export * from './components/ui/AnalysesSelector';
|
|
11
12
|
export * from './components/ui/AnalysisLineIcon';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { RefObject } from 'react';
|
|
2
|
+
import type { SlashCommandItem } from './types';
|
|
3
|
+
export type SlashSuggestionListHandle = {
|
|
4
|
+
onKeyboardEvent: (event: KeyboardEvent) => boolean;
|
|
5
|
+
};
|
|
6
|
+
/** Props forwarded from Tiptap suggestion renderer + our wiring. */
|
|
7
|
+
export type SlashSuggestionListProps = {
|
|
8
|
+
items: SlashCommandItem[];
|
|
9
|
+
command: (item: SlashCommandItem) => void;
|
|
10
|
+
listHandleRef: RefObject<SlashSuggestionListHandle | null>;
|
|
11
|
+
};
|
|
12
|
+
export declare function SlashSuggestionList(props: SlashSuggestionListProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { MutableRefObject } from 'react';
|
|
2
|
+
import type { SuggestionProps } from '@tiptap/suggestion';
|
|
3
|
+
import { type SlashSuggestionListHandle } from './SlashSuggestionList';
|
|
4
|
+
import type { CreateSlashMentionExtensionOptions, SlashCommandItem } from './types';
|
|
5
|
+
export declare function slashMentionSuggestionRender(uiRef: MutableRefObject<SlashSuggestionListHandle | null>): {
|
|
6
|
+
onStart?: (props: SuggestionProps<SlashCommandItem, SlashCommandItem>) => void;
|
|
7
|
+
onUpdate?: (props: SuggestionProps<SlashCommandItem, SlashCommandItem>) => void;
|
|
8
|
+
onExit?: (props: SuggestionProps<SlashCommandItem, SlashCommandItem>) => void;
|
|
9
|
+
onKeyDown?: (p: {
|
|
10
|
+
view: unknown;
|
|
11
|
+
event: KeyboardEvent;
|
|
12
|
+
range: {
|
|
13
|
+
from: number;
|
|
14
|
+
to: number;
|
|
15
|
+
};
|
|
16
|
+
}) => boolean;
|
|
17
|
+
};
|
|
18
|
+
export type CreateSlashMentionExtensionConfiguredOptions = CreateSlashMentionExtensionOptions & {
|
|
19
|
+
onSuggestionUiActiveChange?: (active: boolean) => void;
|
|
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>;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { SlashCommandItem } from './types';
|
|
2
|
+
/** Empty default: pass `slashCommandItems` from the app to enable `/` palette. */
|
|
3
|
+
export declare const DEFAULT_CHAT_SLASH_ITEMS: SlashCommandItem[];
|
|
4
|
+
export declare function filterSlashItems(items: SlashCommandItem[], query: string): SlashCommandItem[];
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type { SlashCommandItem, SlashOnItemCommand, SlashItemCommandContext, CreateSlashMentionExtensionOptions, } from './types';
|
|
2
|
+
export { DEFAULT_CHAT_SLASH_ITEMS, filterSlashItems, } from './defaultChatSlashItems';
|
|
3
|
+
export { createSlashMentionExtension, slashMentionSuggestionRender, } from './createSlashMentionExtension';
|
|
4
|
+
export type { CreateSlashMentionExtensionConfiguredOptions } from './createSlashMentionExtension';
|
|
5
|
+
export type { SlashSuggestionListHandle } from './SlashSuggestionList';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Editor, Range } from '@tiptap/core';
|
|
2
|
+
export type SlashCommandItem = {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
};
|
|
7
|
+
export type SlashItemCommandContext = {
|
|
8
|
+
editor: Editor;
|
|
9
|
+
range: Range;
|
|
10
|
+
item: SlashCommandItem;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* If provided, run before default mention insertion. Return true to skip inserting a mention node.
|
|
14
|
+
*/
|
|
15
|
+
export type SlashOnItemCommand = (ctx: SlashItemCommandContext) => boolean;
|
|
16
|
+
export type CreateSlashMentionExtensionOptions = {
|
|
17
|
+
/** Items shown in the slash menu (filtered by query after `/`). */
|
|
18
|
+
items: SlashCommandItem[];
|
|
19
|
+
/** Trigger character; use `/` for slash commands. */
|
|
20
|
+
slashChar?: string;
|
|
21
|
+
/** Optional custom plugin key when multiple slash editors exist. */
|
|
22
|
+
pluginKey?: import('@tiptap/pm/state').PluginKey;
|
|
23
|
+
/** Custom handler (e.g. insert a block node instead of a mention). */
|
|
24
|
+
onItemCommand?: SlashOnItemCommand;
|
|
25
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sybilion/uilib",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.36",
|
|
4
4
|
"description": "Sybilion Design System — React UI components (Webpack + Stylus)",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public",
|
|
@@ -117,6 +117,13 @@
|
|
|
117
117
|
"peerDependencies": {
|
|
118
118
|
"@auth0/auth0-react": "^2.3.1",
|
|
119
119
|
"@sybilion/platform-sdk": ">=0.0.1",
|
|
120
|
+
"@tiptap/core": "^3.22.0",
|
|
121
|
+
"@tiptap/extension-mention": "^3.22.0",
|
|
122
|
+
"@tiptap/extensions": "^3.22.0",
|
|
123
|
+
"@tiptap/pm": "^3.22.0",
|
|
124
|
+
"@tiptap/react": "^3.22.0",
|
|
125
|
+
"@tiptap/starter-kit": "^3.22.0",
|
|
126
|
+
"@tiptap/suggestion": "^3.22.0",
|
|
120
127
|
"react": ">=18.0.0",
|
|
121
128
|
"react-dom": ">=18.0.0",
|
|
122
129
|
"react-router-dom": ">=6.0.0",
|
|
@@ -151,6 +158,13 @@
|
|
|
151
158
|
"@testing-library/dom": "^10.4.1",
|
|
152
159
|
"@testing-library/jest-dom": "^6.5.0",
|
|
153
160
|
"@testing-library/react": "^16.0.1",
|
|
161
|
+
"@tiptap/core": "^3.22.1",
|
|
162
|
+
"@tiptap/extension-mention": "^3.22.1",
|
|
163
|
+
"@tiptap/extensions": "^3.22.1",
|
|
164
|
+
"@tiptap/pm": "^3.22.1",
|
|
165
|
+
"@tiptap/react": "^3.22.1",
|
|
166
|
+
"@tiptap/starter-kit": "^3.22.1",
|
|
167
|
+
"@tiptap/suggestion": "^3.22.1",
|
|
154
168
|
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
|
|
155
169
|
"@types/jest": "^29.4.0",
|
|
156
170
|
"@types/node": "^18.14.0",
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { HTMLAttributes, ReactNode } from 'react';
|
|
2
2
|
|
|
3
|
+
import type { SlashCommandItem } from '#uilib/tiptap/slash-mention/types';
|
|
4
|
+
|
|
3
5
|
import type { PresetScriptGraph } from './ChatMessage/presetScript';
|
|
4
6
|
|
|
5
7
|
export enum MessageRole {
|
|
@@ -92,6 +94,8 @@ export interface ChatPromptProps {
|
|
|
92
94
|
attachmentAccept?: string;
|
|
93
95
|
/** Called when the user picks files via the attach button. */
|
|
94
96
|
onAttachmentFiles?: (files: File[]) => void;
|
|
97
|
+
/** Slash menu (`/`); omit or pass empty to disable Mention trigger (no default items). */
|
|
98
|
+
slashCommandItems?: SlashCommandItem[];
|
|
95
99
|
}
|
|
96
100
|
|
|
97
101
|
export interface ChatMessageProps {
|
|
@@ -56,6 +56,8 @@ export function ChatChrome({
|
|
|
56
56
|
allowedAttachments,
|
|
57
57
|
allowPdfAttachments = false,
|
|
58
58
|
onAttachmentsDropped,
|
|
59
|
+
slashCommandItems,
|
|
60
|
+
promptPlaceholder,
|
|
59
61
|
}: ChatChromeProps) {
|
|
60
62
|
const filteredAllowedAttachments = useMemo(
|
|
61
63
|
() => filterToTextAttachments(allowedAttachments),
|
|
@@ -287,6 +289,8 @@ export function ChatChrome({
|
|
|
287
289
|
attachments={pendingAttachments}
|
|
288
290
|
onRemoveAttachment={handleRemoveAttachment}
|
|
289
291
|
prefillMessage={promptPrefill ?? undefined}
|
|
292
|
+
placeholder={promptPlaceholder}
|
|
293
|
+
slashCommandItems={slashCommandItems}
|
|
290
294
|
attachmentAccept={
|
|
291
295
|
attachmentsDropzoneEnabled ? attachmentAccept : undefined
|
|
292
296
|
}
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
} from '#uilib/components/ui/Chat/Chat.types';
|
|
7
7
|
import type { ChatEmptyStateProps } from '#uilib/components/ui/Chat/ChatEmptyState/ChatEmptyState.types';
|
|
8
8
|
import type { ChatPresetsLayout } from '#uilib/components/ui/Chat/ChatPresets';
|
|
9
|
+
import type { SlashCommandItem } from '#uilib/tiptap/slash-mention/types';
|
|
9
10
|
import type { ScrollRef } from '@homecode/ui';
|
|
10
11
|
|
|
11
12
|
export type ChatChromeResizeHandleConfig = {
|
|
@@ -62,4 +63,8 @@ export interface ChatChromeProps {
|
|
|
62
63
|
onAttachmentsDropped?: (
|
|
63
64
|
items: ChatAttachmentDropItem[],
|
|
64
65
|
) => void | Promise<void>;
|
|
66
|
+
/** Slash menu (`/`), forwarded to `Chat.Prompt`; omit or pass empty list to disable slash palette. */
|
|
67
|
+
slashCommandItems?: SlashCommandItem[];
|
|
68
|
+
/** Composer placeholder forwarded to `Chat.Prompt`. */
|
|
69
|
+
promptPlaceholder?: string;
|
|
65
70
|
}
|
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
export const PROMPT_INPUT_MAX_HEIGHT_PX = 200;
|
|
3
3
|
export const PROMPT_INPUT_MIN_HEIGHT_PX = 40;
|
|
4
4
|
|
|
5
|
+
/** ProseMirror `view.dom` exists only after TipTap mounts `<EditorContent />`; accessing `.view` can throw earlier. */
|
|
6
|
+
export function chatPromptSafeEditorDom(
|
|
7
|
+
editor: { readonly isDestroyed?: boolean } | null | undefined,
|
|
8
|
+
): HTMLElement | null {
|
|
9
|
+
if (!editor || editor.isDestroyed) return null;
|
|
10
|
+
try {
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
12
|
+
const dom = (editor as any).view?.dom as unknown;
|
|
13
|
+
return dom instanceof HTMLElement ? dom : null;
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
5
19
|
export function chatPromptTextareaFloorPx(cs: CSSStyleDeclaration): number {
|
|
6
20
|
const padY =
|
|
7
21
|
(Number.parseFloat(cs.paddingTop) || 0) +
|
|
@@ -41,3 +55,32 @@ export function syncChatPromptTextareaHeight(
|
|
|
41
55
|
el.style.overflowY =
|
|
42
56
|
contentHeight > PROMPT_INPUT_MAX_HEIGHT_PX ? 'auto' : 'hidden';
|
|
43
57
|
}
|
|
58
|
+
|
|
59
|
+
/** Same sizing rules for TipTap `.ProseMirror` root (`text` mirrors doc plain text length for empty-state). */
|
|
60
|
+
export function syncChatPromptComposerHeight(
|
|
61
|
+
el: HTMLElement,
|
|
62
|
+
text: string,
|
|
63
|
+
): void {
|
|
64
|
+
const floor = chatPromptTextareaFloorPx(getComputedStyle(el));
|
|
65
|
+
|
|
66
|
+
el.style.overflowY = 'hidden';
|
|
67
|
+
let contentHeight: number;
|
|
68
|
+
|
|
69
|
+
const empty = text.trim() === '';
|
|
70
|
+
|
|
71
|
+
if (empty) {
|
|
72
|
+
contentHeight = floor;
|
|
73
|
+
} else {
|
|
74
|
+
el.style.height = '0';
|
|
75
|
+
contentHeight = el.scrollHeight;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const h = Math.min(
|
|
79
|
+
Math.max(contentHeight, floor),
|
|
80
|
+
PROMPT_INPUT_MAX_HEIGHT_PX,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
el.style.height = `${h}px`;
|
|
84
|
+
el.style.overflowY =
|
|
85
|
+
contentHeight > PROMPT_INPUT_MAX_HEIGHT_PX ? 'auto' : 'hidden';
|
|
86
|
+
}
|
|
@@ -26,20 +26,48 @@ INPUT_MAX_HEIGHT = 200px
|
|
|
26
26
|
flex-shrink 0
|
|
27
27
|
align-self flex-end
|
|
28
28
|
|
|
29
|
-
.
|
|
29
|
+
.editorWrap
|
|
30
30
|
flex 1
|
|
31
31
|
min-width 0
|
|
32
|
+
align-self stretch
|
|
33
|
+
|
|
34
|
+
.editorMount
|
|
35
|
+
display flex
|
|
36
|
+
flex 1
|
|
37
|
+
flex-direction column
|
|
38
|
+
min-width 0
|
|
32
39
|
min-height 40px
|
|
33
40
|
max-height INPUT_MAX_HEIGHT
|
|
34
|
-
resize none
|
|
35
|
-
overflow-y auto
|
|
36
41
|
border none
|
|
37
|
-
padding
|
|
42
|
+
padding 0 !important
|
|
38
43
|
border-radius 0 !important
|
|
39
44
|
box-shadow none !important
|
|
45
|
+
background transparent
|
|
46
|
+
|
|
47
|
+
&:focus-within
|
|
48
|
+
box-shadow none !important
|
|
40
49
|
|
|
41
|
-
|
|
50
|
+
& :global(.ProseMirror)
|
|
51
|
+
outline none !important
|
|
52
|
+
border none !important
|
|
53
|
+
flex 1
|
|
54
|
+
padding var(--p-2) 0 0 !important
|
|
55
|
+
margin 0
|
|
56
|
+
white-space pre-wrap
|
|
57
|
+
word-break break-word
|
|
58
|
+
resize none !important
|
|
42
59
|
box-shadow none !important
|
|
60
|
+
min-height 40px !important
|
|
61
|
+
max-height INPUT_MAX_HEIGHT !important
|
|
62
|
+
overflow-y auto !important
|
|
63
|
+
overflow-x hidden !important
|
|
64
|
+
|
|
65
|
+
& :global(.ProseMirror p.is-empty::before)
|
|
66
|
+
color var(--muted-foreground)
|
|
67
|
+
content attr(data-placeholder)
|
|
68
|
+
float left
|
|
69
|
+
height 0
|
|
70
|
+
pointer-events none
|
|
43
71
|
|
|
44
72
|
.submitColumn
|
|
45
73
|
display flex
|
|
@@ -1,22 +1,11 @@
|
|
|
1
1
|
import cn from 'classnames';
|
|
2
|
-
import {
|
|
3
|
-
ChangeEvent,
|
|
4
|
-
FormEvent,
|
|
5
|
-
useEffect,
|
|
6
|
-
useLayoutEffect,
|
|
7
|
-
useRef,
|
|
8
|
-
useState,
|
|
9
|
-
} from 'react';
|
|
2
|
+
import { FormEvent, useCallback, useRef } from 'react';
|
|
10
3
|
|
|
11
|
-
import useEvent from '#uilib/hooks/useEvent';
|
|
12
|
-
import { PaperclipIcon, SendHorizontalIcon } from 'lucide-react';
|
|
13
|
-
|
|
14
|
-
import { Button } from '../../Button';
|
|
15
|
-
import { Input } from '../../Input';
|
|
16
4
|
import type { ChatPromptProps } from '../Chat.types';
|
|
17
|
-
import { syncChatPromptTextareaHeight } from './ChatPrompt.helpers';
|
|
18
5
|
import S from './ChatPrompt.styl';
|
|
19
6
|
import { ChatPromptAttachments } from './ChatPromptAttachments';
|
|
7
|
+
import { ChatPromptComposer } from './ChatPromptComposer';
|
|
8
|
+
import { useChatPromptEditor } from './useChatPromptEditor';
|
|
20
9
|
|
|
21
10
|
export function ChatPrompt({
|
|
22
11
|
onSubmit,
|
|
@@ -24,117 +13,72 @@ export function ChatPrompt({
|
|
|
24
13
|
className,
|
|
25
14
|
footer,
|
|
26
15
|
prefillMessage,
|
|
16
|
+
slashCommandItems,
|
|
27
17
|
attachments = [],
|
|
28
18
|
onRemoveAttachment,
|
|
29
19
|
disabled = false,
|
|
30
20
|
attachmentAccept,
|
|
31
21
|
onAttachmentFiles,
|
|
32
22
|
}: ChatPromptProps) {
|
|
33
|
-
const
|
|
34
|
-
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
35
|
-
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
36
|
-
const showAttachButton = Boolean(attachmentAccept && onAttachmentFiles);
|
|
37
|
-
|
|
38
|
-
useLayoutEffect(() => {
|
|
39
|
-
const el = inputRef.current;
|
|
40
|
-
if (!el) return;
|
|
41
|
-
syncChatPromptTextareaHeight(el, message);
|
|
42
|
-
}, [message]);
|
|
23
|
+
const attachmentsCount = attachments.length;
|
|
43
24
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
25
|
+
const emitSubmitRef = useRef(() => {});
|
|
26
|
+
const { editor, trimmedMessage, resetAfterSend, handleComposerKeyDown } =
|
|
27
|
+
useChatPromptEditor({
|
|
28
|
+
disabled,
|
|
29
|
+
placeholder,
|
|
30
|
+
slashCommandItems,
|
|
31
|
+
prefillMessage,
|
|
32
|
+
attachmentsCount,
|
|
33
|
+
onEnterSubmit: () => emitSubmitRef.current(),
|
|
34
|
+
});
|
|
49
35
|
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
36
|
+
const emitSubmitAndClear = useCallback(() => {
|
|
37
|
+
if (!editor) return;
|
|
38
|
+
if (!trimmedMessage && attachmentsCount === 0) return;
|
|
53
39
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
40
|
+
const msg = trimmedMessage;
|
|
41
|
+
resetAfterSend();
|
|
42
|
+
onSubmit(msg, attachmentsCount > 0 ? attachments : undefined);
|
|
43
|
+
}, [
|
|
44
|
+
attachments,
|
|
45
|
+
attachmentsCount,
|
|
46
|
+
editor,
|
|
47
|
+
onSubmit,
|
|
48
|
+
resetAfterSend,
|
|
49
|
+
trimmedMessage,
|
|
50
|
+
]);
|
|
60
51
|
|
|
61
|
-
|
|
62
|
-
const files = Array.from(e.target.files ?? []);
|
|
63
|
-
e.target.value = '';
|
|
64
|
-
if (files.length > 0) {
|
|
65
|
-
onAttachmentFiles?.(files);
|
|
66
|
-
}
|
|
67
|
-
};
|
|
52
|
+
emitSubmitRef.current = emitSubmitAndClear;
|
|
68
53
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
e.preventDefault();
|
|
74
|
-
handleSubmit(e);
|
|
75
|
-
setMessage('');
|
|
76
|
-
}
|
|
54
|
+
const handleSubmitForm = useCallback(
|
|
55
|
+
(e?: FormEvent) => {
|
|
56
|
+
e?.preventDefault();
|
|
57
|
+
emitSubmitAndClear();
|
|
77
58
|
},
|
|
78
|
-
|
|
59
|
+
[emitSubmitAndClear],
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
if (!editor) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
79
65
|
|
|
80
66
|
return (
|
|
81
|
-
<form onSubmit={
|
|
67
|
+
<form onSubmit={handleSubmitForm} className={cn(S.root, className)}>
|
|
82
68
|
<ChatPromptAttachments
|
|
83
69
|
attachments={attachments}
|
|
84
70
|
onRemove={index => onRemoveAttachment?.(index)}
|
|
85
71
|
disabled={disabled}
|
|
86
72
|
/>
|
|
87
|
-
<
|
|
88
|
-
{
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
disabled={disabled}
|
|
97
|
-
onChange={handleFileInputChange}
|
|
98
|
-
/>
|
|
99
|
-
<Button
|
|
100
|
-
type="button"
|
|
101
|
-
variant="ghost"
|
|
102
|
-
icon
|
|
103
|
-
size="sm"
|
|
104
|
-
className={S.attachButton}
|
|
105
|
-
aria-label="Attach file"
|
|
106
|
-
disabled={disabled}
|
|
107
|
-
onClick={e => {
|
|
108
|
-
e.preventDefault();
|
|
109
|
-
fileInputRef.current?.click();
|
|
110
|
-
}}
|
|
111
|
-
>
|
|
112
|
-
<PaperclipIcon size={16} />
|
|
113
|
-
</Button>
|
|
114
|
-
</>
|
|
115
|
-
) : null}
|
|
116
|
-
|
|
117
|
-
<Input
|
|
118
|
-
ref={inputRef}
|
|
119
|
-
type="textarea"
|
|
120
|
-
rows={1}
|
|
121
|
-
value={message}
|
|
122
|
-
onChange={e => setMessage(e.target.value)}
|
|
123
|
-
placeholder={placeholder || 'Type a message...'}
|
|
124
|
-
className={cn(S.input)}
|
|
125
|
-
/>
|
|
126
|
-
|
|
127
|
-
<div className={S.submitColumn}>
|
|
128
|
-
<Button
|
|
129
|
-
type="submit"
|
|
130
|
-
size="sm"
|
|
131
|
-
disabled={disabled || (!message.trim() && attachments.length === 0)}
|
|
132
|
-
>
|
|
133
|
-
<SendHorizontalIcon size={16} />
|
|
134
|
-
</Button>
|
|
135
|
-
</div>
|
|
136
|
-
</div>
|
|
137
|
-
|
|
73
|
+
<ChatPromptComposer
|
|
74
|
+
editor={editor}
|
|
75
|
+
disabled={disabled}
|
|
76
|
+
trimmedMessage={trimmedMessage}
|
|
77
|
+
attachments={attachments}
|
|
78
|
+
attachmentAccept={attachmentAccept}
|
|
79
|
+
onAttachmentFiles={onAttachmentFiles}
|
|
80
|
+
onComposerKeyDown={handleComposerKeyDown}
|
|
81
|
+
/>
|
|
138
82
|
{footer}
|
|
139
83
|
</form>
|
|
140
84
|
);
|