@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.
@@ -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,2 @@
1
+ export * from './useChatAutoScroll';
2
+ export * from './useSendMessageToLlmChat';
@@ -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-16`).
18
+ * It follows semantic versioning (e.g., `0.101.0-17`).
19
19
  *
20
20
  * @generated
21
21
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@promptbook/components",
3
- "version": "0.101.0-17",
3
+ "version": "0.101.0-18",
4
4
  "description": "Promptbook: Run AI apps in plain human language across multiple models and platforms",
5
5
  "private": false,
6
6
  "sideEffects": false,
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-17';
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
- const [isAutoScrolling, setAutoScrolling] = react.useState(true);
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
- const [isMobile, setIsMobile] = react.useState(false);
4338
- // Detect mobile device
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
- 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: () => {
4475
- const chatMessagesElement = chatMessagesRef.current;
4476
- if (chatMessagesElement === null) {
4477
- return;
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: (chatMessagesElement) => {
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 && ChatPersistence.isAvailable() && messages.length > 0) {
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