@promptbook/components 0.101.0-17 → 0.101.0-18
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/esm/index.es.js +220 -68
- package/esm/index.es.js.map +1 -1
- package/esm/typings/src/_packages/components.index.d.ts +6 -0
- package/esm/typings/src/_packages/types.index.d.ts +4 -0
- package/esm/typings/src/book-components/Chat/LlmChat/LlmChatProps.d.ts +13 -0
- package/esm/typings/src/book-components/Chat/hooks/index.d.ts +2 -0
- package/esm/typings/src/book-components/Chat/hooks/useChatAutoScroll.d.ts +41 -0
- package/esm/typings/src/book-components/Chat/hooks/useSendMessageToLlmChat.d.ts +44 -0
- package/esm/typings/src/version.d.ts +1 -1
- package/package.json +1 -1
- package/umd/index.umd.js +221 -67
- package/umd/index.umd.js.map +1 -1
|
@@ -15,6 +15,9 @@ import { BookEditor } from '../book-components/BookEditor/BookEditor';
|
|
|
15
15
|
import { DEFAULT_BOOK_FONT_CLASS } from '../book-components/BookEditor/config';
|
|
16
16
|
import { Chat } from '../book-components/Chat/Chat/Chat';
|
|
17
17
|
import type { ChatProps } from '../book-components/Chat/Chat/ChatProps';
|
|
18
|
+
import { useChatAutoScroll } from '../book-components/Chat/hooks/useChatAutoScroll';
|
|
19
|
+
import type { SendMessageToLlmChatFunction } from '../book-components/Chat/hooks/useSendMessageToLlmChat';
|
|
20
|
+
import { useSendMessageToLlmChat } from '../book-components/Chat/hooks/useSendMessageToLlmChat';
|
|
18
21
|
import { LlmChat } from '../book-components/Chat/LlmChat/LlmChat';
|
|
19
22
|
import type { LlmChatProps } from '../book-components/Chat/LlmChat/LlmChatProps';
|
|
20
23
|
import type { ChatMessage } from '../book-components/Chat/types/ChatMessage';
|
|
@@ -44,6 +47,9 @@ export { BookEditor };
|
|
|
44
47
|
export { DEFAULT_BOOK_FONT_CLASS };
|
|
45
48
|
export { Chat };
|
|
46
49
|
export type { ChatProps };
|
|
50
|
+
export { useChatAutoScroll };
|
|
51
|
+
export type { SendMessageToLlmChatFunction };
|
|
52
|
+
export { useSendMessageToLlmChat };
|
|
47
53
|
export { LlmChat };
|
|
48
54
|
export type { LlmChatProps };
|
|
49
55
|
export type { ChatMessage };
|
|
@@ -13,6 +13,8 @@ import type { MockedChatDelayConfig } from '../book-components/AvatarProfile/Ava
|
|
|
13
13
|
import type { MockedChatProps } from '../book-components/AvatarProfile/AvatarProfile/MockedChat';
|
|
14
14
|
import type { BookEditorProps } from '../book-components/BookEditor/BookEditor';
|
|
15
15
|
import type { ChatProps } from '../book-components/Chat/Chat/ChatProps';
|
|
16
|
+
import type { ChatAutoScrollConfig } from '../book-components/Chat/hooks/useChatAutoScroll';
|
|
17
|
+
import type { SendMessageToLlmChatFunction } from '../book-components/Chat/hooks/useSendMessageToLlmChat';
|
|
16
18
|
import type { LlmChatProps } from '../book-components/Chat/LlmChat/LlmChatProps';
|
|
17
19
|
import type { ChatMessage } from '../book-components/Chat/types/ChatMessage';
|
|
18
20
|
import type { ChatParticipant } from '../book-components/Chat/types/ChatParticipant';
|
|
@@ -340,6 +342,8 @@ export type { MockedChatDelayConfig };
|
|
|
340
342
|
export type { MockedChatProps };
|
|
341
343
|
export type { BookEditorProps };
|
|
342
344
|
export type { ChatProps };
|
|
345
|
+
export type { ChatAutoScrollConfig };
|
|
346
|
+
export type { SendMessageToLlmChatFunction };
|
|
343
347
|
export type { LlmChatProps };
|
|
344
348
|
export type { ChatMessage };
|
|
345
349
|
export type { ChatParticipant };
|
|
@@ -2,6 +2,7 @@ import type { LlmExecutionTools } from '../../../execution/LlmExecutionTools';
|
|
|
2
2
|
import type { ChatProps } from '../Chat/ChatProps';
|
|
3
3
|
import type { ChatMessage } from '../types/ChatMessage';
|
|
4
4
|
import type { ChatParticipant } from '../types/ChatParticipant';
|
|
5
|
+
import type { SendMessageToLlmChatFunction } from '../hooks/useSendMessageToLlmChat';
|
|
5
6
|
/**
|
|
6
7
|
* Props for LlmChat component, derived from ChatProps but with LLM-specific modifications
|
|
7
8
|
*
|
|
@@ -17,8 +18,20 @@ export type LlmChatProps = Omit<ChatProps, 'messages' | 'onMessage' | 'onChange'
|
|
|
17
18
|
* When provided, the conversation will be saved and restored from localStorage
|
|
18
19
|
*/
|
|
19
20
|
readonly persistenceKey?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Optional initial messages to pre-populate the chat.
|
|
23
|
+
* - They can include both USER and ASSISTANT messages.
|
|
24
|
+
* - They are only used when there is no persisted conversation (persistence takes precedence).
|
|
25
|
+
* - They are not automatically persisted until the user sends a new message.
|
|
26
|
+
*/
|
|
27
|
+
readonly initialMessages?: ReadonlyArray<ChatMessage>;
|
|
20
28
|
/**
|
|
21
29
|
* Called when the chat state changes (messages, participants, etc.)
|
|
22
30
|
*/
|
|
23
31
|
onChange?(messages: ReadonlyArray<ChatMessage>, participants: ReadonlyArray<ChatParticipant>): void;
|
|
32
|
+
/**
|
|
33
|
+
* Optional external sendMessage function produced by useSendMessageToLlmChat hook.
|
|
34
|
+
* When provided, LlmChat will attach its internal handler to it (no React context needed).
|
|
35
|
+
*/
|
|
36
|
+
readonly sendMessage?: SendMessageToLlmChatFunction;
|
|
24
37
|
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for the auto-scroll behavior
|
|
3
|
+
*/
|
|
4
|
+
export type ChatAutoScrollConfig = {
|
|
5
|
+
/**
|
|
6
|
+
* Threshold in pixels from bottom to consider as "at bottom"
|
|
7
|
+
* @default 100
|
|
8
|
+
*/
|
|
9
|
+
bottomThreshold?: number;
|
|
10
|
+
/**
|
|
11
|
+
* Whether to use smooth scrolling
|
|
12
|
+
* @default true
|
|
13
|
+
*/
|
|
14
|
+
smoothScroll?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Delay before checking scroll position after new messages (in milliseconds)
|
|
17
|
+
* @default 100
|
|
18
|
+
*/
|
|
19
|
+
scrollCheckDelay?: number;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Hook for managing auto-scroll behavior in chat components
|
|
23
|
+
*
|
|
24
|
+
* This hook provides:
|
|
25
|
+
* - Automatic scrolling to bottom when new messages arrive (if user is already at bottom)
|
|
26
|
+
* - Detection of when user scrolls away from bottom
|
|
27
|
+
* - Scroll-to-bottom functionality with smooth animation
|
|
28
|
+
* - Mobile-optimized scrolling behavior
|
|
29
|
+
*
|
|
30
|
+
* @public exported from `@promptbook/components`
|
|
31
|
+
*/
|
|
32
|
+
export declare function useChatAutoScroll(config?: ChatAutoScrollConfig): {
|
|
33
|
+
isAutoScrolling: boolean;
|
|
34
|
+
chatMessagesRef: (element: HTMLDivElement | null) => void;
|
|
35
|
+
handleScroll: (event: React.UIEvent<HTMLDivElement>) => void;
|
|
36
|
+
handleMessagesChange: () => void;
|
|
37
|
+
scrollToBottom: () => void;
|
|
38
|
+
enableAutoScroll: () => void;
|
|
39
|
+
disableAutoScroll: () => void;
|
|
40
|
+
isMobile: boolean;
|
|
41
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Function type for sending a message to LlmChat.
|
|
3
|
+
*
|
|
4
|
+
* Implementation detail: The returned function is "attachable".
|
|
5
|
+
* LlmChat will call the internal `_attach` method (if present) to bind
|
|
6
|
+
* its real message handler. Messages sent before attachment are queued
|
|
7
|
+
* and flushed after attachment.
|
|
8
|
+
*
|
|
9
|
+
* @public exported from `@promptbook/components`
|
|
10
|
+
*/
|
|
11
|
+
export type SendMessageToLlmChatFunction = {
|
|
12
|
+
/**
|
|
13
|
+
* Send a message to the bound LlmChat instance (or queue it until attached).
|
|
14
|
+
*/
|
|
15
|
+
(message: string): void;
|
|
16
|
+
/**
|
|
17
|
+
* Internal method used by the <LlmChat/> component to attach its handler.
|
|
18
|
+
* Not intended for consumer usage.
|
|
19
|
+
*
|
|
20
|
+
* @internal
|
|
21
|
+
*/
|
|
22
|
+
_attach?: (handler: (message: string) => Promise<void> | void) => void;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Hook to create a sendMessage function for an <LlmChat/> component WITHOUT needing any React Context.
|
|
26
|
+
*
|
|
27
|
+
* Usage pattern:
|
|
28
|
+
* ```tsx
|
|
29
|
+
* const sendMessage = useSendMessageToLlmChat();
|
|
30
|
+
* return (
|
|
31
|
+
* <>
|
|
32
|
+
* <button onClick={() => sendMessage('Hello!')}>Hello</button>
|
|
33
|
+
* <LlmChat llmTools={llmTools} sendMessage={sendMessage} />
|
|
34
|
+
* </>
|
|
35
|
+
* );
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* - No provider wrapping needed.
|
|
39
|
+
* - Safe to call before the <LlmChat/> mounts (messages will be queued).
|
|
40
|
+
* - Keeps DRY by letting <LlmChat/> reuse its internal `handleMessage` logic.
|
|
41
|
+
*
|
|
42
|
+
* @public exported from `@promptbook/components`
|
|
43
|
+
*/
|
|
44
|
+
export declare function useSendMessageToLlmChat(): SendMessageToLlmChatFunction;
|
|
@@ -15,7 +15,7 @@ export declare const BOOK_LANGUAGE_VERSION: string_semantic_version;
|
|
|
15
15
|
export declare const PROMPTBOOK_ENGINE_VERSION: string_promptbook_version;
|
|
16
16
|
/**
|
|
17
17
|
* Represents the version string of the Promptbook engine.
|
|
18
|
-
* It follows semantic versioning (e.g., `0.101.0-
|
|
18
|
+
* It follows semantic versioning (e.g., `0.101.0-17`).
|
|
19
19
|
*
|
|
20
20
|
* @generated
|
|
21
21
|
*/
|
package/package.json
CHANGED
package/umd/index.umd.js
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* @generated
|
|
23
23
|
* @see https://github.com/webgptorg/promptbook
|
|
24
24
|
*/
|
|
25
|
-
const PROMPTBOOK_ENGINE_VERSION = '0.101.0-
|
|
25
|
+
const PROMPTBOOK_ENGINE_VERSION = '0.101.0-18';
|
|
26
26
|
/**
|
|
27
27
|
* TODO: string_promptbook_version should be constrained to the all versions of Promptbook engine
|
|
28
28
|
* Note: [💞] Ignore a discrepancy between file name and entity name
|
|
@@ -4131,6 +4131,147 @@
|
|
|
4131
4131
|
*/
|
|
4132
4132
|
const TemplateIcon = ({ size }) => (jsxRuntime.jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "currentColor", children: jsxRuntime.jsx("path", { d: "M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z" }) }));
|
|
4133
4133
|
|
|
4134
|
+
/**
|
|
4135
|
+
* Hook for managing auto-scroll behavior in chat components
|
|
4136
|
+
*
|
|
4137
|
+
* This hook provides:
|
|
4138
|
+
* - Automatic scrolling to bottom when new messages arrive (if user is already at bottom)
|
|
4139
|
+
* - Detection of when user scrolls away from bottom
|
|
4140
|
+
* - Scroll-to-bottom functionality with smooth animation
|
|
4141
|
+
* - Mobile-optimized scrolling behavior
|
|
4142
|
+
*
|
|
4143
|
+
* @public exported from `@promptbook/components`
|
|
4144
|
+
*/
|
|
4145
|
+
function useChatAutoScroll(config = {}) {
|
|
4146
|
+
const { bottomThreshold = 100, smoothScroll = true, scrollCheckDelay = 100, } = config;
|
|
4147
|
+
const [isAutoScrolling, setIsAutoScrolling] = react.useState(true);
|
|
4148
|
+
const [isMobile, setIsMobile] = react.useState(false);
|
|
4149
|
+
const chatMessagesRef = react.useRef(null);
|
|
4150
|
+
const scrollTimeoutRef = react.useRef(null);
|
|
4151
|
+
const lastScrollHeightRef = react.useRef(0);
|
|
4152
|
+
// Detect mobile device
|
|
4153
|
+
react.useEffect(() => {
|
|
4154
|
+
const checkMobile = () => {
|
|
4155
|
+
const isMobileDevice = window.innerWidth <= 768 ||
|
|
4156
|
+
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
|
4157
|
+
setIsMobile(isMobileDevice);
|
|
4158
|
+
};
|
|
4159
|
+
checkMobile();
|
|
4160
|
+
window.addEventListener('resize', checkMobile);
|
|
4161
|
+
return () => window.removeEventListener('resize', checkMobile);
|
|
4162
|
+
}, []);
|
|
4163
|
+
// Check if user is at the bottom of the chat
|
|
4164
|
+
const checkIfAtBottom = react.useCallback((element) => {
|
|
4165
|
+
const { scrollTop, scrollHeight, clientHeight } = element;
|
|
4166
|
+
return scrollTop + clientHeight >= scrollHeight - bottomThreshold;
|
|
4167
|
+
}, [bottomThreshold]);
|
|
4168
|
+
// Scroll to bottom function
|
|
4169
|
+
const scrollToBottom = react.useCallback((behavior = 'smooth') => {
|
|
4170
|
+
const chatMessagesElement = chatMessagesRef.current;
|
|
4171
|
+
if (!chatMessagesElement)
|
|
4172
|
+
return;
|
|
4173
|
+
if (isMobile) {
|
|
4174
|
+
// Mobile-optimized scrolling
|
|
4175
|
+
chatMessagesElement.scrollTo({
|
|
4176
|
+
top: chatMessagesElement.scrollHeight,
|
|
4177
|
+
behavior: smoothScroll ? behavior : 'auto',
|
|
4178
|
+
});
|
|
4179
|
+
}
|
|
4180
|
+
else {
|
|
4181
|
+
// Desktop scrolling
|
|
4182
|
+
if (smoothScroll && behavior === 'smooth') {
|
|
4183
|
+
chatMessagesElement.style.scrollBehavior = 'smooth';
|
|
4184
|
+
chatMessagesElement.scrollTop = chatMessagesElement.scrollHeight;
|
|
4185
|
+
chatMessagesElement.style.scrollBehavior = 'auto';
|
|
4186
|
+
}
|
|
4187
|
+
else {
|
|
4188
|
+
chatMessagesElement.scrollTop = chatMessagesElement.scrollHeight;
|
|
4189
|
+
}
|
|
4190
|
+
}
|
|
4191
|
+
}, [isMobile, smoothScroll]);
|
|
4192
|
+
// Handle scroll events
|
|
4193
|
+
const handleScroll = react.useCallback((event) => {
|
|
4194
|
+
const element = event.target;
|
|
4195
|
+
if (!element)
|
|
4196
|
+
return;
|
|
4197
|
+
// Clear any pending scroll timeout
|
|
4198
|
+
if (scrollTimeoutRef.current) {
|
|
4199
|
+
clearTimeout(scrollTimeoutRef.current);
|
|
4200
|
+
}
|
|
4201
|
+
// Debounce scroll position check to avoid too frequent updates
|
|
4202
|
+
scrollTimeoutRef.current = setTimeout(() => {
|
|
4203
|
+
const isAtBottom = checkIfAtBottom(element);
|
|
4204
|
+
setIsAutoScrolling(isAtBottom);
|
|
4205
|
+
}, 50);
|
|
4206
|
+
}, [checkIfAtBottom]);
|
|
4207
|
+
// Auto-scroll when messages change (if user is at bottom)
|
|
4208
|
+
const handleMessagesChange = react.useCallback(() => {
|
|
4209
|
+
const chatMessagesElement = chatMessagesRef.current;
|
|
4210
|
+
if (!chatMessagesElement)
|
|
4211
|
+
return;
|
|
4212
|
+
// Check if this is a new message (scroll height increased)
|
|
4213
|
+
const currentScrollHeight = chatMessagesElement.scrollHeight;
|
|
4214
|
+
const hasNewContent = currentScrollHeight > lastScrollHeightRef.current;
|
|
4215
|
+
lastScrollHeightRef.current = currentScrollHeight;
|
|
4216
|
+
if (!hasNewContent)
|
|
4217
|
+
return;
|
|
4218
|
+
// If user is set to auto-scroll, scroll to bottom
|
|
4219
|
+
if (isAutoScrolling) {
|
|
4220
|
+
// Delay scroll slightly to ensure DOM has updated
|
|
4221
|
+
setTimeout(() => {
|
|
4222
|
+
scrollToBottom('smooth');
|
|
4223
|
+
}, scrollCheckDelay);
|
|
4224
|
+
}
|
|
4225
|
+
}, [isAutoScrolling, scrollToBottom, scrollCheckDelay]);
|
|
4226
|
+
// Ref callback for chat messages container
|
|
4227
|
+
const chatMessagesRefCallback = react.useCallback((element) => {
|
|
4228
|
+
chatMessagesRef.current = element;
|
|
4229
|
+
if (element) {
|
|
4230
|
+
// Update last scroll height
|
|
4231
|
+
lastScrollHeightRef.current = element.scrollHeight;
|
|
4232
|
+
// If auto-scrolling is enabled, scroll to bottom
|
|
4233
|
+
if (isAutoScrolling) {
|
|
4234
|
+
// Use requestAnimationFrame for smoother initial scroll
|
|
4235
|
+
requestAnimationFrame(() => {
|
|
4236
|
+
scrollToBottom('auto');
|
|
4237
|
+
});
|
|
4238
|
+
}
|
|
4239
|
+
}
|
|
4240
|
+
}, [isAutoScrolling, scrollToBottom]);
|
|
4241
|
+
// Manual scroll to bottom (for button click)
|
|
4242
|
+
const handleScrollToBottomClick = react.useCallback(() => {
|
|
4243
|
+
setIsAutoScrolling(true);
|
|
4244
|
+
scrollToBottom('smooth');
|
|
4245
|
+
}, [scrollToBottom]);
|
|
4246
|
+
// Force auto-scroll back on (useful for programmatic control)
|
|
4247
|
+
const enableAutoScroll = react.useCallback(() => {
|
|
4248
|
+
setIsAutoScrolling(true);
|
|
4249
|
+
scrollToBottom('smooth');
|
|
4250
|
+
}, [scrollToBottom]);
|
|
4251
|
+
// Disable auto-scroll (useful for programmatic control)
|
|
4252
|
+
const disableAutoScroll = react.useCallback(() => {
|
|
4253
|
+
setIsAutoScrolling(false);
|
|
4254
|
+
}, []);
|
|
4255
|
+
// Cleanup timeout on unmount
|
|
4256
|
+
react.useEffect(() => {
|
|
4257
|
+
return () => {
|
|
4258
|
+
if (scrollTimeoutRef.current) {
|
|
4259
|
+
clearTimeout(scrollTimeoutRef.current);
|
|
4260
|
+
}
|
|
4261
|
+
};
|
|
4262
|
+
}, []);
|
|
4263
|
+
return {
|
|
4264
|
+
isAutoScrolling,
|
|
4265
|
+
chatMessagesRef: chatMessagesRefCallback,
|
|
4266
|
+
handleScroll,
|
|
4267
|
+
handleMessagesChange,
|
|
4268
|
+
scrollToBottom: handleScrollToBottomClick,
|
|
4269
|
+
enableAutoScroll,
|
|
4270
|
+
disableAutoScroll,
|
|
4271
|
+
isMobile,
|
|
4272
|
+
};
|
|
4273
|
+
}
|
|
4274
|
+
|
|
4134
4275
|
/**
|
|
4135
4276
|
* Parses markdown buttons in the format [Button Text](?message=Message%20to%20send)
|
|
4136
4277
|
* Returns both the content without buttons and the extracted buttons
|
|
@@ -4319,9 +4460,9 @@
|
|
|
4319
4460
|
// exportHeaderMarkdown,
|
|
4320
4461
|
participants = [], } = props;
|
|
4321
4462
|
const { onUseTemplate } = props;
|
|
4322
|
-
|
|
4463
|
+
// Use the auto-scroll hook
|
|
4464
|
+
const { isAutoScrolling, chatMessagesRef, handleScroll, handleMessagesChange, scrollToBottom, isMobile: isMobileFromHook, } = useChatAutoScroll();
|
|
4323
4465
|
const textareaRef = react.useRef(null);
|
|
4324
|
-
const chatMessagesRef = react.useRef(null);
|
|
4325
4466
|
const buttonSendRef = react.useRef(null);
|
|
4326
4467
|
const [ratingModalOpen, setRatingModalOpen] = react.useState(false);
|
|
4327
4468
|
const [selectedMessage, setSelectedMessage] = react.useState(null);
|
|
@@ -4334,18 +4475,8 @@
|
|
|
4334
4475
|
// const [inputValue, setInputValue] = useState('');
|
|
4335
4476
|
const [mode] = react.useState('LIGHT'); // Simplified light/dark mode
|
|
4336
4477
|
const [ratingConfirmation, setRatingConfirmation] = react.useState(null);
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
react.useEffect(() => {
|
|
4340
|
-
const checkMobile = () => {
|
|
4341
|
-
const isMobileDevice = window.innerWidth <= 768 ||
|
|
4342
|
-
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
|
4343
|
-
setIsMobile(isMobileDevice);
|
|
4344
|
-
};
|
|
4345
|
-
checkMobile();
|
|
4346
|
-
window.addEventListener('resize', checkMobile);
|
|
4347
|
-
return () => window.removeEventListener('resize', checkMobile);
|
|
4348
|
-
}, []);
|
|
4478
|
+
// Use mobile detection from the hook
|
|
4479
|
+
const isMobile = isMobileFromHook;
|
|
4349
4480
|
react.useEffect(( /* Focus textarea on page load */) => {
|
|
4350
4481
|
if (!textareaRef.current) {
|
|
4351
4482
|
return;
|
|
@@ -4471,59 +4602,16 @@
|
|
|
4471
4602
|
return { ...message, content: humanizeAiText(message.content) };
|
|
4472
4603
|
});
|
|
4473
4604
|
}, [messages, isAiTextHumanized]);
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
// Mobile-optimized scroll to bottom
|
|
4480
|
-
if (isMobile) {
|
|
4481
|
-
chatMessagesElement.scrollTo({
|
|
4482
|
-
top: chatMessagesElement.scrollHeight,
|
|
4483
|
-
behavior: 'smooth',
|
|
4484
|
-
});
|
|
4485
|
-
}
|
|
4486
|
-
else {
|
|
4487
|
-
chatMessagesElement.style.scrollBehavior = 'smooth';
|
|
4488
|
-
chatMessagesElement.scrollBy(0, 10000);
|
|
4489
|
-
chatMessagesElement.style.scrollBehavior = 'auto';
|
|
4490
|
-
}
|
|
4491
|
-
}, children: jsxRuntime.jsx(ArrowIcon, { direction: "DOWN", size: 33 }) }) })), isVoiceCalling && (jsxRuntime.jsx("div", { className: styles$1.voiceCallIndicatorBar, children: jsxRuntime.jsxs("div", { className: styles$1.voiceCallIndicator, children: [jsxRuntime.jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "currentColor", children: jsxRuntime.jsx("path", { d: "M6.62 10.79c1.44 2.83 3.76 5.14 6.59 6.59l2.2-2.2c.27-.27.67-.36 1.02-.24 1.12.37 2.33.57 3.57.57.55 0 1 .45 1 1V20c0 .55-.45 1-1 1-9.39 0-17-7.61-17-17 0-.55.45-1 1-1h3.5c.55 0 1 .45 1 1 0 1.25.2 2.45.57 3.57.11.35.03.74-.25 1.02l-2.2 2.2z" }) }), jsxRuntime.jsx("span", { children: "Voice call active" }), jsxRuntime.jsx("div", { className: styles$1.voiceCallPulse })] }) })), jsxRuntime.jsxs("div", { className: classNames(actionsAlignmentClass), children: [onReset && postprocessedMessages.length !== 0 && (jsxRuntime.jsxs("button", { className: classNames(styles$1.resetButton), onClick: () => {
|
|
4605
|
+
// Trigger auto-scroll when messages change
|
|
4606
|
+
react.useEffect(() => {
|
|
4607
|
+
handleMessagesChange();
|
|
4608
|
+
}, [postprocessedMessages, handleMessagesChange]);
|
|
4609
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [ratingConfirmation && jsxRuntime.jsx("div", { className: styles$1.ratingConfirmation, children: ratingConfirmation }), jsxRuntime.jsx("div", { className: classNames(className, styles$1.Chat, useChatCssClassName('Chat')), style, children: jsxRuntime.jsxs("div", { className: classNames(className, styles$1.chatMainFlow, useChatCssClassName('chatMainFlow')), children: [children && jsxRuntime.jsx("div", { className: classNames(styles$1.chatBar, chatBarCssClassName), children: children }), !isAutoScrolling && (jsxRuntime.jsx("div", { className: styles$1.scrollToBottomContainer, children: jsxRuntime.jsx("button", { "data-button-type": "custom", className: classNames(styles$1.scrollToBottom, scrollToBottomCssClassName), onClick: scrollToBottom, children: jsxRuntime.jsx(ArrowIcon, { direction: "DOWN", size: 33 }) }) })), isVoiceCalling && (jsxRuntime.jsx("div", { className: styles$1.voiceCallIndicatorBar, children: jsxRuntime.jsxs("div", { className: styles$1.voiceCallIndicator, children: [jsxRuntime.jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "currentColor", children: jsxRuntime.jsx("path", { d: "M6.62 10.79c1.44 2.83 3.76 5.14 6.59 6.59l2.2-2.2c.27-.27.67-.36 1.02-.24 1.12.37 2.33.57 3.57.57.55 0 1 .45 1 1V20c0 .55-.45 1-1 1-9.39 0-17-7.61-17-17 0-.55.45-1 1-1h3.5c.55 0 1 .45 1 1 0 1.25.2 2.45.57 3.57.11.35.03.74-.25 1.02l-2.2 2.2z" }) }), jsxRuntime.jsx("span", { children: "Voice call active" }), jsxRuntime.jsx("div", { className: styles$1.voiceCallPulse })] }) })), jsxRuntime.jsxs("div", { className: classNames(actionsAlignmentClass), children: [onReset && postprocessedMessages.length !== 0 && (jsxRuntime.jsxs("button", { className: classNames(styles$1.resetButton), onClick: () => {
|
|
4492
4610
|
if (!confirm(`Do you really want to reset the chat?`)) {
|
|
4493
4611
|
return;
|
|
4494
4612
|
}
|
|
4495
4613
|
onReset();
|
|
4496
|
-
}, children: [jsxRuntime.jsx(ResetIcon, {}), jsxRuntime.jsx("span", { className: styles$1.resetButtonText, children: "New chat" })] })), onUseTemplate && (jsxRuntime.jsxs("button", { className: classNames(styles$1.useTemplateButton), onClick: onUseTemplate, children: [jsxRuntime.jsx("span", { className: styles$1.resetButtonText, children: "Use this template" }), jsxRuntime.jsx(TemplateIcon, { size: 16 })] }))] }), jsxRuntime.jsx("div", { className: classNames(styles$1.chatMessages, useChatCssClassName('chatMessages')), ref: (
|
|
4497
|
-
chatMessagesRef.current = chatMessagesElement;
|
|
4498
|
-
if (chatMessagesElement === null) {
|
|
4499
|
-
return;
|
|
4500
|
-
}
|
|
4501
|
-
if (!isAutoScrolling) {
|
|
4502
|
-
return;
|
|
4503
|
-
}
|
|
4504
|
-
// Mobile-optimized scrolling
|
|
4505
|
-
if (isMobile) {
|
|
4506
|
-
// Delay scroll slightly on mobile for better performance
|
|
4507
|
-
requestAnimationFrame(() => {
|
|
4508
|
-
chatMessagesElement.scrollTo({
|
|
4509
|
-
top: chatMessagesElement.scrollHeight,
|
|
4510
|
-
behavior: 'smooth',
|
|
4511
|
-
});
|
|
4512
|
-
});
|
|
4513
|
-
}
|
|
4514
|
-
else {
|
|
4515
|
-
// Desktop smooth scrolling
|
|
4516
|
-
chatMessagesElement.style.scrollBehavior = 'smooth';
|
|
4517
|
-
chatMessagesElement.scrollBy(0, 1000);
|
|
4518
|
-
chatMessagesElement.style.scrollBehavior = 'auto';
|
|
4519
|
-
}
|
|
4520
|
-
}, onScroll: (event) => {
|
|
4521
|
-
const element = event.target;
|
|
4522
|
-
if (!(element instanceof HTMLDivElement)) {
|
|
4523
|
-
return;
|
|
4524
|
-
}
|
|
4525
|
-
setAutoScrolling(element.scrollTop + element.clientHeight > element.scrollHeight - 100);
|
|
4526
|
-
}, children: postprocessedMessages.map((message, i) => {
|
|
4614
|
+
}, children: [jsxRuntime.jsx(ResetIcon, {}), jsxRuntime.jsx("span", { className: styles$1.resetButtonText, children: "New chat" })] })), onUseTemplate && (jsxRuntime.jsxs("button", { className: classNames(styles$1.useTemplateButton), onClick: onUseTemplate, children: [jsxRuntime.jsx("span", { className: styles$1.resetButtonText, children: "Use this template" }), jsxRuntime.jsx(TemplateIcon, { size: 16 })] }))] }), jsxRuntime.jsx("div", { className: classNames(styles$1.chatMessages, useChatCssClassName('chatMessages')), ref: chatMessagesRef, onScroll: handleScroll, children: postprocessedMessages.map((message, i) => {
|
|
4527
4615
|
const participant = participants.find((participant) => participant.name === message.from);
|
|
4528
4616
|
const avatarSrc = (participant && participant.avatarSrc) || '';
|
|
4529
4617
|
const color = Color.from((participant && participant.color) || '#ccc');
|
|
@@ -5397,6 +5485,53 @@
|
|
|
5397
5485
|
return (jsxRuntime.jsx("div", { "data-book-component": "BookEditor", ref: hostRef, className: classNames(styles.BookEditor, isVerbose && styles.isVerbose, className), style: style, children: shadowReady && shadowRootRef.current ? reactDom.createPortal(editorInner, shadowRootRef.current) : jsxRuntime.jsx(jsxRuntime.Fragment, { children: "Loading..." }) }));
|
|
5398
5486
|
}
|
|
5399
5487
|
|
|
5488
|
+
/**
|
|
5489
|
+
* Hook to create a sendMessage function for an <LlmChat/> component WITHOUT needing any React Context.
|
|
5490
|
+
*
|
|
5491
|
+
* Usage pattern:
|
|
5492
|
+
* ```tsx
|
|
5493
|
+
* const sendMessage = useSendMessageToLlmChat();
|
|
5494
|
+
* return (
|
|
5495
|
+
* <>
|
|
5496
|
+
* <button onClick={() => sendMessage('Hello!')}>Hello</button>
|
|
5497
|
+
* <LlmChat llmTools={llmTools} sendMessage={sendMessage} />
|
|
5498
|
+
* </>
|
|
5499
|
+
* );
|
|
5500
|
+
* ```
|
|
5501
|
+
*
|
|
5502
|
+
* - No provider wrapping needed.
|
|
5503
|
+
* - Safe to call before the <LlmChat/> mounts (messages will be queued).
|
|
5504
|
+
* - Keeps DRY by letting <LlmChat/> reuse its internal `handleMessage` logic.
|
|
5505
|
+
*
|
|
5506
|
+
* @public exported from `@promptbook/components`
|
|
5507
|
+
*/
|
|
5508
|
+
function useSendMessageToLlmChat() {
|
|
5509
|
+
const ref = react.useRef(null);
|
|
5510
|
+
if (!ref.current) {
|
|
5511
|
+
let handler = null;
|
|
5512
|
+
const queue = [];
|
|
5513
|
+
const sendMessage = (message) => {
|
|
5514
|
+
if (handler) {
|
|
5515
|
+
// Fire and forget
|
|
5516
|
+
void handler(message);
|
|
5517
|
+
}
|
|
5518
|
+
else {
|
|
5519
|
+
queue.push(message);
|
|
5520
|
+
}
|
|
5521
|
+
};
|
|
5522
|
+
sendMessage._attach = (attachedHandler) => {
|
|
5523
|
+
handler = attachedHandler;
|
|
5524
|
+
// Flush queued messages
|
|
5525
|
+
while (queue.length > 0) {
|
|
5526
|
+
const next = queue.shift();
|
|
5527
|
+
void handler(next);
|
|
5528
|
+
}
|
|
5529
|
+
};
|
|
5530
|
+
ref.current = sendMessage;
|
|
5531
|
+
}
|
|
5532
|
+
return ref.current;
|
|
5533
|
+
}
|
|
5534
|
+
|
|
5400
5535
|
/**
|
|
5401
5536
|
* Utility functions for persisting chat conversations in localStorage
|
|
5402
5537
|
*
|
|
@@ -5483,16 +5618,22 @@
|
|
|
5483
5618
|
* @public exported from `@promptbook/components`
|
|
5484
5619
|
*/
|
|
5485
5620
|
function LlmChat(props) {
|
|
5486
|
-
const { llmTools, persistenceKey, onChange, onReset, ...restProps } = props;
|
|
5621
|
+
const { llmTools, persistenceKey, onChange, onReset, initialMessages, sendMessage, ...restProps } = props;
|
|
5487
5622
|
// Internal state management
|
|
5488
|
-
const [messages, setMessages] = react.useState([]);
|
|
5623
|
+
const [messages, setMessages] = react.useState(() => (initialMessages ? [...initialMessages] : []));
|
|
5489
5624
|
const [tasksProgress, setTasksProgress] = react.useState([]);
|
|
5625
|
+
/**
|
|
5626
|
+
* Tracks whether the user (or system via persistence restoration) has interacted.
|
|
5627
|
+
* We do NOT persist purely initialMessages until the user sends something.
|
|
5628
|
+
*/
|
|
5629
|
+
const hasUserInteractedRef = react.useRef(false);
|
|
5490
5630
|
// Load persisted messages on component mount
|
|
5491
5631
|
react.useEffect(() => {
|
|
5492
5632
|
if (persistenceKey && ChatPersistence.isAvailable()) {
|
|
5493
5633
|
const persistedMessages = ChatPersistence.loadMessages(persistenceKey);
|
|
5494
5634
|
if (persistedMessages.length > 0) {
|
|
5495
5635
|
setMessages(persistedMessages);
|
|
5636
|
+
hasUserInteractedRef.current = true; // Persisted conversation exists; allow saving next changes
|
|
5496
5637
|
// Notify about loaded messages
|
|
5497
5638
|
if (onChange) {
|
|
5498
5639
|
onChange(persistedMessages, participants);
|
|
@@ -5502,7 +5643,10 @@
|
|
|
5502
5643
|
}, [persistenceKey]); // Only depend on persistenceKey, not participants or onChange to avoid infinite loops
|
|
5503
5644
|
// Save messages to localStorage whenever messages change (and persistence is enabled)
|
|
5504
5645
|
react.useEffect(() => {
|
|
5505
|
-
if (persistenceKey &&
|
|
5646
|
+
if (persistenceKey &&
|
|
5647
|
+
ChatPersistence.isAvailable() &&
|
|
5648
|
+
messages.length > 0 &&
|
|
5649
|
+
hasUserInteractedRef.current) {
|
|
5506
5650
|
ChatPersistence.saveMessages(persistenceKey, messages);
|
|
5507
5651
|
}
|
|
5508
5652
|
}, [messages, persistenceKey]);
|
|
@@ -5523,6 +5667,7 @@
|
|
|
5523
5667
|
], [llmTools.profile, llmTools.title]);
|
|
5524
5668
|
// Handle user messages and LLM responses
|
|
5525
5669
|
const handleMessage = react.useCallback(async (messageContent) => {
|
|
5670
|
+
hasUserInteractedRef.current = true;
|
|
5526
5671
|
// Add user message
|
|
5527
5672
|
const userMessage = {
|
|
5528
5673
|
id: `user_${Date.now()}`,
|
|
@@ -5610,6 +5755,7 @@
|
|
|
5610
5755
|
const handleReset = react.useCallback(async () => {
|
|
5611
5756
|
setMessages([]);
|
|
5612
5757
|
setTasksProgress([]);
|
|
5758
|
+
hasUserInteractedRef.current = false;
|
|
5613
5759
|
// Clear persisted messages if persistence is enabled
|
|
5614
5760
|
if (persistenceKey && ChatPersistence.isAvailable()) {
|
|
5615
5761
|
ChatPersistence.clearMessages(persistenceKey);
|
|
@@ -5622,6 +5768,12 @@
|
|
|
5622
5768
|
onChange([], participants);
|
|
5623
5769
|
}
|
|
5624
5770
|
}, [persistenceKey, onReset, onChange, participants]);
|
|
5771
|
+
// Attach internal handler to external sendMessage (from useSendMessageToLlmChat) if provided
|
|
5772
|
+
react.useEffect(() => {
|
|
5773
|
+
if (sendMessage && sendMessage._attach) {
|
|
5774
|
+
sendMessage._attach(handleMessage);
|
|
5775
|
+
}
|
|
5776
|
+
}, [sendMessage, handleMessage]);
|
|
5625
5777
|
return (jsxRuntime.jsx(Chat, { ...restProps, messages, onReset, tasksProgress, participants, onMessage: handleMessage, onReset: handleReset }));
|
|
5626
5778
|
}
|
|
5627
5779
|
|
|
@@ -5643,6 +5795,8 @@
|
|
|
5643
5795
|
exports.isMarkdownContent = isMarkdownContent;
|
|
5644
5796
|
exports.parseMessageButtons = parseMessageButtons;
|
|
5645
5797
|
exports.renderMarkdown = renderMarkdown;
|
|
5798
|
+
exports.useChatAutoScroll = useChatAutoScroll;
|
|
5799
|
+
exports.useSendMessageToLlmChat = useSendMessageToLlmChat;
|
|
5646
5800
|
|
|
5647
5801
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
5648
5802
|
|