@sybilion/uilib 1.3.71 → 1.3.73

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. package/dist/esm/components/ui/Chat/Chat.js +2 -2
  2. package/dist/esm/components/ui/Chat/ChatChrome/ChatChrome.js +2 -2
  3. package/dist/esm/components/ui/Chat/ChatChrome/ChatChrome.styl.js +1 -1
  4. package/dist/esm/components/ui/Chat/ChatSheet/ChatSelector.js +7 -2
  5. package/dist/esm/components/ui/Chat/ChatSheet/useChatPanelChromeModel.js +41 -9
  6. package/dist/esm/contexts/chat-context.js +19 -3
  7. package/dist/esm/types/src/components/ui/Chat/Chat.d.ts +1 -1
  8. package/dist/esm/types/src/components/ui/Chat/Chat.types.d.ts +2 -0
  9. package/dist/esm/types/src/components/ui/Chat/ChatChrome/ChatChrome.d.ts +1 -1
  10. package/dist/esm/types/src/components/ui/Chat/ChatChrome/ChatChrome.types.d.ts +2 -0
  11. package/dist/esm/types/src/components/ui/Chat/ChatSheet/ChatSelector.d.ts +3 -1
  12. package/dist/esm/types/src/components/ui/Chat/ChatSheet/ChatSheet.d.ts +1 -1
  13. package/dist/esm/types/src/components/ui/Chat/ChatSheet/useChatPanelChromeModel.d.ts +6 -1
  14. package/dist/esm/types/src/contexts/chat-context.d.ts +7 -3
  15. package/package.json +1 -1
  16. package/src/components/ui/Chat/Chat.tsx +6 -1
  17. package/src/components/ui/Chat/Chat.types.ts +2 -0
  18. package/src/components/ui/Chat/ChatChrome/ChatChrome.styl +1 -1
  19. package/src/components/ui/Chat/ChatChrome/ChatChrome.tsx +2 -0
  20. package/src/components/ui/Chat/ChatChrome/ChatChrome.types.ts +2 -0
  21. package/src/components/ui/Chat/ChatSheet/ChatSelector.tsx +8 -1
  22. package/src/components/ui/Chat/ChatSheet/ChatSheet.tsx +1 -1
  23. package/src/components/ui/Chat/ChatSheet/useChatPanelChromeModel.tsx +50 -8
  24. package/src/contexts/chat-context.tsx +29 -4
@@ -7,8 +7,8 @@ import { ChatMessage } from './ChatMessage/ChatMessage.js';
7
7
  import { ChatPrompt } from './ChatPrompt/ChatPrompt.js';
8
8
  import { ChatSelector } from './ChatSheet/ChatSelector.js';
9
9
 
