@sybilion/uilib 1.3.43 → 1.3.44
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/ChatPrompt.js +2 -2
- package/dist/esm/components/ui/Chat/ChatPrompt/ChatPrompt.styl.js +1 -1
- package/dist/esm/components/ui/Chat/ChatPrompt/ChatPromptComposer.js +2 -2
- package/dist/esm/components/ui/Chat/ChatPrompt/useChatPromptEditor.js +24 -17
- package/dist/esm/components/ui/Page/PageHeader/PageHeader.js +1 -1
- package/dist/esm/components/ui/TextWithDeferTooltip/TextWithDeferTooltip.js +23 -1
- package/dist/esm/components/widgets/DriverCard/DriverCard.js +1 -1
- package/dist/esm/types/src/components/ui/Chat/ChatPrompt/ChatPromptComposer.d.ts +1 -3
- package/dist/esm/types/src/components/ui/Chat/ChatPrompt/useChatPromptEditor.d.ts +0 -2
- package/dist/esm/types/src/docs/pages/ChatSheetPage.d.ts +1 -0
- package/package.json +1 -1
- package/src/components/ui/Chat/ChatPrompt/ChatPrompt.styl +1 -1
- package/src/components/ui/Chat/ChatPrompt/ChatPrompt.tsx +9 -11
- package/src/components/ui/Chat/ChatPrompt/ChatPromptComposer.tsx +2 -9
- package/src/components/ui/Chat/ChatPrompt/useChatPromptEditor.ts +34 -21
- package/src/components/ui/Page/PageHeader/PageHeader.tsx +1 -1
- package/src/components/ui/TextWithDeferTooltip/TextWithDeferTooltip.tsx +35 -4
- package/src/components/widgets/DriverCard/DriverCard.tsx +5 -1
- package/src/docs/pages/ChatSheetPage.tsx +61 -0
- package/src/docs/pages/TooltipPage.tsx +0 -31
- package/src/docs/registry.ts +6 -0
|
@@ -9,7 +9,7 @@ import { useChatPromptEditor } from './useChatPromptEditor.js';
|
|
|
9
9
|
function ChatPrompt({ onSubmit, placeholder, className, footer, prefillMessage, slashCommandItems, onSlashItemCommand, attachments = [], onRemoveAttachment, disabled = false, attachmentAccept, onAttachmentFiles, }) {
|
|
10
10
|
const attachmentsCount = attachments.length;
|
|
11
11
|
const emitSubmitRef = useRef(() => { });
|
|
12
|
-
const { editor, trimmedMessage, resetAfterSend
|
|
12
|
+
const { editor, trimmedMessage, resetAfterSend } = useChatPromptEditor({
|
|
13
13
|
disabled,
|
|
14
14
|
placeholder,
|
|
15
15
|
slashCommandItems,
|
|
@@ -42,7 +42,7 @@ function ChatPrompt({ onSubmit, placeholder, className, footer, prefillMessage,
|
|
|
42
42
|
if (!editor) {
|
|
43
43
|
return null;
|
|
44
44
|
}
|
|
45
|
-
return (jsxs("form", { onSubmit: handleSubmitForm, className: cn(S.root, className), children: [jsx(ChatPromptAttachments, { attachments: attachments, onRemove: index => onRemoveAttachment?.(index), disabled: disabled }), jsx(ChatPromptComposer, { editor: editor, disabled: disabled, trimmedMessage: trimmedMessage, attachments: attachments, attachmentAccept: attachmentAccept, onAttachmentFiles: onAttachmentFiles
|
|
45
|
+
return (jsxs("form", { onSubmit: handleSubmitForm, className: cn(S.root, className), children: [jsx(ChatPromptAttachments, { attachments: attachments, onRemove: index => onRemoveAttachment?.(index), disabled: disabled }), jsx(ChatPromptComposer, { editor: editor, disabled: disabled, trimmedMessage: trimmedMessage, attachments: attachments, attachmentAccept: attachmentAccept, onAttachmentFiles: onAttachmentFiles }), footer] }));
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
export { ChatPrompt };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import styleInject from 'style-inject';
|
|
2
2
|
|
|
3
|
-
var css_248z = "@media (max-width:768px){:root{--page-x-padding:var(--p-6);--page-y-padding:var(--p-6)}}.ChatPrompt_root__5G5bq{align-items:stretch;display:flex;flex-direction:column;gap:var(--p-2);padding:var(--p-6);padding-top:var(--p-5);position:relative}.ChatPrompt_composer__H3c3N{align-items:flex-end;display:flex;flex-direction:row;gap:var(--p-3);position:relative;width:100%}.ChatPrompt_fileInput__xdgPn{display:none}.ChatPrompt_attachButton__gi-qF{align-self:flex-end;flex-shrink:0}.ChatPrompt_editorWrap__Q7gat{align-self:stretch;flex:1;min-width:0}.ChatPrompt_editorMount__Phh4D{background:transparent;border:none;border-radius:0!important;box-shadow:none!important;display:flex;flex:1;flex-direction:column;max-height:200px;min-height:40px;min-width:0;padding:0!important}.ChatPrompt_editorMount__Phh4D:focus-within{box-shadow:none!important}.ChatPrompt_editorMount__Phh4D .ProseMirror{border:none!important;box-shadow:none!important;flex:1;margin:0;max-height:200px!important;min-height:40px!important;outline:none!important;overflow-x:hidden!important;overflow-y:auto!important;padding:var(--p-2) 0 0!important;resize:none!important;white-space:pre-wrap;word-break:break-word}.ChatPrompt_editorMount__Phh4D .ProseMirror p.is-empty:before{color:var(--muted-foreground);content:attr(data-placeholder);float:left;height:0;pointer-events:none}.ChatPrompt_submitColumn__0rY1R{align-items:center;display:flex;flex-direction:column;flex-shrink:0;justify-content:flex-end}.ChatPrompt_submitColumn__0rY1R>button:focus{box-shadow:0 0 0 2px var(--brand-color-500)!important}.ChatPrompt_submitColumn__0rY1R>button:first-child{border:none;position:relative;transition:all .2s}.ChatPrompt_submitColumn__0rY1R>button:first-child:focus{transform:scale(1.2)}.ChatPrompt_submitColumn__0rY1R>button:first-child:before{bottom:-100%;content:\"\";left:-100%;position:absolute;right:-100%;top:-100%}.ChatPrompt_attachments__KG-fG{display:flex;flex-wrap:wrap;gap:var(--p-2);margin-bottom:var(--p-2)}.ChatPrompt_attachmentItem__QJk7J{flex:1 1 300px;max-width:300px;min-width:0}";
|
|
3
|
+
var css_248z = "@media (max-width:768px){:root{--page-x-padding:var(--p-6);--page-y-padding:var(--p-6)}}.ChatPrompt_root__5G5bq{align-items:stretch;display:flex;flex-direction:column;gap:var(--p-2);padding:var(--p-6);padding-top:var(--p-5);position:relative}.ChatPrompt_composer__H3c3N{align-items:flex-end;display:flex;flex-direction:row;gap:var(--p-3);position:relative;width:100%}.ChatPrompt_fileInput__xdgPn{display:none}.ChatPrompt_attachButton__gi-qF{align-self:flex-end;flex-shrink:0}.ChatPrompt_editorWrap__Q7gat{align-self:stretch;flex:1;min-width:0}.ChatPrompt_editorMount__Phh4D{background:transparent;border:none;border-radius:0!important;box-shadow:none!important;display:flex;flex:1;flex-direction:column;max-height:200px;min-height:40px;min-width:0;padding:0!important}.ChatPrompt_editorMount__Phh4D:focus-within{box-shadow:none!important}.ChatPrompt_editorMount__Phh4D .ProseMirror{border:none!important;box-shadow:none!important;flex:1;margin:0;max-height:200px!important;min-height:40px!important;outline:none!important;overflow-x:hidden!important;overflow-y:auto!important;padding:var(--p-2) 0 0!important;resize:none!important;white-space:pre-wrap;word-break:break-word}.ChatPrompt_editorMount__Phh4D .ProseMirror p.is-empty.is-editor-empty:before{color:var(--muted-foreground);content:attr(data-placeholder);float:left;height:0;pointer-events:none}.ChatPrompt_submitColumn__0rY1R{align-items:center;display:flex;flex-direction:column;flex-shrink:0;justify-content:flex-end}.ChatPrompt_submitColumn__0rY1R>button:focus{box-shadow:0 0 0 2px var(--brand-color-500)!important}.ChatPrompt_submitColumn__0rY1R>button:first-child{border:none;position:relative;transition:all .2s}.ChatPrompt_submitColumn__0rY1R>button:first-child:focus{transform:scale(1.2)}.ChatPrompt_submitColumn__0rY1R>button:first-child:before{bottom:-100%;content:\"\";left:-100%;position:absolute;right:-100%;top:-100%}.ChatPrompt_attachments__KG-fG{display:flex;flex-wrap:wrap;gap:var(--p-2);margin-bottom:var(--p-2)}.ChatPrompt_attachmentItem__QJk7J{flex:1 1 300px;max-width:300px;min-width:0}";
|
|
4
4
|
var S = {"root":"ChatPrompt_root__5G5bq","composer":"ChatPrompt_composer__H3c3N","fileInput":"ChatPrompt_fileInput__xdgPn","attachButton":"ChatPrompt_attachButton__gi-qF","editorWrap":"ChatPrompt_editorWrap__Q7gat","editorMount":"ChatPrompt_editorMount__Phh4D","submitColumn":"ChatPrompt_submitColumn__0rY1R","attachments":"ChatPrompt_attachments__KG-fG","attachmentItem":"ChatPrompt_attachmentItem__QJk7J"};
|
|
5
5
|
styleInject(css_248z);
|
|
6
6
|
|
|
@@ -5,7 +5,7 @@ import { PaperclipIcon, SendHorizontalIcon } from 'lucide-react';
|
|
|
5
5
|
import { Button } from '../../Button/Button.js';
|
|
6
6
|
import S from './ChatPrompt.styl.js';
|
|
7
7
|
|
|
8
|
-
function ChatPromptComposer({ editor, disabled, trimmedMessage, attachments, attachmentAccept, onAttachmentFiles,
|
|
8
|
+
function ChatPromptComposer({ editor, disabled, trimmedMessage, attachments, attachmentAccept, onAttachmentFiles, }) {
|
|
9
9
|
const fileInputRef = useRef(null);
|
|
10
10
|
const showAttachButton = Boolean(attachmentAccept && onAttachmentFiles);
|
|
11
11
|
const handleFileInputChange = useCallback((e) => {
|
|
@@ -19,7 +19,7 @@ function ChatPromptComposer({ editor, disabled, trimmedMessage, attachments, att
|
|
|
19
19
|
return (jsxs("div", { className: S.composer, children: [showAttachButton ? (jsxs(Fragment, { children: [jsx("input", { ref: fileInputRef, type: "file", accept: attachmentAccept, multiple: true, className: S.fileInput, disabled: disabled, onChange: handleFileInputChange }), jsx(Button, { type: "button", variant: "ghost", icon: true, size: "sm", className: S.attachButton, "aria-label": "Attach file", disabled: disabled, onClick: e => {
|
|
20
20
|
e.preventDefault();
|
|
21
21
|
fileInputRef.current?.click();
|
|
22
|
-
}, children: jsx(PaperclipIcon, { size: 16 }) })] })) : null, jsx("div", { className: S.editorWrap,
|
|
22
|
+
}, children: jsx(PaperclipIcon, { size: 16 }) })] })) : null, jsx("div", { className: S.editorWrap, children: jsx(EditorContent, { editor: editor, className: S.editorMount }) }), jsx("div", { className: S.submitColumn, children: jsx(Button, { type: "submit", size: "sm", disabled: disabled || !canSubmit, onMouseDown: e => e.preventDefault(), children: jsx(SendHorizontalIcon, { size: 16 }) }) })] }));
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export { ChatPromptComposer };
|
|
@@ -44,7 +44,6 @@ function useChatPromptEditor({ disabled, placeholder, slashCommandItems, onSlash
|
|
|
44
44
|
Placeholder.configure({
|
|
45
45
|
placeholder: placeholderText,
|
|
46
46
|
showOnlyWhenEditable: true,
|
|
47
|
-
showOnlyCurrent: false,
|
|
48
47
|
}),
|
|
49
48
|
];
|
|
50
49
|
if (slashItemsStable.length > 0) {
|
|
@@ -70,6 +69,28 @@ function useChatPromptEditor({ disabled, placeholder, slashCommandItems, onSlash
|
|
|
70
69
|
});
|
|
71
70
|
}, []);
|
|
72
71
|
const trimmedMessage = plainDraft.trim();
|
|
72
|
+
const trimmedMessageRef = useRef(trimmedMessage);
|
|
73
|
+
trimmedMessageRef.current = trimmedMessage;
|
|
74
|
+
const attachmentsCountRef = useRef(attachmentsCount);
|
|
75
|
+
attachmentsCountRef.current = attachmentsCount;
|
|
76
|
+
const onEnterSubmitRef = useRef(onEnterSubmit);
|
|
77
|
+
onEnterSubmitRef.current = onEnterSubmit;
|
|
78
|
+
const handleEditorKeyDown = useCallback((_view, event) => {
|
|
79
|
+
if (!(event.key === 'Enter' &&
|
|
80
|
+
!event.shiftKey &&
|
|
81
|
+
!event.metaKey &&
|
|
82
|
+
!event.ctrlKey)) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
if (slashOpenRef.current)
|
|
86
|
+
return false;
|
|
87
|
+
if (!trimmedMessageRef.current && attachmentsCountRef.current === 0) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
event.preventDefault();
|
|
91
|
+
onEnterSubmitRef.current();
|
|
92
|
+
return true;
|
|
93
|
+
}, []);
|
|
73
94
|
const editor = useEditor({
|
|
74
95
|
extensions,
|
|
75
96
|
content: CHAT_PROMPT_EMPTY_DOC,
|
|
@@ -80,6 +101,7 @@ function useChatPromptEditor({ disabled, placeholder, slashCommandItems, onSlash
|
|
|
80
101
|
spellcheck: 'true',
|
|
81
102
|
'aria-label': ariaLabelComposer,
|
|
82
103
|
},
|
|
104
|
+
handleKeyDown: handleEditorKeyDown,
|
|
83
105
|
},
|
|
84
106
|
onTransaction: ({ editor: ed }) => {
|
|
85
107
|
const dom = chatPromptSafeEditorDom(ed);
|
|
@@ -89,7 +111,7 @@ function useChatPromptEditor({ disabled, placeholder, slashCommandItems, onSlash
|
|
|
89
111
|
setPlainDraft(ed.getText());
|
|
90
112
|
},
|
|
91
113
|
onCreate: bindEditorDom,
|
|
92
|
-
}, [extensions, bindEditorDom, ariaLabelComposer]);
|
|
114
|
+
}, [extensions, bindEditorDom, ariaLabelComposer, handleEditorKeyDown]);
|
|
93
115
|
useEffect(() => {
|
|
94
116
|
if (!editor)
|
|
95
117
|
return;
|
|
@@ -131,8 +153,6 @@ function useChatPromptEditor({ disabled, placeholder, slashCommandItems, onSlash
|
|
|
131
153
|
return;
|
|
132
154
|
syncChatPromptComposerHeight(dom, plainDraft);
|
|
133
155
|
}, [editor, plainDraft]);
|
|
134
|
-
const onEnterSubmitRef = useRef(onEnterSubmit);
|
|
135
|
-
onEnterSubmitRef.current = onEnterSubmit;
|
|
136
156
|
const resetAfterSend = useCallback(() => {
|
|
137
157
|
if (!editor)
|
|
138
158
|
return;
|
|
@@ -144,23 +164,10 @@ function useChatPromptEditor({ disabled, placeholder, slashCommandItems, onSlash
|
|
|
144
164
|
});
|
|
145
165
|
setPlainDraft('');
|
|
146
166
|
}, [editor]);
|
|
147
|
-
const handleComposerKeyDown = useCallback((e) => {
|
|
148
|
-
if (!(e.key === 'Enter' && !e.shiftKey && !e.metaKey && !e.ctrlKey))
|
|
149
|
-
return;
|
|
150
|
-
if (!editorDomRef.current?.contains(e.target))
|
|
151
|
-
return;
|
|
152
|
-
if (slashOpenRef.current)
|
|
153
|
-
return;
|
|
154
|
-
if (!trimmedMessage && attachmentsCount === 0)
|
|
155
|
-
return;
|
|
156
|
-
e.preventDefault();
|
|
157
|
-
onEnterSubmitRef.current();
|
|
158
|
-
}, [attachmentsCount, trimmedMessage]);
|
|
159
167
|
return {
|
|
160
168
|
editor,
|
|
161
169
|
trimmedMessage,
|
|
162
170
|
resetAfterSend,
|
|
163
|
-
handleComposerKeyDown,
|
|
164
171
|
};
|
|
165
172
|
}
|
|
166
173
|
|
|
@@ -8,7 +8,7 @@ import S from './PageHeader.styl.js';
|
|
|
8
8
|
|
|
9
9
|
function PageHeader({ breadcrumbs, breadcrumbClientLogo, breadcrumbCompanyName, breadcrumbSidebarTrigger = true, title, subheader, actions, }) {
|
|
10
10
|
const { isScrolled } = useContext(PageContext);
|
|
11
|
-
return (jsx("div", { className: cn(S.root, actions && S.hasActions, isScrolled && S.scrolled), children: jsxs("div", { className: S.inner, children: [jsx(Breadcrumbs, { className: S.breadcrumbs, items: breadcrumbs ?? [], clientLogo: breadcrumbClientLogo, companyName: breadcrumbCompanyName, sidebarTrigger: breadcrumbSidebarTrigger, children: isScrolled && (jsxs("div", { className: S.titleDupe, children: [jsx(BreadCrumbsSeparator, { size: 14 }), title] })) }), jsxs("div", { className: S.main, children: [jsxs("div", { className: S.title, children: [jsx("h1", { children: title }), subheader && (jsx(TextWithDeferTooltip, { className: S.subheader, children: subheader }))] }), actions && jsx("div", { className: S.actions, children: actions })] })] }) }));
|
|
11
|
+
return (jsx("div", { className: cn(S.root, actions && S.hasActions, isScrolled && S.scrolled), children: jsxs("div", { className: S.inner, children: [jsx(Breadcrumbs, { className: S.breadcrumbs, items: breadcrumbs ?? [], clientLogo: breadcrumbClientLogo, companyName: breadcrumbCompanyName, sidebarTrigger: breadcrumbSidebarTrigger, children: isScrolled && (jsxs("div", { className: S.titleDupe, children: [jsx(BreadCrumbsSeparator, { size: 14 }), title] })) }), jsxs("div", { className: S.main, children: [jsxs("div", { className: S.title, children: [jsx("h1", { children: title }), subheader && (jsx(TextWithDeferTooltip, { className: S.subheader, overTrigger: true, children: subheader }))] }), actions && jsx("div", { className: S.actions, children: actions })] })] }) }));
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export { PageHeader };
|
|
@@ -4,19 +4,41 @@ import { Tooltip, TooltipTrigger, TooltipContent } from '../Tooltip/Tooltip.js';
|
|
|
4
4
|
|
|
5
5
|
function TextWithDeferTooltip({ className, children, width, maxWidth, side = 'bottom', overTrigger = false, ...props }) {
|
|
6
6
|
const [withTooltip, setWithTooltip] = useState(false);
|
|
7
|
+
const [tooltipStyles, setTooltipStyles] = useState({});
|
|
7
8
|
const ref = useRef(null);
|
|
9
|
+
const cachedFontSizeRef = useRef(null);
|
|
8
10
|
const handleMouseEnter = () => {
|
|
9
11
|
if (!ref.current)
|
|
10
12
|
return;
|
|
11
13
|
const isOverflowingHorizontally = ref.current.scrollWidth - ref.current.clientWidth > 3;
|
|
12
14
|
const isOverflowingVertically = ref.current.scrollHeight - ref.current.clientHeight > 3;
|
|
13
15
|
if (isOverflowingHorizontally || isOverflowingVertically) {
|
|
16
|
+
const styles = {
|
|
17
|
+
fontSize: cachedFontSizeRef.current || undefined,
|
|
18
|
+
};
|
|
19
|
+
if (ref.current) {
|
|
20
|
+
const { width: rectWidth, left, top, } = ref.current.getBoundingClientRect();
|
|
21
|
+
styles.width = `${width ?? rectWidth}px`;
|
|
22
|
+
if (!cachedFontSizeRef.current) {
|
|
23
|
+
const { fontSize } = window.getComputedStyle(ref.current);
|
|
24
|
+
cachedFontSizeRef.current = fontSize;
|
|
25
|
+
styles.fontSize = fontSize;
|
|
26
|
+
}
|
|
27
|
+
if (overTrigger) {
|
|
28
|
+
styles.transform = `translate(${left}px, ${top}px) !important`;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
setTooltipStyles(styles);
|
|
14
32
|
setWithTooltip(true);
|
|
15
33
|
}
|
|
16
34
|
};
|
|
17
35
|
const textElement = (jsx("div", { ref: ref, className: className, onMouseEnter: handleMouseEnter, ...props, children: children }));
|
|
18
36
|
if (withTooltip) {
|
|
19
|
-
|
|
37
|
+
const tooltipSide = overTrigger ? 'bottom' : side;
|
|
38
|
+
return (jsxs(Tooltip, { open: withTooltip, onOpenChange: setWithTooltip, children: [jsx(TooltipTrigger, { asChild: true, children: textElement }), jsx(TooltipContent, { side: tooltipSide, style: {
|
|
39
|
+
...(maxWidth !== undefined && { maxWidth: `${maxWidth}px` }),
|
|
40
|
+
...tooltipStyles,
|
|
41
|
+
}, overTrigger: overTrigger, children: children })] }));
|
|
20
42
|
}
|
|
21
43
|
return textElement;
|
|
22
44
|
}
|
|
@@ -83,7 +83,7 @@ function DriverCard({ selectedDriver, isLoading, inQueue = false, driverSelector
|
|
|
83
83
|
const directionText = direction > 0 ? 'Positive' : 'Negative';
|
|
84
84
|
const DirectionIcon = direction > 0 ? TrendUpIcon : TrendDownIcon;
|
|
85
85
|
const nameElem = (jsx("h4", { className: `${S.driverTitle} ${S.truncated}`, children: name }));
|
|
86
|
-
return (jsx(Card, { className: S.root, paddingSize: "l", children: jsx(CardContent, { noScroll: true, children: jsxs("div", { className: S.cardContent, children: [jsx("div", { className: S.driverHeader, children: jsxs("div", { className: S.headerContent, children: [jsxs("div", { className: S.topHeader, children: [jsxs("p", { className: S.categoryInfo, children: [jsx("span", { className: S.categoryIcon, children: getCategoryIcon(category) }), jsx("span", { className: S.categoryText, children: category })] }), driverSelector] }), name.length > 60 ? (jsxs(Tooltip, { children: [jsx(LabelWithId, { id: id, label: jsx(TooltipTrigger, { asChild: true, children: nameElem }) }), jsx(TooltipContent, { side: "left", className: S.tooltipContent, children: jsx("div", { className: S.tooltipTitle, children: name }) })] })) : (jsx(LabelWithId, { id: id, label: nameElem })), jsx("p", { className: S.regionDisplay, children: regionDisplay })] }) }), jsx("div", { className: S.metricsSection, children: jsx("div", { className: S.importanceScore, children: importanceDisplay }) }), jsxs("div", { className: S.directionLagSection, children: [jsxs(Badge, { variant: direction > 0 ? 'green' : 'red', className: S.directionBadge, children: [jsx(DirectionIcon, { className: S.trendIcon }), directionText, " correlation"] }), jsxs("span", { className: S.lagInfo, children: ["Lag: ", lag] })] }), jsx(DriverPerformanceChart, { driver: selectedDriver }), jsx("p", { className: S.description, children: summary ?? '' })] }) }) }));
|
|
86
|
+
return (jsx(Card, { className: S.root, paddingSize: "l", children: jsx(CardContent, { noScroll: true, children: jsxs("div", { className: S.cardContent, children: [jsx("div", { className: S.driverHeader, children: jsxs("div", { className: S.headerContent, children: [jsxs("div", { className: S.topHeader, children: [jsxs("p", { className: S.categoryInfo, children: [jsx("span", { className: S.categoryIcon, children: getCategoryIcon(category) }), jsx("span", { className: S.categoryText, children: category })] }), driverSelector] }), name.length > 60 ? (jsxs(Tooltip, { children: [jsx(LabelWithId, { id: id, label: jsx(TooltipTrigger, { asChild: true, children: nameElem }) }), jsx(TooltipContent, { side: "left", className: S.tooltipContent, overTrigger: true, children: jsx("div", { className: S.tooltipTitle, children: name }) })] })) : (jsx(LabelWithId, { id: id, label: nameElem })), jsx("p", { className: S.regionDisplay, children: regionDisplay })] }) }), jsx("div", { className: S.metricsSection, children: jsx("div", { className: S.importanceScore, children: importanceDisplay }) }), jsxs("div", { className: S.directionLagSection, children: [jsxs(Badge, { variant: direction > 0 ? 'green' : 'red', className: S.directionBadge, children: [jsx(DirectionIcon, { className: S.trendIcon }), directionText, " correlation"] }), jsxs("span", { className: S.lagInfo, children: ["Lag: ", lag] })] }), jsx(DriverPerformanceChart, { driver: selectedDriver }), jsx("p", { className: S.description, children: summary ?? '' })] }) }) }));
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
export { DriverCard };
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { type KeyboardEvent as ReactKeyboardEvent } from 'react';
|
|
2
1
|
import type { Editor } from '@tiptap/core';
|
|
3
2
|
import type { ChatAttachmentDropItem } from '../Chat.types';
|
|
4
3
|
export type ChatPromptComposerProps = {
|
|
@@ -8,6 +7,5 @@ export type ChatPromptComposerProps = {
|
|
|
8
7
|
attachments: ChatAttachmentDropItem[];
|
|
9
8
|
attachmentAccept?: string;
|
|
10
9
|
onAttachmentFiles?: (files: File[]) => void;
|
|
11
|
-
onComposerKeyDown: (event: ReactKeyboardEvent) => void;
|
|
12
10
|
};
|
|
13
|
-
export declare function ChatPromptComposer({ editor, disabled, trimmedMessage, attachments, attachmentAccept, onAttachmentFiles,
|
|
11
|
+
export declare function ChatPromptComposer({ editor, disabled, trimmedMessage, attachments, attachmentAccept, onAttachmentFiles, }: ChatPromptComposerProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { type KeyboardEvent as ReactKeyboardEvent } from 'react';
|
|
2
1
|
import type { SlashCommandItem, SlashOnItemCommand } from '#uilib/tiptap/slash-mention/types';
|
|
3
2
|
import type { Editor } from '@tiptap/core';
|
|
4
3
|
export type UseChatPromptEditorOptions = {
|
|
@@ -16,6 +15,5 @@ export type UseChatPromptEditorResult = {
|
|
|
16
15
|
editor: Editor | null;
|
|
17
16
|
trimmedMessage: string;
|
|
18
17
|
resetAfterSend: () => void;
|
|
19
|
-
handleComposerKeyDown: (event: ReactKeyboardEvent) => void;
|
|
20
18
|
};
|
|
21
19
|
export declare function useChatPromptEditor({ disabled, placeholder, slashCommandItems, onSlashItemCommand, prefillMessage, attachmentsCount, onEnterSubmit, }: UseChatPromptEditorOptions): UseChatPromptEditorResult;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function ChatSheetPage(): import("react/jsx-runtime").JSX.Element;
|
package/package.json
CHANGED
|
@@ -62,7 +62,7 @@ INPUT_MAX_HEIGHT = 200px
|
|
|
62
62
|
overflow-y auto !important
|
|
63
63
|
overflow-x hidden !important
|
|
64
64
|
|
|
65
|
-
& :global(.ProseMirror p.is-empty::before)
|
|
65
|
+
& :global(.ProseMirror p.is-empty.is-editor-empty::before)
|
|
66
66
|
color var(--muted-foreground)
|
|
67
67
|
content attr(data-placeholder)
|
|
68
68
|
float left
|
|
@@ -24,16 +24,15 @@ export function ChatPrompt({
|
|
|
24
24
|
const attachmentsCount = attachments.length;
|
|
25
25
|
|
|
26
26
|
const emitSubmitRef = useRef(() => {});
|
|
27
|
-
const { editor, trimmedMessage, resetAfterSend
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
});
|
|
27
|
+
const { editor, trimmedMessage, resetAfterSend } = useChatPromptEditor({
|
|
28
|
+
disabled,
|
|
29
|
+
placeholder,
|
|
30
|
+
slashCommandItems,
|
|
31
|
+
onSlashItemCommand,
|
|
32
|
+
prefillMessage,
|
|
33
|
+
attachmentsCount,
|
|
34
|
+
onEnterSubmit: () => emitSubmitRef.current(),
|
|
35
|
+
});
|
|
37
36
|
|
|
38
37
|
const emitSubmitAndClear = useCallback(() => {
|
|
39
38
|
if (!editor) return;
|
|
@@ -79,7 +78,6 @@ export function ChatPrompt({
|
|
|
79
78
|
attachments={attachments}
|
|
80
79
|
attachmentAccept={attachmentAccept}
|
|
81
80
|
onAttachmentFiles={onAttachmentFiles}
|
|
82
|
-
onComposerKeyDown={handleComposerKeyDown}
|
|
83
81
|
/>
|
|
84
82
|
{footer}
|
|
85
83
|
</form>
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type ChangeEvent,
|
|
3
|
-
type KeyboardEvent as ReactKeyboardEvent,
|
|
4
|
-
useCallback,
|
|
5
|
-
useRef,
|
|
6
|
-
} from 'react';
|
|
1
|
+
import { type ChangeEvent, useCallback, useRef } from 'react';
|
|
7
2
|
|
|
8
3
|
import type { Editor } from '@tiptap/core';
|
|
9
4
|
import { EditorContent } from '@tiptap/react';
|
|
@@ -20,7 +15,6 @@ export type ChatPromptComposerProps = {
|
|
|
20
15
|
attachments: ChatAttachmentDropItem[];
|
|
21
16
|
attachmentAccept?: string;
|
|
22
17
|
onAttachmentFiles?: (files: File[]) => void;
|
|
23
|
-
onComposerKeyDown: (event: ReactKeyboardEvent) => void;
|
|
24
18
|
};
|
|
25
19
|
|
|
26
20
|
export function ChatPromptComposer({
|
|
@@ -30,7 +24,6 @@ export function ChatPromptComposer({
|
|
|
30
24
|
attachments,
|
|
31
25
|
attachmentAccept,
|
|
32
26
|
onAttachmentFiles,
|
|
33
|
-
onComposerKeyDown,
|
|
34
27
|
}: ChatPromptComposerProps) {
|
|
35
28
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
36
29
|
const showAttachButton = Boolean(attachmentAccept && onAttachmentFiles);
|
|
@@ -79,7 +72,7 @@ export function ChatPromptComposer({
|
|
|
79
72
|
</>
|
|
80
73
|
) : null}
|
|
81
74
|
|
|
82
|
-
<div className={S.editorWrap}
|
|
75
|
+
<div className={S.editorWrap}>
|
|
83
76
|
<EditorContent editor={editor} className={S.editorMount} />
|
|
84
77
|
</div>
|
|
85
78
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import {
|
|
2
|
-
type KeyboardEvent as ReactKeyboardEvent,
|
|
3
2
|
useCallback,
|
|
4
3
|
useEffect,
|
|
5
4
|
useLayoutEffect,
|
|
@@ -43,7 +42,6 @@ export type UseChatPromptEditorResult = {
|
|
|
43
42
|
editor: Editor | null;
|
|
44
43
|
trimmedMessage: string;
|
|
45
44
|
resetAfterSend: () => void;
|
|
46
|
-
handleComposerKeyDown: (event: ReactKeyboardEvent) => void;
|
|
47
45
|
};
|
|
48
46
|
|
|
49
47
|
export function useChatPromptEditor({
|
|
@@ -94,7 +92,6 @@ export function useChatPromptEditor({
|
|
|
94
92
|
Placeholder.configure({
|
|
95
93
|
placeholder: placeholderText,
|
|
96
94
|
showOnlyWhenEditable: true,
|
|
97
|
-
showOnlyCurrent: false,
|
|
98
95
|
}),
|
|
99
96
|
];
|
|
100
97
|
if (slashItemsStable.length > 0) {
|
|
@@ -126,6 +123,38 @@ export function useChatPromptEditor({
|
|
|
126
123
|
|
|
127
124
|
const trimmedMessage = plainDraft.trim();
|
|
128
125
|
|
|
126
|
+
const trimmedMessageRef = useRef(trimmedMessage);
|
|
127
|
+
trimmedMessageRef.current = trimmedMessage;
|
|
128
|
+
|
|
129
|
+
const attachmentsCountRef = useRef(attachmentsCount);
|
|
130
|
+
attachmentsCountRef.current = attachmentsCount;
|
|
131
|
+
|
|
132
|
+
const onEnterSubmitRef = useRef(onEnterSubmit);
|
|
133
|
+
onEnterSubmitRef.current = onEnterSubmit;
|
|
134
|
+
|
|
135
|
+
const handleEditorKeyDown = useCallback(
|
|
136
|
+
(_view: unknown, event: KeyboardEvent) => {
|
|
137
|
+
if (
|
|
138
|
+
!(
|
|
139
|
+
event.key === 'Enter' &&
|
|
140
|
+
!event.shiftKey &&
|
|
141
|
+
!event.metaKey &&
|
|
142
|
+
!event.ctrlKey
|
|
143
|
+
)
|
|
144
|
+
) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
if (slashOpenRef.current) return false;
|
|
148
|
+
if (!trimmedMessageRef.current && attachmentsCountRef.current === 0) {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
event.preventDefault();
|
|
152
|
+
onEnterSubmitRef.current();
|
|
153
|
+
return true;
|
|
154
|
+
},
|
|
155
|
+
[],
|
|
156
|
+
);
|
|
157
|
+
|
|
129
158
|
const editor = useEditor(
|
|
130
159
|
{
|
|
131
160
|
extensions,
|
|
@@ -137,6 +166,7 @@ export function useChatPromptEditor({
|
|
|
137
166
|
spellcheck: 'true',
|
|
138
167
|
'aria-label': ariaLabelComposer,
|
|
139
168
|
},
|
|
169
|
+
handleKeyDown: handleEditorKeyDown,
|
|
140
170
|
},
|
|
141
171
|
onTransaction: ({ editor: ed }) => {
|
|
142
172
|
const dom = chatPromptSafeEditorDom(ed);
|
|
@@ -147,7 +177,7 @@ export function useChatPromptEditor({
|
|
|
147
177
|
},
|
|
148
178
|
onCreate: bindEditorDom,
|
|
149
179
|
},
|
|
150
|
-
[extensions, bindEditorDom, ariaLabelComposer],
|
|
180
|
+
[extensions, bindEditorDom, ariaLabelComposer, handleEditorKeyDown],
|
|
151
181
|
);
|
|
152
182
|
|
|
153
183
|
useEffect(() => {
|
|
@@ -188,9 +218,6 @@ export function useChatPromptEditor({
|
|
|
188
218
|
syncChatPromptComposerHeight(dom, plainDraft);
|
|
189
219
|
}, [editor, plainDraft]);
|
|
190
220
|
|
|
191
|
-
const onEnterSubmitRef = useRef(onEnterSubmit);
|
|
192
|
-
onEnterSubmitRef.current = onEnterSubmit;
|
|
193
|
-
|
|
194
221
|
const resetAfterSend = useCallback(() => {
|
|
195
222
|
if (!editor) return;
|
|
196
223
|
editor.chain().clearContent().focus().run();
|
|
@@ -201,23 +228,9 @@ export function useChatPromptEditor({
|
|
|
201
228
|
setPlainDraft('');
|
|
202
229
|
}, [editor]);
|
|
203
230
|
|
|
204
|
-
const handleComposerKeyDown = useCallback(
|
|
205
|
-
(e: ReactKeyboardEvent) => {
|
|
206
|
-
if (!(e.key === 'Enter' && !e.shiftKey && !e.metaKey && !e.ctrlKey))
|
|
207
|
-
return;
|
|
208
|
-
if (!editorDomRef.current?.contains(e.target as Node)) return;
|
|
209
|
-
if (slashOpenRef.current) return;
|
|
210
|
-
if (!trimmedMessage && attachmentsCount === 0) return;
|
|
211
|
-
e.preventDefault();
|
|
212
|
-
onEnterSubmitRef.current();
|
|
213
|
-
},
|
|
214
|
-
[attachmentsCount, trimmedMessage],
|
|
215
|
-
);
|
|
216
|
-
|
|
217
231
|
return {
|
|
218
232
|
editor,
|
|
219
233
|
trimmedMessage,
|
|
220
234
|
resetAfterSend,
|
|
221
|
-
handleComposerKeyDown,
|
|
222
235
|
};
|
|
223
236
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useRef, useState } from 'react';
|
|
1
|
+
import { CSSProperties, useRef, useState } from 'react';
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
Tooltip,
|
|
@@ -18,7 +18,9 @@ function TextWithDeferTooltip({
|
|
|
18
18
|
...props
|
|
19
19
|
}: TextWithDeferTooltipProps) {
|
|
20
20
|
const [withTooltip, setWithTooltip] = useState(false);
|
|
21
|
+
const [tooltipStyles, setTooltipStyles] = useState<CSSProperties>({});
|
|
21
22
|
const ref = useRef<HTMLDivElement>(null);
|
|
23
|
+
const cachedFontSizeRef = useRef<string | null>(null);
|
|
22
24
|
|
|
23
25
|
const handleMouseEnter = () => {
|
|
24
26
|
if (!ref.current) return;
|
|
@@ -29,6 +31,31 @@ function TextWithDeferTooltip({
|
|
|
29
31
|
ref.current.scrollHeight - ref.current.clientHeight > 3;
|
|
30
32
|
|
|
31
33
|
if (isOverflowingHorizontally || isOverflowingVertically) {
|
|
34
|
+
const styles: CSSProperties = {
|
|
35
|
+
fontSize: cachedFontSizeRef.current || undefined,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
if (ref.current) {
|
|
39
|
+
const {
|
|
40
|
+
width: rectWidth,
|
|
41
|
+
left,
|
|
42
|
+
top,
|
|
43
|
+
} = ref.current.getBoundingClientRect();
|
|
44
|
+
|
|
45
|
+
styles.width = `${width ?? rectWidth}px`;
|
|
46
|
+
|
|
47
|
+
if (!cachedFontSizeRef.current) {
|
|
48
|
+
const { fontSize } = window.getComputedStyle(ref.current);
|
|
49
|
+
cachedFontSizeRef.current = fontSize;
|
|
50
|
+
styles.fontSize = fontSize;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (overTrigger) {
|
|
54
|
+
styles.transform = `translate(${left}px, ${top}px) !important`;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
setTooltipStyles(styles);
|
|
32
59
|
setWithTooltip(true);
|
|
33
60
|
}
|
|
34
61
|
};
|
|
@@ -45,14 +72,18 @@ function TextWithDeferTooltip({
|
|
|
45
72
|
);
|
|
46
73
|
|
|
47
74
|
if (withTooltip) {
|
|
75
|
+
const tooltipSide = overTrigger ? 'bottom' : side;
|
|
76
|
+
|
|
48
77
|
return (
|
|
49
78
|
<Tooltip open={withTooltip} onOpenChange={setWithTooltip}>
|
|
50
79
|
<TooltipTrigger asChild>{textElement}</TooltipTrigger>
|
|
51
80
|
<TooltipContent
|
|
52
|
-
side={
|
|
81
|
+
side={tooltipSide}
|
|
82
|
+
style={{
|
|
83
|
+
...(maxWidth !== undefined && { maxWidth: `${maxWidth}px` }),
|
|
84
|
+
...tooltipStyles,
|
|
85
|
+
}}
|
|
53
86
|
overTrigger={overTrigger}
|
|
54
|
-
maxWidth={maxWidth}
|
|
55
|
-
style={width !== undefined ? { width: `${width}px` } : undefined}
|
|
56
87
|
>
|
|
57
88
|
{children}
|
|
58
89
|
</TooltipContent>
|
|
@@ -177,7 +177,11 @@ export function DriverCard({
|
|
|
177
177
|
id={id}
|
|
178
178
|
label={<TooltipTrigger asChild>{nameElem}</TooltipTrigger>}
|
|
179
179
|
/>
|
|
180
|
-
<TooltipContent
|
|
180
|
+
<TooltipContent
|
|
181
|
+
side="left"
|
|
182
|
+
className={S.tooltipContent}
|
|
183
|
+
overTrigger
|
|
184
|
+
>
|
|
181
185
|
<div className={S.tooltipTitle}>{name}</div>
|
|
182
186
|
</TooltipContent>
|
|
183
187
|
</Tooltip>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { SlashCommandItem } from '#uilib/components/ui/Chat';
|
|
2
|
+
import { PageContentSection } from '#uilib/components/ui/Page';
|
|
3
|
+
import { MessageSquare } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
import { AppPageHeader } from '../components/AppPageHeader/AppPageHeader';
|
|
6
|
+
import { DOCS_CHAT_USER_KEY } from '../docsConstants';
|
|
7
|
+
import { DocsHeaderActions } from '../docsHeaderActions';
|
|
8
|
+
|
|
9
|
+
const DOCS_CHAT_SHEET_SCOPE_ID = `${DOCS_CHAT_USER_KEY}-docs-chat-sheet-portal`;
|
|
10
|
+
|
|
11
|
+
const DOCS_SAMPLE_SLASH_ITEMS: SlashCommandItem[] = [
|
|
12
|
+
{
|
|
13
|
+
id: 'sample-command',
|
|
14
|
+
label: 'sample-command',
|
|
15
|
+
description: 'Demo slash item for portal ChatSheet regression.',
|
|
16
|
+
},
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
export default function ChatSheetPage() {
|
|
20
|
+
return (
|
|
21
|
+
<>
|
|
22
|
+
<AppPageHeader
|
|
23
|
+
breadcrumbs={[{ label: 'Chat' }, { label: 'Chat sheet' }]}
|
|
24
|
+
title="Chat sheet (portal)"
|
|
25
|
+
subheader="Same integration as design-demo: ChatSheet in page header actions, chat panel portaled into the shell sidebar slot. No inline ChatChrome on this page."
|
|
26
|
+
actions={
|
|
27
|
+
<DocsHeaderActions
|
|
28
|
+
scopeId={DOCS_CHAT_SHEET_SCOPE_ID}
|
|
29
|
+
slashCommandItems={DOCS_SAMPLE_SLASH_ITEMS}
|
|
30
|
+
triggerLabel={
|
|
31
|
+
<>
|
|
32
|
+
<MessageSquare size={20} />
|
|
33
|
+
AI Assistant
|
|
34
|
+
</>
|
|
35
|
+
}
|
|
36
|
+
/>
|
|
37
|
+
}
|
|
38
|
+
/>
|
|
39
|
+
<PageContentSection>
|
|
40
|
+
<p style={{ marginBottom: 16, fontSize: 14, lineHeight: 1.5 }}>
|
|
41
|
+
Open <strong>AI Assistant</strong> in the header. The composer runs
|
|
42
|
+
inside the portaled chat panel (not inline on this page).
|
|
43
|
+
</p>
|
|
44
|
+
<h3 style={{ marginBottom: 8, fontSize: 14, fontWeight: 600 }}>
|
|
45
|
+
Regression checklist
|
|
46
|
+
</h3>
|
|
47
|
+
<ul
|
|
48
|
+
style={{ margin: 0, paddingLeft: 20, fontSize: 14, lineHeight: 1.6 }}
|
|
49
|
+
>
|
|
50
|
+
<li>Plain Enter with text → submit and clear composer</li>
|
|
51
|
+
<li>
|
|
52
|
+
Plain Enter must not create empty lines with duplicated placeholders
|
|
53
|
+
</li>
|
|
54
|
+
<li>Shift+Enter with text → multiline, no placeholder after text</li>
|
|
55
|
+
<li>Empty Enter → no submit</li>
|
|
56
|
+
<li>Submit button and slash menu still work</li>
|
|
57
|
+
</ul>
|
|
58
|
+
</PageContentSection>
|
|
59
|
+
</>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -11,9 +11,6 @@ import { DocsHeaderActions } from '../docsHeaderActions';
|
|
|
11
11
|
|
|
12
12
|
const TOOLTIP_SIDES = ['left', 'top', 'bottom', 'right'] as const;
|
|
13
13
|
|
|
14
|
-
const OVER_TRIGGER_TEXT =
|
|
15
|
-
'Actual Order volume for Baerlocher MB 301, a compound that includes stearin as a component. MB 301 is typically used in construction-grade applications.';
|
|
16
|
-
|
|
17
14
|
export default function TooltipPage() {
|
|
18
15
|
return (
|
|
19
16
|
<>
|
|
@@ -37,34 +34,6 @@ export default function TooltipPage() {
|
|
|
37
34
|
))}
|
|
38
35
|
</div>
|
|
39
36
|
</PageContentSection>
|
|
40
|
-
<PageContentSection>
|
|
41
|
-
<p style={{ margin: '0 0 8px', fontWeight: 600 }}>Over trigger</p>
|
|
42
|
-
<div
|
|
43
|
-
style={{
|
|
44
|
-
maxWidth: 500,
|
|
45
|
-
border: '1px dashed var(--border)',
|
|
46
|
-
padding: 8,
|
|
47
|
-
}}
|
|
48
|
-
>
|
|
49
|
-
<Tooltip>
|
|
50
|
-
<TooltipTrigger asChild>
|
|
51
|
-
<div
|
|
52
|
-
style={{
|
|
53
|
-
overflow: 'hidden',
|
|
54
|
-
textOverflow: 'ellipsis',
|
|
55
|
-
whiteSpace: 'nowrap',
|
|
56
|
-
fontWeight: 600,
|
|
57
|
-
}}
|
|
58
|
-
>
|
|
59
|
-
{OVER_TRIGGER_TEXT}
|
|
60
|
-
</div>
|
|
61
|
-
</TooltipTrigger>
|
|
62
|
-
<TooltipContent overTrigger maxWidth={400}>
|
|
63
|
-
{OVER_TRIGGER_TEXT}
|
|
64
|
-
</TooltipContent>
|
|
65
|
-
</Tooltip>
|
|
66
|
-
</div>
|
|
67
|
-
</PageContentSection>
|
|
68
37
|
</>
|
|
69
38
|
);
|
|
70
39
|
}
|
package/src/docs/registry.ts
CHANGED
|
@@ -115,6 +115,12 @@ export const DOC_REGISTRY: DocEntry[] = [
|
|
|
115
115
|
section: 'Chat',
|
|
116
116
|
load: () => import('./pages/ChatSlashCommandsPage'),
|
|
117
117
|
},
|
|
118
|
+
{
|
|
119
|
+
slug: 'chat-sheet',
|
|
120
|
+
title: 'Chat sheet (portal)',
|
|
121
|
+
section: 'Chat',
|
|
122
|
+
load: () => import('./pages/ChatSheetPage'),
|
|
123
|
+
},
|
|
118
124
|
{
|
|
119
125
|
slug: 'chat-user-csv-attachment',
|
|
120
126
|
title: 'Chat user CSV attachment',
|