10
- function Chat({ children, className, isEmpty, scopeId, onChatDeleted, ...props }) {
11
- return (jsxs("div", { className: cn(S.root, className, isEmpty && S.isEmpty), ...props, children: [scopeId ? (jsx("div", { className: S.header, children: jsx(ChatSelector, { id: scopeId, onChatDeleted: onChatDeleted }) })) : null, children] }));
10
+ function Chat({ children, className, isEmpty, scopeId, onChatDeleted, onNewChat, ...props }) {
11
+ return (jsxs("div", { className: cn(S.root, className, isEmpty && S.isEmpty), ...props, children: [scopeId ? (jsx("div", { className: S.header, children: jsx(ChatSelector, { id: scopeId, onChatDeleted: onChatDeleted, onNewChat: onNewChat }) })) : null, children] }));
12
12
  }
13
13
  Chat.Prompt = ChatPrompt;
14
14
  Chat.Message = ChatMessage;
@@ -14,7 +14,7 @@ import { filterToTextAttachments, isAttachmentsDropzoneEnabled, buildAcceptAttr
14
14
  import { extractChatAttachmentItems } from '../chatAttachmentExtract.js';
15
15
  import S from './ChatChrome.styl.js';
16
16
 
17
- function ChatChrome({ showResizeHandle, resizeHandle, onClose, isEmpty, renderPresets, messages, onQuickReply, suppressedQuickReplyKeys, isLoading, scriptContinueLabel, onScriptContinue, renderMessageChart, showSyntheticBranchButtons, unusedBranchKeys, showInlinePresets, isLastMessageFromUser, scrollRef, effectiveScopeId, onPromptSubmit, onChatDeleted, promptPrefill, footerClassName, emptyState, allowedAttachments, allowPdfAttachments = false, onAttachmentsDropped, slashCommandItems, onSlashItemCommand, promptPlaceholder, }) {
17
+ function ChatChrome({ showResizeHandle, resizeHandle, onClose, isEmpty, renderPresets, messages, onQuickReply, suppressedQuickReplyKeys, isLoading, scriptContinueLabel, onScriptContinue, renderMessageChart, showSyntheticBranchButtons, unusedBranchKeys, showInlinePresets, isLastMessageFromUser, scrollRef, effectiveScopeId, onPromptSubmit, onChatDeleted, onNewChat, promptPrefill, footerClassName, emptyState, allowedAttachments, allowPdfAttachments = false, onAttachmentsDropped, slashCommandItems, onSlashItemCommand, promptPlaceholder, }) {
18
18
  const filteredAllowedAttachments = useMemo(() => filterToTextAttachments(allowedAttachments), [allowedAttachments]);
19
19
  const attachmentsDropzoneEnabled = isAttachmentsDropzoneEnabled(allowedAttachments, allowPdfAttachments);
20
20
  const attachmentAccept = useMemo(() => buildAcceptAttr(filteredAllowedAttachments, allowPdfAttachments), [filteredAllowedAttachments, allowPdfAttachments]);
@@ -66,7 +66,7 @@ function ChatChrome({ showResizeHandle, resizeHandle, onClose, isEmpty, renderPr
66
66
  if (inner)
67
67
  setTimeout(scrollToBottom, 100);
68
68
  }, [isEmpty, messages.length]);
69
- return (jsxs("div", { className: S.root, children: [showResizeHandle && resizeHandle ? (jsx(PanelResizeHandle, { edge: "leading", isActive: resizeHandle.isActive, startWidthPx: resizeHandle.startWidthPx, getShellWidth: resizeHandle.getShellWidth, onDragWidth: resizeHandle.onDragWidth, onDragComplete: resizeHandle.onDragComplete, className: cn(SidebarStem.sidebarResizeHandle, S.chatResizeHandle) })) : null, jsx("div", { className: S.panelHeader, children: onClose ? (jsx(Button, { type: "button", variant: "ghost", icon: true, className: S.panelClose, "aria-label": "Close chat", onClick: onClose, children: jsx(X, { className: "size-4" }) })) : null }), jsxs("div", { className: S.content, children: [attachmentsDropzoneEnabled ? (jsx(DropZone, { accept: attachmentAccept, label: "Drop text files to attach", multiple: true, ghost: true, overlayScope: "container", disabled: promptDisabled, className: S.attachmentDropzone, onFiles: handleAttachmentFiles })) : null, jsxs(Chat, { isEmpty: isEmpty, scopeId: effectiveScopeId, onChatDeleted: onChatDeleted, children: [isEmpty ? (jsxs(Fragment, { children: [jsx(Chat.EmptyState, { ...emptyState }), renderPresets('fixed')] })) : (jsx("div", { className: S.scrollWrapper, children: jsxs(Scroll, { y: true, yScrollbarClassName: S.scrollbar, className: S.scroll, innerClassName: S.scrollInner, offset: { y: { before: 56, after: 180 } }, fadeSize: "m", autoHide: true, ref: scrollRef, children: [messages.map((msg, index, arr) => {
69
+ return (jsxs("div", { className: S.root, children: [showResizeHandle && resizeHandle ? (jsx(PanelResizeHandle, { edge: "leading", isActive: resizeHandle.isActive, startWidthPx: resizeHandle.startWidthPx, getShellWidth: resizeHandle.getShellWidth, onDragWidth: resizeHandle.onDragWidth, onDragComplete: resizeHandle.onDragComplete, className: cn(SidebarStem.sidebarResizeHandle, S.chatResizeHandle) })) : null, jsx("div", { className: S.panelHeader, children: onClose ? (jsx(Button, { type: "button", variant: "ghost", icon: true, className: S.panelClose, "aria-label": "Close chat", onClick: onClose, children: jsx(X, { className: "size-4" }) })) : null }), jsxs("div", { className: S.content, children: [attachmentsDropzoneEnabled ? (jsx(DropZone, { accept: attachmentAccept, label: "Drop text files to attach", multiple: true, ghost: true, overlayScope: "container", disabled: promptDisabled, className: S.attachmentDropzone, onFiles: handleAttachmentFiles })) : null, jsxs(Chat, { isEmpty: isEmpty, scopeId: effectiveScopeId, onChatDeleted: onChatDeleted, onNewChat: onNewChat, children: [isEmpty ? (jsxs(Fragment, { children: [jsx(Chat.EmptyState, { ...emptyState }), renderPresets('fixed')] })) : (jsx("div", { className: S.scrollWrapper, children: jsxs(Scroll, { y: true, yScrollbarClassName: S.scrollbar, className: S.scroll, innerClassName: S.scrollInner, offset: { y: { before: 56, after: 180 } }, fadeSize: "m", autoHide: true, ref: scrollRef, children: [messages.map((msg, index, arr) => {
70
70
  const isLast = index === arr.length - 1;
71
71
  return (jsx(Chat.Message, { role: msg.role, text: msg.text, inProgress: msg.inProgress, userTextFileAttachments: msg.userTextFileAttachments, onQuickReply: onQuickReply, suppressedQuickReplyKeys: suppressedQuickReplyKeys, quickReplyDisabled: isLoading, isLastMessage: isLast, scriptContinue: isLast && scriptContinueLabel
72
72
  ? { label: scriptContinueLabel }
@@ -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)}}.ChatChrome_root__oh4Ay{border-radius:var(--p-4);display:flex;flex-direction:column;height:100%;min-height:0;overflow:hidden;position:relative}.ChatChrome_chatResizeHandle__epfiT{background-color:var(--page-color);border-radius:2.5px;height:calc(100vh + 200px);left:0;opacity:0;position:absolute;right:auto;top:-200px;touch-action:none;transition:opacity .15s ease-out;-webkit-user-select:none;-moz-user-select:none;user-select:none;width:5px;z-index:30}.ChatChrome_chatResizeHandle__epfiT:before{left:0;right:auto}.ChatChrome_panelHeader__Hkfit{align-items:center;display:flex;flex-shrink:0;justify-content:flex-end;min-height:70px;padding:var(--p-2) var(--p-3);position:absolute;width:100%;z-index:100}.ChatChrome_panelClose__DbKxz{flex-shrink:0}.ChatChrome_content__5qFEi{display:flex;flex:1;flex-direction:column;min-height:0;position:relative}.ChatChrome_attachmentDropzone__OC8UI{inset:0;position:absolute;z-index:200}.ChatChrome_scrollWrapper__m4HMu{flex:1;min-height:0;position:relative}.ChatChrome_scroll__oCxoJ{align-items:flex-end;height:100%;max-height:100%;max-width:100%;padding-bottom:var(--p-2);position:absolute;width:100%;z-index:3}.ChatChrome_scrollbar__Hu0aG{right:0!important}.ChatChrome_scrollInner__K9hIy{min-height:100%;padding-top:var(--p-10)}.ChatChrome_scrollInner__K9hIy:after{content:\"\";display:block;height:170px}.ChatChrome_footer__a5Bpp{backdrop-filter:blur(30px);background-color:var(--background-alpha-800);border-top:1px solid var(--border);bottom:0;box-shadow:0 0 20px 16px var(--background);display:flex;flex-direction:column;position:absolute;width:100%;z-index:50}.ChatChrome_notice__JACIw{color:var(--muted-foreground);font-size:var(--text-xs);left:0;margin-bottom:var(--p-1);pointer-events:none;position:absolute;right:0;text-align:center;top:calc(var(--p-7)*-1)}@media (max-width:768px){.ChatChrome_notice__JACIw{font-size:10px}}.ChatChrome_loader__9-lnf{color:var(--muted-foreground);margin:var(--p-2) var(--p-6) var(--p-10)}.ChatChrome_branchRow__NMDNv{display:flex;flex-wrap:wrap;gap:8px;margin-top:var(--p-6);padding:0 var(--p-6);width:100%}.ChatChrome_branchBtnWrap__aOSVP{display:inline-flex;vertical-align:middle}";
3
+ var css_248z = "@media (max-width:768px){:root{--page-x-padding:var(--p-6);--page-y-padding:var(--p-6)}}.ChatChrome_root__oh4Ay{border-radius:var(--p-4);display:flex;flex-direction:column;height:100%;min-height:0;overflow:hidden;position:relative}.ChatChrome_chatResizeHandle__epfiT{background-color:var(--page-color);border-radius:2.5px;height:calc(100vh + 200px);left:0;opacity:0;position:absolute;right:auto;top:-200px;touch-action:none;transition:opacity .15s ease-out;-webkit-user-select:none;-moz-user-select:none;user-select:none;width:5px;z-index:30}.ChatChrome_chatResizeHandle__epfiT:before{left:0;right:auto}.ChatChrome_panelHeader__Hkfit{align-items:center;display:flex;flex-shrink:0;justify-content:flex-end;min-height:70px;padding:var(--p-2) var(--p-3);position:absolute;width:100%;z-index:100}.ChatChrome_panelClose__DbKxz{flex-shrink:0}.ChatChrome_content__5qFEi{display:flex;flex:1;flex-direction:column;min-height:0;position:relative}.ChatChrome_attachmentDropzone__OC8UI{inset:0;position:absolute;z-index:200}.ChatChrome_scrollWrapper__m4HMu{flex:1;min-height:0;position:relative}.ChatChrome_scroll__oCxoJ{align-items:flex-end;height:100%;max-height:100%;max-width:100%;padding-bottom:var(--p-2);position:absolute;width:100%;z-index:3}.ChatChrome_scrollbar__Hu0aG{right:0!important}.ChatChrome_scrollInner__K9hIy{min-height:100%;padding-top:var(--p-10)}.ChatChrome_scrollInner__K9hIy:after{content:\"\";display:block;height:260px}.ChatChrome_footer__a5Bpp{backdrop-filter:blur(30px);background-color:var(--background-alpha-800);border-top:1px solid var(--border);bottom:0;box-shadow:0 0 20px 16px var(--background);display:flex;flex-direction:column;position:absolute;width:100%;z-index:50}.ChatChrome_notice__JACIw{color:var(--muted-foreground);font-size:var(--text-xs);left:0;margin-bottom:var(--p-1);pointer-events:none;position:absolute;right:0;text-align:center;top:calc(var(--p-7)*-1)}@media (max-width:768px){.ChatChrome_notice__JACIw{font-size:10px}}.ChatChrome_loader__9-lnf{color:var(--muted-foreground);margin:var(--p-2) var(--p-6) var(--p-10)}.ChatChrome_branchRow__NMDNv{display:flex;flex-wrap:wrap;gap:8px;margin-top:var(--p-6);padding:0 var(--p-6);width:100%}.ChatChrome_branchBtnWrap__aOSVP{display:inline-flex;vertical-align:middle}";
4
4
  var S = {"root":"ChatChrome_root__oh4Ay","chatResizeHandle":"ChatChrome_chatResizeHandle__epfiT","panelHeader":"ChatChrome_panelHeader__Hkfit","panelClose":"ChatChrome_panelClose__DbKxz","content":"ChatChrome_content__5qFEi","attachmentDropzone":"ChatChrome_attachmentDropzone__OC8UI","scrollWrapper":"ChatChrome_scrollWrapper__m4HMu","scroll":"ChatChrome_scroll__oCxoJ","scrollbar":"ChatChrome_scrollbar__Hu0aG","scrollInner":"ChatChrome_scrollInner__K9hIy","footer":"ChatChrome_footer__a5Bpp","notice":"ChatChrome_notice__JACIw","loader":"ChatChrome_loader__9-lnf","branchRow":"ChatChrome_branchRow__NMDNv","branchBtnWrap":"ChatChrome_branchBtnWrap__aOSVP"};
5
5
  styleInject(css_248z);
6
6
 
@@ -6,11 +6,16 @@ import { Button } from '../../Button/Button.js';
6
6
  import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '../../Select/Select.js';
7
7
  import S from './ChatSelector.styl.js';
8
8
 
9
- function ChatSelector({ id, className, onChatDeleted, }) {
9
+ function ChatSelector({ id, className, onChatDeleted, onNewChat, }) {
10
10
  const { chats, currentChatId, setCurrentChatId, newChat, deleteChat } = useChatsForScopeId(id);
11
11
  const handleValueChange = (value) => {
12
12
  if (value === 'new') {
13
- newChat();
13
+ if (onNewChat) {
14
+ onNewChat();
15
+ }
16
+ else {
17
+ newChat();
18
+ }
14
19
  }
15
20
  else if (value) {
16
21
  setCurrentChatId(value);
@@ -18,11 +18,11 @@ import { shouldOpenChatFromUrl, shouldHealChatShellDesync, shouldCloseStaleLocal
18
18
  /** Fallback when `scopeId` prop omitted; apps should pass an explicit composite scope (e.g. `${userId}-dashboard`). */
19
19
  const NO_SCOPE_FALLBACK = '__uilib_chat_scope_unset__';
20
20
  const SCRIPT_STEP_DELAY_MS = 1200;
21
- const CHAT_NEW_SHORTCUT_KEY = 'o';
21
+ const CHAT_NEW_SHORTCUT_KEY = '0';
22
22
  const CHAT_QUERY_PARAM = 'chat';
23
23
  const CHAT_OPEN_VALUE = 'open';
24
24
  const PROMPT_QUERY_PARAM = 'prompt';
25
- function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onScriptComplete, renderMessageChart, emptyState, allowedAttachments, allowPdfAttachments, onAttachmentsDropped, slashCommandItems, onSlashItemCommand, transformSendPayload, }) {
25
+ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onScriptComplete, renderMessageChart, emptyState, allowedAttachments, allowPdfAttachments, onAttachmentsDropped, slashCommandItems, onSlashItemCommand, copyHistoryOnNewChat = false, transformSendPayload, }) {
26
26
  const effectiveScopeId = scopeId ?? NO_SCOPE_FALLBACK;
27
27
  const isMobile = useIsMobile();
28
28
  const { chatPanelContainer, isOpen: sidebarNavOpen, setOpen: setSidebarNavOpen, sidebarWidthPx, chatWidthPx, setChatWidthPx, getShellWidth, chatPanelOpen: shellChatPanelOpen, setChatPanelOpen, } = useSidebar();
@@ -158,8 +158,39 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
158
158
  }
159
159
  };
160
160
  const isEmpty = isChatEmpty(chat) && !isLoading;
161
- const openNewChatWithPrefill = useCallback((prompt) => {
161
+ const startEmptyNewChat = useCallback(() => {
162
+ const priorChatId = currentChatId;
162
163
  const sessionId = newChat();
164
+ if (sessionId && priorChatId) {
165
+ endLocalDemoFlow(priorChatId);
166
+ }
167
+ if (sessionId) {
168
+ setPromptLinkPrefill(null);
169
+ }
170
+ return sessionId;
171
+ }, [newChat, currentChatId, endLocalDemoFlow]);
172
+ const startNewChatFromSelector = useCallback(() => {
173
+ const priorChatId = currentChatId;
174
+ const messages = chat?.messages ?? [];
175
+ const sessionId = newChat(copyHistoryOnNewChat && messages.length > 0
176
+ ? { seedMessages: messages }
177
+ : undefined);
178
+ if (sessionId && priorChatId) {
179
+ endLocalDemoFlow(priorChatId);
180
+ }
181
+ if (sessionId) {
182
+ setPromptLinkPrefill(null);
183
+ }
184
+ return sessionId;
185
+ }, [
186
+ newChat,
187
+ copyHistoryOnNewChat,
188
+ chat?.messages,
189
+ currentChatId,
190
+ endLocalDemoFlow,
191
+ ]);
192
+ const openNewChatWithPrefill = useCallback((prompt) => {
193
+ const sessionId = startEmptyNewChat();
163
194
  if (sessionId == null) {
164
195
  logger.warn('Chat prefill: sign in to use the assistant.');
165
196
  return;
@@ -167,7 +198,7 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
167
198
  const trimmed = prompt.trim();
168
199
  setPromptLinkPrefill(trimmed || null);
169
200
  onOpenChange(true);
170
- }, [newChat, onOpenChange]);
201
+ }, [startEmptyNewChat, onOpenChange]);
171
202
  /**
172
203
  * App link: `?prompt=…` — open panel, pre-fill composer, strip param (read once on mount
173
204
  * from `location.search`). If the selected session already has messages, `newChat()` first.
@@ -193,7 +224,7 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
193
224
  promptParamHandledInEffectRef.current = true;
194
225
  const needsFirstSession = chats.length === 0;
195
226
  if (!isEmpty || needsFirstSession) {
196
- const sessionId = newChat();
227
+ const sessionId = startEmptyNewChat();
197
228
  if (sessionId == null) {
198
229
  logger.warn('Chat prompt link: sign in to use the assistant.');
199
230
  mutateSearchParams(n => {
@@ -216,14 +247,14 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
216
247
  event: 'keydown',
217
248
  callback: (e) => {
218
249
  const ke = e;
219
- if (ke.key.toLowerCase() !== CHAT_NEW_SHORTCUT_KEY ||
250
+ if (ke.key !== CHAT_NEW_SHORTCUT_KEY ||
220
251
  !(ke.metaKey || ke.ctrlKey) ||
221
- !ke.shiftKey ||
252
+ ke.shiftKey ||
222
253
  ke.altKey) {
223
254
  return;
224
255
  }
225
256
  ke.preventDefault();
226
- newChat();
257
+ startEmptyNewChat();
227
258
  onOpenChange(true);
228
259
  },
229
260
  });
@@ -875,6 +906,7 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
875
906
  effectiveScopeId,
876
907
  onPromptSubmit: onPromptSubmitWithSlashCommands,
877
908
  onChatDeleted: endLocalDemoFlow,
909
+ onNewChat: startNewChatFromSelector,
878
910
  promptPrefill: promptLinkPrefill,
879
911
  emptyState: resolvedEmptyState,
880
912
  allowedAttachments,
@@ -895,7 +927,7 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
895
927
  isOpen,
896
928
  onOpenChange,
897
929
  toggleOpen,
898
- newChat,
930
+ newChat: startEmptyNewChat,
899
931
  openNewChatWithPrefill,
900
932
  chatPanelContainer,
901
933
  };
@@ -80,6 +80,19 @@ function addScopeIdToRegistry(scopeId) {
80
80
  LS.set(CHAT_SCOPE_IDS_REGISTRY_KEY, [...registry, scopeId]);
81
81
  }
82
82
  }
83
+ /** Shallow-clone messages for seeding another session; drops in-progress rows. */
84
+ function cloneMessagesForNewSession(messages) {
85
+ return messages
86
+ .filter(message => !message.inProgress)
87
+ .map(message => ({
88
+ ...message,
89
+ ...(message.userTextFileAttachments
90
+ ? {
91
+ userTextFileAttachments: message.userTextFileAttachments.map(attachment => ({ ...attachment })),
92
+ }
93
+ : {}),
94
+ }));
95
+ }
83
96
  function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessageFn, }) {
84
97
  const [chats, setChats] = useState(() => {
85
98
  if (userSwitchKey === null)
@@ -125,15 +138,18 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
125
138
  return null;
126
139
  return v;
127
140
  }, [currentChatId]);
128
- const newChat = useCallback((scopeId) => {
141
+ const newChat = useCallback((scopeId, options) => {
129
142
  if (userSwitchKey === null)
130
143
  return undefined;
131
144
  addScopeIdToRegistry(scopeId);
132
145
  const sessionId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
146
+ const seededMessages = options?.seedMessages && options.seedMessages.length > 0
147
+ ? cloneMessagesForNewSession(options.seedMessages)
148
+ : [];
133
149
  const newChat = {
134
150
  session_id: sessionId,
135
151
  name: '',
136
- messages: [],
152
+ messages: seededMessages,
137
153
  };
138
154
  setChats(prev => {
139
155
  const currentChats = prev[scopeId] ?? [];
@@ -436,7 +452,7 @@ function useChatsForScopeId(scopeId) {
436
452
  currentChatId,
437
453
  isOutboundPending,
438
454
  setCurrentChatId: (targetId) => setCurrentChatId(scopeId, targetId),
439
- newChat: () => newChat(scopeId),
455
+ newChat: (options) => newChat(scopeId, options),
440
456
  addMessage: (chatId, role, text, options) => addMessage(scopeId, chatId, role, text, options),
441
457
  removeMessageById: (chatId, messageId) => removeMessageById(scopeId, chatId, messageId),
442
458
  updateMessageById: (chatId, messageId, patch) => updateMessageById(scopeId, chatId, messageId, patch),
@@ -2,7 +2,7 @@ import { ChatPresets } from '#uilib/components/ui/Chat/ChatPresets';
2
2
  import type { ChatProps } from './Chat.types';
3
3
  import { ChatEmptyState } from './ChatEmptyState/ChatEmptyState';
4
4
  import { ChatMessage } from './ChatMessage';
5
- export declare function Chat({ children, className, isEmpty, scopeId, onChatDeleted, ...props }: ChatProps): import("react/jsx-runtime").JSX.Element;
5
+ export declare function Chat({ children, className, isEmpty, scopeId, onChatDeleted, onNewChat, ...props }: ChatProps): import("react/jsx-runtime").JSX.Element;
6
6
  export declare namespace Chat {
7
7
  var Prompt: import("react").ForwardRefExoticComponent<import("./Chat.types").ChatPromptProps & import("react").RefAttributes<import(".").ChatPromptComposerHandle>>;
8
8
  var Message: typeof ChatMessage;
@@ -113,4 +113,6 @@ export interface ChatProps extends HTMLAttributes<HTMLDivElement> {
113
113
  /** Chat context scope; when set, header shows chat selector + delete. */
114
114
  scopeId?: string;
115
115
  onChatDeleted?: (sessionId: string) => void;
116
+ /** "+ New Chat" in the selector; when omitted, starts an empty session. */
117
+ onNewChat?: () => void;
116
118
  }
@@ -1,2 +1,2 @@
1
1
  import type { ChatChromeProps } from './ChatChrome.types';
2
- export declare function ChatChrome({ showResizeHandle, resizeHandle, onClose, isEmpty, renderPresets, messages, onQuickReply, suppressedQuickReplyKeys, isLoading, scriptContinueLabel, onScriptContinue, renderMessageChart, showSyntheticBranchButtons, unusedBranchKeys, showInlinePresets, isLastMessageFromUser, scrollRef, effectiveScopeId, onPromptSubmit, onChatDeleted, promptPrefill, footerClassName, emptyState, allowedAttachments, allowPdfAttachments, onAttachmentsDropped, slashCommandItems, onSlashItemCommand, promptPlaceholder, }: ChatChromeProps): import("react/jsx-runtime").JSX.Element;
2
+ export declare function ChatChrome({ showResizeHandle, resizeHandle, onClose, isEmpty, renderPresets, messages, onQuickReply, suppressedQuickReplyKeys, isLoading, scriptContinueLabel, onScriptContinue, renderMessageChart, showSyntheticBranchButtons, unusedBranchKeys, showInlinePresets, isLastMessageFromUser, scrollRef, effectiveScopeId, onPromptSubmit, onChatDeleted, onNewChat, promptPrefill, footerClassName, emptyState, allowedAttachments, allowPdfAttachments, onAttachmentsDropped, slashCommandItems, onSlashItemCommand, promptPlaceholder, }: ChatChromeProps): import("react/jsx-runtime").JSX.Element;
@@ -34,6 +34,8 @@ export interface ChatChromeProps {
34
34
  effectiveScopeId: string;
35
35
  onPromptSubmit: (message: string, attachments?: ChatAttachmentDropItem[]) => void | Promise<void>;
36
36
  onChatDeleted: (sessionId: string) => void;
37
+ /** "+ New Chat" in the header selector; omit for default empty session. */
38
+ onNewChat?: () => void;
37
39
  /** `?prompt=` deep link text for one-shot composer pre-fill. */
38
40
  promptPrefill?: string | null;
39
41
  footerClassName?: string;
@@ -2,5 +2,7 @@ export interface ChatSelectorProps {
2
2
  id: string;
3
3
  className?: string;
4
4
  onChatDeleted?: (sessionId: string) => void;
5
+ /** When set, used for "+ New Chat" instead of the default empty `newChat()`. */
6
+ onNewChat?: () => void;
5
7
  }
6
- export declare function ChatSelector({ id, className, onChatDeleted, }: ChatSelectorProps): import("react/jsx-runtime").JSX.Element;
8
+ export declare function ChatSelector({ id, className, onChatDeleted, onNewChat, }: ChatSelectorProps): import("react/jsx-runtime").JSX.Element;
@@ -1,7 +1,7 @@
1
1
  import { type UseChatPanelChromeModelInput } from './useChatPanelChromeModel';
2
2
  export type ChatSheetActions = {
3
3
  open: () => void;
4
- /** Starts a new chat session and opens the panel (same as the new-chat keyboard shortcut). */
4
+ /** Starts an empty chat session and opens the panel (same as Cmd/Ctrl+0). */
5
5
  openNewChat: () => void;
6
6
  /** Starts a new chat, opens the panel, and pre-fills the composer (user sends manually). */
7
7
  openNewChatWithPrefill: (prompt: string) => void;
@@ -26,6 +26,11 @@ export type UseChatPanelChromeModelInput = {
26
26
  slashCommandItems?: SlashCommandItem[];
27
27
  /** Custom slash command handler (palette pick or Enter on `/id`). */
28
28
  onSlashItemCommand?: SlashOnItemCommand;
29
+ /**
30
+ * When true, "+ New Chat" in the selector seeds from the current session.
31
+ * Keyboard shortcut and other new-chat entry points always start empty.
32
+ */
33
+ copyHistoryOnNewChat?: boolean;
29
34
  /** Override or extend the default send payload (e.g. api vs display text split). */
30
35
  transformSendPayload?: (message: string, attachments: ChatAttachmentDropItem[] | undefined, defaultPayload: string | ChatSendMessagePayload) => string | ChatSendMessagePayload | Promise<string | ChatSendMessagePayload>;
31
36
  };
@@ -39,4 +44,4 @@ export type UseChatPanelChromeModelResult = {
39
44
  openNewChatWithPrefill: (prompt: string) => void;
40
45
  chatPanelContainer: HTMLElement | null;
41
46
  };
42
- export declare function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onScriptComplete, renderMessageChart, emptyState, allowedAttachments, allowPdfAttachments, onAttachmentsDropped, slashCommandItems, onSlashItemCommand, transformSendPayload, }: UseChatPanelChromeModelInput): UseChatPanelChromeModelResult;
47
+ export declare function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onScriptComplete, renderMessageChart, emptyState, allowedAttachments, allowPdfAttachments, onAttachmentsDropped, slashCommandItems, onSlashItemCommand, copyHistoryOnNewChat, transformSendPayload, }: UseChatPanelChromeModelInput): UseChatPanelChromeModelResult;
@@ -13,9 +13,13 @@ export type UpdateChatMessagePatch = {
13
13
  text?: string;
14
14
  inProgress?: boolean;
15
15
  };
16
+ export type NewChatOptions = {
17
+ /** When set, seeds the new session's messages (e.g. continue dialog in reports). */
18
+ seedMessages?: readonly Message[];
19
+ };
16
20
  export interface ChatContextType {
17
21
  /** Returns the new session id, or undefined if no user / not created. */
18
- newChat: (scopeId: string) => string | undefined;
22
+ newChat: (scopeId: string, options?: NewChatOptions) => string | undefined;
19
23
  setCurrentChatId: (currScopeId: string, sessionId: string) => void;
20
24
  addMessage: (scopeId: string, chatId: string, role: MessageRole, text: string, options?: AddChatMessageOptions) => string | undefined;
21
25
  removeMessageById: (scopeId: string, chatId: string, messageId: string) => void;
@@ -60,7 +64,7 @@ export declare function useChatsForScopeId(scopeId: string): {
60
64
  currentChatId: string;
61
65
  isOutboundPending: boolean;
62
66
  setCurrentChatId: (targetId: string) => void;
63
- newChat: () => string;
67
+ newChat: (options?: NewChatOptions) => string;
64
68
  addMessage: (chatId: string, role: MessageRole, text: string, options?: AddChatMessageOptions) => string;
65
69
  removeMessageById: (chatId: string, messageId: string) => void;
66
70
  updateMessageById: (chatId: string, messageId: string, patch: UpdateChatMessagePatch) => void;
@@ -74,7 +78,7 @@ export declare function useChatsForDataset(scopeId: string): {
74
78
  currentChatId: string;
75
79
  isOutboundPending: boolean;
76
80
  setCurrentChatId: (targetId: string) => void;
77
- newChat: () => string;
81
+ newChat: (options?: NewChatOptions) => string;
78
82
  addMessage: (chatId: string, role: MessageRole, text: string, options?: AddChatMessageOptions) => string;
79
83
  removeMessageById: (chatId: string, messageId: string) => void;
80
84
  updateMessageById: (chatId: string, messageId: string, patch: UpdateChatMessagePatch) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sybilion/uilib",
3
- "version": "1.3.71",
3
+ "version": "1.3.73",
4
4
  "description": "Sybilion Design System — React UI components (Webpack + Stylus)",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -15,13 +15,18 @@ export function Chat({
15
15
  isEmpty,
16
16
  scopeId,
17
17
  onChatDeleted,
18
+ onNewChat,
18
19
  ...props
19
20
  }: ChatProps) {
20
21
  return (
21
22
  <div className={cn(S.root, className, isEmpty && S.isEmpty)} {...props}>
22
23
  {scopeId ? (
23
24
  <div className={S.header}>
24
- <ChatSelector id={scopeId} onChatDeleted={onChatDeleted} />
25
+ <ChatSelector
26
+ id={scopeId}
27
+ onChatDeleted={onChatDeleted}
28
+ onNewChat={onNewChat}
29
+ />
25
30
  </div>
26
31
  ) : null}
27
32
  {children}
@@ -127,4 +127,6 @@ export interface ChatProps extends HTMLAttributes<HTMLDivElement> {
127
127
  /** Chat context scope; when set, header shows chat selector + delete. */
128
128
  scopeId?: string;
129
129
  onChatDeleted?: (sessionId: string) => void;
130
+ /** "+ New Chat" in the selector; when omitted, starts an empty session. */
131
+ onNewChat?: () => void;
130
132
  }
@@ -88,7 +88,7 @@
88
88
  &::after
89
89
  content ''
90
90
  display block
91
- height 170px
91
+ height 260px
92
92
 
93
93
 
94
94
  // ---------------
@@ -45,6 +45,7 @@ export function ChatChrome({
45
45
  effectiveScopeId,
46
46
  onPromptSubmit,
47
47
  onChatDeleted,
48
+ onNewChat,
48
49
  promptPrefill,
49
50
  footerClassName,
50
51
  emptyState,
@@ -174,6 +175,7 @@ export function ChatChrome({
174
175
  isEmpty={isEmpty}
175
176
  scopeId={effectiveScopeId}
176
177
  onChatDeleted={onChatDeleted}
178
+ onNewChat={onNewChat}
177
179
  >
178
180
  {isEmpty ? (
179
181
  <>
@@ -47,6 +47,8 @@ export interface ChatChromeProps {
47
47
  attachments?: ChatAttachmentDropItem[],
48
48
  ) => void | Promise<void>;
49
49
  onChatDeleted: (sessionId: string) => void;
50
+ /** "+ New Chat" in the header selector; omit for default empty session. */
51
+ onNewChat?: () => void;
50
52
  /** `?prompt=` deep link text for one-shot composer pre-fill. */
51
53
  promptPrefill?: string | null;
52
54
  footerClassName?: string;
@@ -18,19 +18,26 @@ export interface ChatSelectorProps {
18
18
  id: string;
19
19
  className?: string;
20
20
  onChatDeleted?: (sessionId: string) => void;
21
+ /** When set, used for "+ New Chat" instead of the default empty `newChat()`. */
22
+ onNewChat?: () => void;
21
23
  }
22
24
 
23
25
  export function ChatSelector({
24
26
  id,
25
27
  className,
26
28
  onChatDeleted,
29
+ onNewChat,
27
30
  }: ChatSelectorProps) {
28
31
  const { chats, currentChatId, setCurrentChatId, newChat, deleteChat } =
29
32
  useChatsForScopeId(id);
30
33
 
31
34
  const handleValueChange = (value: string) => {
32
35
  if (value === 'new') {
33
- newChat();
36
+ if (onNewChat) {
37
+ onNewChat();
38
+ } else {
39
+ newChat();
40
+ }
34
41
  } else if (value) {
35
42
  setCurrentChatId(value);
36
43
  }
@@ -9,7 +9,7 @@ import {
9
9
 
10
10
  export type ChatSheetActions = {
11
11
  open: () => void;
12
- /** Starts a new chat session and opens the panel (same as the new-chat keyboard shortcut). */
12
+ /** Starts an empty chat session and opens the panel (same as Cmd/Ctrl+0). */
13
13
  openNewChat: () => void;
14
14
  /** Starts a new chat, opens the panel, and pre-fills the composer (user sends manually). */
15
15
  openNewChatWithPrefill: (prompt: string) => void;
@@ -86,6 +86,11 @@ export type UseChatPanelChromeModelInput = {
86
86
  slashCommandItems?: SlashCommandItem[];
87
87
  /** Custom slash command handler (palette pick or Enter on `/id`). */
88
88
  onSlashItemCommand?: SlashOnItemCommand;
89
+ /**
90
+ * When true, "+ New Chat" in the selector seeds from the current session.
91
+ * Keyboard shortcut and other new-chat entry points always start empty.
92
+ */
93
+ copyHistoryOnNewChat?: boolean;
89
94
  /** Override or extend the default send payload (e.g. api vs display text split). */
90
95
  transformSendPayload?: (
91
96
  message: string,
@@ -125,7 +130,7 @@ type PresetScriptState = {
125
130
  };
126
131
 
127
132
  const SCRIPT_STEP_DELAY_MS = 1200;
128
- const CHAT_NEW_SHORTCUT_KEY = 'o';
133
+ const CHAT_NEW_SHORTCUT_KEY = '0';
129
134
 
130
135
  const CHAT_QUERY_PARAM = 'chat';
131
136
  const CHAT_OPEN_VALUE = 'open';
@@ -144,6 +149,7 @@ export function useChatPanelChromeModel({
144
149
  onAttachmentsDropped,
145
150
  slashCommandItems,
146
151
  onSlashItemCommand,
152
+ copyHistoryOnNewChat = false,
147
153
  transformSendPayload,
148
154
  }: UseChatPanelChromeModelInput): UseChatPanelChromeModelResult {
149
155
  const effectiveScopeId = scopeId ?? NO_SCOPE_FALLBACK;
@@ -337,9 +343,44 @@ export function useChatPanelChromeModel({
337
343
 
338
344
  const isEmpty = isChatEmpty(chat) && !isLoading;
339
345
 
346
+ const startEmptyNewChat = useCallback(() => {
347
+ const priorChatId = currentChatId;
348
+ const sessionId = newChat();
349
+ if (sessionId && priorChatId) {
350
+ endLocalDemoFlow(priorChatId);
351
+ }
352
+ if (sessionId) {
353
+ setPromptLinkPrefill(null);
354
+ }
355
+ return sessionId;
356
+ }, [newChat, currentChatId, endLocalDemoFlow]);
357
+
358
+ const startNewChatFromSelector = useCallback(() => {
359
+ const priorChatId = currentChatId;
360
+ const messages = chat?.messages ?? [];
361
+ const sessionId = newChat(
362
+ copyHistoryOnNewChat && messages.length > 0
363
+ ? { seedMessages: messages }
364
+ : undefined,
365
+ );
366
+ if (sessionId && priorChatId) {
367
+ endLocalDemoFlow(priorChatId);
368
+ }
369
+ if (sessionId) {
370
+ setPromptLinkPrefill(null);
371
+ }
372
+ return sessionId;
373
+ }, [
374
+ newChat,
375
+ copyHistoryOnNewChat,
376
+ chat?.messages,
377
+ currentChatId,
378
+ endLocalDemoFlow,
379
+ ]);
380
+
340
381
  const openNewChatWithPrefill = useCallback(
341
382
  (prompt: string) => {
342
- const sessionId = newChat();
383
+ const sessionId = startEmptyNewChat();
343
384
  if (sessionId == null) {
344
385
  logger.warn('Chat prefill: sign in to use the assistant.');
345
386
  return;
@@ -349,7 +390,7 @@ export function useChatPanelChromeModel({
349
390
  setPromptLinkPrefill(trimmed || null);
350
391
  onOpenChange(true);
351
392
  },
352
- [newChat, onOpenChange],
393
+ [startEmptyNewChat, onOpenChange],
353
394
  );
354
395
 
355
396
  /**
@@ -381,7 +422,7 @@ export function useChatPanelChromeModel({
381
422
  const needsFirstSession = chats.length === 0;
382
423
 
383
424
  if (!isEmpty || needsFirstSession) {
384
- const sessionId = newChat();
425
+ const sessionId = startEmptyNewChat();
385
426
  if (sessionId == null) {
386
427
  logger.warn('Chat prompt link: sign in to use the assistant.');
387
428
  mutateSearchParams(n => {
@@ -408,15 +449,15 @@ export function useChatPanelChromeModel({
408
449
  callback: (e: Event) => {
409
450
  const ke = e as KeyboardEvent;
410
451
  if (
411
- ke.key.toLowerCase() !== CHAT_NEW_SHORTCUT_KEY ||
452
+ ke.key !== CHAT_NEW_SHORTCUT_KEY ||
412
453
  !(ke.metaKey || ke.ctrlKey) ||
413
- !ke.shiftKey ||
454
+ ke.shiftKey ||
414
455
  ke.altKey
415
456
  ) {
416
457
  return;
417
458
  }
418
459
  ke.preventDefault();
419
- newChat();
460
+ startEmptyNewChat();
420
461
  onOpenChange(true);
421
462
  },
422
463
  });
@@ -1192,6 +1233,7 @@ export function useChatPanelChromeModel({
1192
1233
  effectiveScopeId,
1193
1234
  onPromptSubmit: onPromptSubmitWithSlashCommands,
1194
1235
  onChatDeleted: endLocalDemoFlow,
1236
+ onNewChat: startNewChatFromSelector,
1195
1237
  promptPrefill: promptLinkPrefill,
1196
1238
  emptyState: resolvedEmptyState,
1197
1239
  allowedAttachments,
@@ -1214,7 +1256,7 @@ export function useChatPanelChromeModel({
1214
1256
  isOpen,
1215
1257
  onOpenChange,
1216
1258
  toggleOpen,
1217
- newChat,
1259
+ newChat: startEmptyNewChat,
1218
1260
  openNewChatWithPrefill,
1219
1261
  chatPanelContainer,
1220
1262
  };
@@ -45,9 +45,14 @@ export type UpdateChatMessagePatch = {
45
45
  inProgress?: boolean;
46
46
  };
47
47
 
48
+ export type NewChatOptions = {
49
+ /** When set, seeds the new session's messages (e.g. continue dialog in reports). */
50
+ seedMessages?: readonly Message[];
51
+ };
52
+
48
53
  export interface ChatContextType {
49
54
  /** Returns the new session id, or undefined if no user / not created. */
50
- newChat: (scopeId: string) => string | undefined;
55
+ newChat: (scopeId: string, options?: NewChatOptions) => string | undefined;
51
56
  setCurrentChatId: (currScopeId: string, sessionId: string) => void;
52
57
  addMessage: (
53
58
  scopeId: string,
@@ -173,6 +178,22 @@ function addScopeIdToRegistry(scopeId: string) {
173
178
  }
174
179
  }
175
180
 
181
+ /** Shallow-clone messages for seeding another session; drops in-progress rows. */
182
+ function cloneMessagesForNewSession(messages: readonly Message[]): Message[] {
183
+ return messages
184
+ .filter(message => !message.inProgress)
185
+ .map(message => ({
186
+ ...message,
187
+ ...(message.userTextFileAttachments
188
+ ? {
189
+ userTextFileAttachments: message.userTextFileAttachments.map(
190
+ attachment => ({ ...attachment }),
191
+ ),
192
+ }
193
+ : {}),
194
+ }));
195
+ }
196
+
176
197
  export interface ChatProviderProps {
177
198
  children: ReactNode;
178
199
  /** When null, chat state is cleared (logged out). When set, only LS rows for scopes starting with `${userId}-` are loaded. */
@@ -249,15 +270,19 @@ export function ChatProvider({
249
270
  );
250
271
 
251
272
  const newChat = useCallback(
252
- (scopeId: string): string | undefined => {
273
+ (scopeId: string, options?: NewChatOptions): string | undefined => {
253
274
  if (userSwitchKey === null) return undefined;
254
275
  addScopeIdToRegistry(scopeId);
255
276
 
256
277
  const sessionId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
278
+ const seededMessages =
279
+ options?.seedMessages && options.seedMessages.length > 0
280
+ ? cloneMessagesForNewSession(options.seedMessages)
281
+ : [];
257
282
  const newChat: Chat = {
258
283
  session_id: sessionId,
259
284
  name: '',
260
- messages: [],
285
+ messages: seededMessages,
261
286
  };
262
287
 
263
288
  setChats(prev => {
@@ -672,7 +697,7 @@ export function useChatsForScopeId(scopeId: string) {
672
697
  currentChatId,
673
698
  isOutboundPending,
674
699
  setCurrentChatId: (targetId: string) => setCurrentChatId(scopeId, targetId),
675
- newChat: () => newChat(scopeId),
700
+ newChat: (options?: NewChatOptions) => newChat(scopeId, options),
676
701
  addMessage: (
677
702
  chatId: string,
678
703
  role: MessageRole,