@sybilion/uilib 1.3.78 → 1.3.80

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 (51) hide show
  1. package/dist/esm/components/ui/Chat/Chat.js +2 -2
  2. package/dist/esm/components/ui/Chat/Chat.styl.js +1 -1
  3. package/dist/esm/components/ui/Chat/ChatChrome/ChatChrome.js +10 -6
  4. package/dist/esm/components/ui/Chat/ChatChrome/ChatChrome.styl.js +2 -2
  5. package/dist/esm/components/ui/Chat/ChatEmptyState/ChatEmptyState.styl.js +1 -1
  6. package/dist/esm/components/ui/Chat/ChatMessage/ChatMessage.js +2 -2
  7. package/dist/esm/components/ui/Chat/ChatPresets/ChatPresets.styl.js +1 -1
  8. package/dist/esm/components/ui/Chat/ChatSheet/ChatSelector.js +2 -2
  9. package/dist/esm/components/ui/Chat/ChatSheet/ChatSheet.js +2 -1
  10. package/dist/esm/components/ui/Chat/ChatSheet/useChatPanelChromeModel.js +6 -5
  11. package/dist/esm/components/ui/Chat/buildChatSendMessagePayload.js +3 -1
  12. package/dist/esm/components/ui/FileChip/FileChip.js +4 -0
  13. package/dist/esm/components/widgets/DriversComparisonChart/driversComparisonChart.helpers.js +3 -1
  14. package/dist/esm/contexts/chat-context.js +51 -5
  15. package/dist/esm/index.js +1 -0
  16. package/dist/esm/types/src/components/ui/Chat/Chat.d.ts +1 -1
  17. package/dist/esm/types/src/components/ui/Chat/Chat.types.d.ts +12 -0
  18. package/dist/esm/types/src/components/ui/Chat/ChatChrome/ChatChrome.d.ts +1 -1
  19. package/dist/esm/types/src/components/ui/Chat/ChatChrome/ChatChrome.types.d.ts +4 -0
  20. package/dist/esm/types/src/components/ui/Chat/ChatMessage/ChatMessage.d.ts +1 -1
  21. package/dist/esm/types/src/components/ui/Chat/ChatSheet/ChatSelector.d.ts +2 -1
  22. package/dist/esm/types/src/components/ui/Chat/ChatSheet/ChatSheet.d.ts +1 -1
  23. package/dist/esm/types/src/components/ui/Chat/ChatSheet/useChatPanelChromeModel.d.ts +5 -3
  24. package/dist/esm/types/src/components/ui/Chat/index.d.ts +3 -1
  25. package/dist/esm/types/src/components/ui/FileChip/FileChip.types.d.ts +1 -1
  26. package/dist/esm/types/src/contexts/chat-context.d.ts +16 -4
  27. package/package.json +1 -1
  28. package/src/components/ui/Chat/Chat.styl +4 -1
  29. package/src/components/ui/Chat/Chat.tsx +2 -1
  30. package/src/components/ui/Chat/Chat.types.ts +14 -0
  31. package/src/components/ui/Chat/ChatChrome/ChatChrome.styl +20 -10
  32. package/src/components/ui/Chat/ChatChrome/ChatChrome.styl.d.ts +2 -0
  33. package/src/components/ui/Chat/ChatChrome/ChatChrome.tsx +27 -10
  34. package/src/components/ui/Chat/ChatChrome/ChatChrome.types.ts +4 -0
  35. package/src/components/ui/Chat/ChatEmptyState/ChatEmptyState.styl +1 -2
  36. package/src/components/ui/Chat/ChatMessage/ChatMessage.tsx +9 -1
  37. package/src/components/ui/Chat/ChatPresets/ChatPresets.styl +5 -4
  38. package/src/components/ui/Chat/ChatSheet/ChatSelector.tsx +3 -1
  39. package/src/components/ui/Chat/ChatSheet/ChatSheet.tsx +2 -0
  40. package/src/components/ui/Chat/ChatSheet/useChatPanelChromeModel.tsx +24 -5
  41. package/src/components/ui/Chat/buildChatSendMessagePayload.ts +2 -1
  42. package/src/components/ui/Chat/index.ts +4 -0
  43. package/src/components/ui/FileChip/FileChip.tsx +11 -0
  44. package/src/components/ui/FileChip/FileChip.types.ts +1 -1
  45. package/src/components/widgets/DriversComparisonChart/driversComparisonChart.helpers.test.ts +13 -4
  46. package/src/components/widgets/DriversComparisonChart/driversComparisonChart.helpers.ts +2 -1
  47. package/src/contexts/chat-context.tsx +80 -6
  48. package/src/docs/pages/ChatPage.styl +6 -0
  49. package/src/docs/pages/ChatPage.styl.d.ts +7 -0
  50. package/src/docs/pages/ChatPage.tsx +30 -87
  51. package/src/docs/pages/ChatSlashCommandsPage.tsx +4 -4
@@ -1,5 +1,5 @@
1
1
  import { ReactNode } from 'react';
2
- import { type Chat, type ChatSendMessagePayload, type Message, MessageRole, type UserTextFileAttachment } from '#uilib/components/ui/Chat/Chat.types';
2
+ import { type Chat, type ChatMeta, type ChatSendMessagePayload, type Message, MessageRole, type UserTextFileAttachment } from '#uilib/components/ui/Chat/Chat.types';
3
3
  import type { ChatResponse } from '#uilib/types/chat-api.types';
4
4
  export type SendChatMessageFn = (message: string, targetChatId: string) => Promise<ChatResponse>;
5
5
  export type { ChatSendMessagePayload, UserTextFileAttachment, } from '#uilib/components/ui/Chat/Chat.types';
@@ -7,16 +7,22 @@ export type AddChatMessageOptions = {
7
7
  userTextFileAttachments?: UserTextFileAttachment[];
8
8
  /** SYSTEM-only: mark as transient progress placeholder (shimmer until removed). */
9
9
  inProgress?: boolean;
10
+ meta?: ChatMeta;
10
11
  };
11
12
  export type UpdateChatMessagePatch = {
12
13
  role?: MessageRole;
13
14
  text?: string;
14
15
  inProgress?: boolean;
16
+ meta?: ChatMeta;
15
17
  };
16
18
  export type NewChatOptions = {
17
19
  /** When set, seeds the new session's messages (e.g. continue dialog in reports). */
18
20
  seedMessages?: readonly Message[];
19
21
  };
22
+ export type SendMessageResult = {
23
+ response: string;
24
+ sessionId: string;
25
+ };
20
26
  export interface ChatContextType {
21
27
  /** Returns the new session id, or undefined if no user / not created. */
22
28
  newChat: (scopeId: string, options?: NewChatOptions) => string | undefined;
@@ -26,7 +32,9 @@ export interface ChatContextType {
26
32
  updateMessageById: (scopeId: string, chatId: string, messageId: string, patch: UpdateChatMessagePatch) => void;
27
33
  /** Replaces all messages on a session (e.g. seeding from another chat). */
28
34
  setChatMessages: (scopeId: string, chatId: string, messages: Message[]) => void;
29
- sendMessage: (scopeId: string, message: string | ChatSendMessagePayload, chatId?: string) => Promise<string>;
35
+ /** Shallow-merge keys into `chat.meta` and persist. */
36
+ updateChatMeta: (scopeId: string, chatId: string, patch: ChatMeta) => void;
37
+ sendMessage: (scopeId: string, message: string | ChatSendMessagePayload, chatId?: string) => Promise<SendMessageResult>;
30
38
  getChatsForScopeId: (scopeId: string) => Chat[];
31
39
  getCurrentChatId: (scopeId: string) => string | null;
32
40
  deleteChat: (scopeId: string, sessionId: string) => void;
@@ -68,7 +76,9 @@ export declare function useChatsForScopeId(scopeId: string): {
68
76
  addMessage: (chatId: string, role: MessageRole, text: string, options?: AddChatMessageOptions) => string;
69
77
  removeMessageById: (chatId: string, messageId: string) => void;
70
78
  updateMessageById: (chatId: string, messageId: string, patch: UpdateChatMessagePatch) => void;
71
- sendMessage: (message: string | ChatSendMessagePayload, chatId?: string) => Promise<string>;
79
+ sendMessage: (message: string | ChatSendMessagePayload, chatId?: string) => Promise<SendMessageResult>;
80
+ updateChatMeta: (chatId: string, patch: ChatMeta) => void;
81
+ setChatMessages: (chatId: string, messages: Message[]) => void;
72
82
  deleteChat: (sessionId: string) => void;
73
83
  };
74
84
  /** @deprecated Use useChatsForScopeId */
@@ -82,7 +92,9 @@ export declare function useChatsForDataset(scopeId: string): {
82
92
  addMessage: (chatId: string, role: MessageRole, text: string, options?: AddChatMessageOptions) => string;
83
93
  removeMessageById: (chatId: string, messageId: string) => void;
84
94
  updateMessageById: (chatId: string, messageId: string, patch: UpdateChatMessagePatch) => void;
85
- sendMessage: (message: string | ChatSendMessagePayload, chatId?: string) => Promise<string>;
95
+ sendMessage: (message: string | ChatSendMessagePayload, chatId?: string) => Promise<SendMessageResult>;
96
+ updateChatMeta: (chatId: string, patch: ChatMeta) => void;
97
+ setChatMessages: (chatId: string, messages: Message[]) => void;
86
98
  deleteChat: (sessionId: string) => void;
87
99
  };
88
100
  export declare function useCurrentChat(scopeId: string): Chat;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sybilion/uilib",
3
- "version": "1.3.78",
3
+ "version": "1.3.80",
4
4
  "description": "Sybilion Design System — React UI components (Webpack + Stylus)",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -1,6 +1,7 @@
1
1
  .root
2
2
  display flex
3
3
  flex-direction column
4
+ flex 1
4
5
  height 100%
5
6
  min-height 0
6
7
  // max-height 100vh
@@ -15,4 +16,6 @@
15
16
  padding-right var(--p-12) // to not overlap with ChatSheet close button
16
17
 
17
18
  .isEmpty
18
- padding-bottom 170px // goes under prompt
19
+ flex 1
20
+ min-height 0
21
+ overflow hidden
@@ -16,11 +16,12 @@ export function Chat({
16
16
  scopeId,
17
17
  onChatDeleted,
18
18
  onNewChat,
19
+ hideChatSelector = false,
19
20
  ...props
20
21
  }: ChatProps) {
21
22
  return (
22
23
  <div className={cn(S.root, className, isEmpty && S.isEmpty)} {...props}>
23
- {scopeId ? (
24
+ {scopeId && !hideChatSelector ? (
24
25
  <div className={S.header}>
25
26
  <ChatSelector
26
27
  id={scopeId}
@@ -29,8 +29,14 @@ export type ChatSendMessagePayload = {
29
29
  omitUserMessage?: boolean;
30
30
  /** Shimmer label while waiting on the API; defaults to "Thinking...". */
31
31
  loadingLabel?: string;
32
+ /** When set, show as a SYSTEM inProgress bubble instead of the footer shimmer. */
33
+ systemProgressLabel?: string;
32
34
  };
33
35
 
36
+ export type ChatMetaValue = string | number | boolean | null;
37
+
38
+ export type ChatMeta = Record<string, ChatMetaValue>;
39
+
34
40
  export interface Message {
35
41
  id: string;
36
42
  role: MessageRole;
@@ -39,12 +45,14 @@ export interface Message {
39
45
  userTextFileAttachments?: UserTextFileAttachment[];
40
46
  /** SYSTEM-only: transient progress placeholder while work is in flight. */
41
47
  inProgress?: boolean;
48
+ meta?: ChatMeta;
42
49
  }
43
50
 
44
51
  export interface Chat {
45
52
  session_id: string;
46
53
  name: string;
47
54
  messages: Message[];
55
+ meta?: ChatMeta;
48
56
  }
49
57
 
50
58
  export interface ChatPreset {
@@ -124,6 +132,10 @@ export interface ChatMessageProps {
124
132
  onScriptContinue?: () => void;
125
133
  /** Renders `[CHART]` placeholder in assistant messages (app supplies dataset chart). */
126
134
  renderMessageChart?: () => ReactNode;
135
+ /** Full message for system-role render delegation. */
136
+ message?: Message;
137
+ /** When set, SYSTEM messages render via this callback instead of plain text. */
138
+ renderSystemMessage?: (message: Message) => ReactNode;
127
139
  }
128
140
 
129
141
  export interface ChatProps extends HTMLAttributes<HTMLDivElement> {
@@ -135,4 +147,6 @@ export interface ChatProps extends HTMLAttributes<HTMLDivElement> {
135
147
  onChatDeleted?: (sessionId: string) => void;
136
148
  /** "+ New Chat" in the selector; when omitted, starts an empty session. */
137
149
  onNewChat?: () => void;
150
+ /** When true, skip built-in header ChatSelector (e.g. external page-header slot). */
151
+ hideChatSelector?: boolean;
138
152
  }
@@ -83,29 +83,39 @@
83
83
  .scrollInner
84
84
  padding-top var(--p-10)
85
85
  min-height 100%
86
- // padding-bottom 320px // goes under prompt
87
86
 
88
- &::after
89
- content ''
90
- display block
91
- height 260px
87
+ .emptyBody
88
+ flex 1
89
+ min-height 0
90
+ display flex
91
+ flex-direction column
92
+ overflow hidden
92
93
 
94
+ & > *
95
+ flex 1
96
+ min-height 0
97
+ overflow auto
93
98
 
94
99
  // ---------------
95
100
 
96
101
  .footer
102
+ position relative
97
103
  z-index 50
98
104
  display flex
99
105
  flex-direction column
100
-
101
- position absolute
102
- bottom 0
103
- width 100%;
106
+ flex-shrink 0
107
+ width 100%
104
108
 
105
109
  backdrop-filter blur(30px)
106
110
  background-color var(--background-alpha-800)
107
111
  border-top 1px solid var(--border)
108
- box-shadow 0 0 20px 16px var(--background)
112
+ box-shadow 0 8px 24px 0 var(--background)
113
+
114
+ .fixedPresets
115
+ position relative
116
+ z-index 10
117
+ flex-shrink 0
118
+ width 100%
109
119
 
110
120
  .notice
111
121
  position absolute
@@ -6,6 +6,8 @@ interface CssExports {
6
6
  'branchRow': string;
7
7
  'chatResizeHandle': string;
8
8
  'content': string;
9
+ 'emptyBody': string;
10
+ 'fixedPresets': string;
9
11
  'footer': string;
10
12
  'loader': string;
11
13
  'notice': string;
@@ -14,7 +14,7 @@ import { DropZone } from '../../DropZone/DropZone';
14
14
  import { PanelResizeHandle } from '../../Sidebar/Sidebar';
15
15
  import SidebarStem from '../../Sidebar/Sidebar.styl';
16
16
  import { Chat } from '../Chat';
17
- import type { ChatAttachmentDropItem } from '../Chat.types';
17
+ import { type ChatAttachmentDropItem, MessageRole } from '../Chat.types';
18
18
  import {
19
19
  buildAcceptAttr,
20
20
  filterToTextAttachments,
@@ -38,6 +38,7 @@ export function ChatChrome({
38
38
  scriptContinueLabel,
39
39
  onScriptContinue,
40
40
  renderMessageChart,
41
+ renderSystemMessage,
41
42
  showSyntheticBranchButtons,
42
43
  unusedBranchKeys,
43
44
  showInlinePresets,
@@ -56,6 +57,7 @@ export function ChatChrome({
56
57
  slashCommandItems,
57
58
  onSlashItemCommand,
58
59
  promptPlaceholder,
60
+ hideChatSelector = false,
59
61
  }: ChatChromeProps) {
60
62
  const filteredAllowedAttachments = useMemo(
61
63
  () => filterToTextAttachments(allowedAttachments),
@@ -74,6 +76,13 @@ export function ChatChrome({
74
76
  >([]);
75
77
  const [isExtractingAttachments, setIsExtractingAttachments] = useState(false);
76
78
  const promptDisabled = isExtractingAttachments;
79
+ const hasInProgressSystemMessage = useMemo(
80
+ () =>
81
+ messages.some(
82
+ msg => msg.role === MessageRole.SYSTEM && msg.inProgress === true,
83
+ ),
84
+ [messages],
85
+ );
77
86
 
78
87
  const handleAttachmentFiles = useCallback(
79
88
  (files: File[]) => {
@@ -186,12 +195,12 @@ export function ChatChrome({
186
195
  scopeId={effectiveScopeId}
187
196
  onChatDeleted={onChatDeleted}
188
197
  onNewChat={onNewChat}
198
+ hideChatSelector={hideChatSelector}
189
199
  >
190
200
  {isEmpty ? (
191
- <>
201
+ <div className={S.emptyBody}>
192
202
  <Chat.EmptyState {...emptyState} />
193
- {renderPresets('fixed')}
194
- </>
203
+ </div>
195
204
  ) : (
196
205
  <div className={S.scrollWrapper}>
197
206
  <Scroll
@@ -199,7 +208,7 @@ export function ChatChrome({
199
208
  yScrollbarClassName={S.scrollbar}
200
209
  className={S.scroll}
201
210
  innerClassName={S.scrollInner}
202
- offset={{ y: { before: 56, after: 180 } }}
211
+ offset={{ y: { before: 56, after: 24 } }}
203
212
  fadeSize="m"
204
213
  autoHide
205
214
  ref={scrollRef}
@@ -209,6 +218,7 @@ export function ChatChrome({
209
218
  return (
210
219
  <Chat.Message
211
220
  key={msg.id}
221
+ message={msg}
212
222
  role={msg.role}
213
223
  text={msg.text}
214
224
  inProgress={msg.inProgress}
@@ -229,6 +239,7 @@ export function ChatChrome({
229
239
  : undefined
230
240
  }
231
241
  renderMessageChart={renderMessageChart}
242
+ renderSystemMessage={renderSystemMessage}
232
243
  />
233
244
  );
234
245
  })}
@@ -259,15 +270,21 @@ export function ChatChrome({
259
270
 
260
271
  {showInlinePresets && renderPresets('inline')}
261
272
 
262
- {isLoading && (isLastMessageFromUser || loadingLabel) && (
263
- <TextShimmer duration={1} spread={5} className={S.loader}>
264
- {loadingLabel ?? 'Thinking...'}
265
- </TextShimmer>
266
- )}
273
+ {isLoading &&
274
+ !hasInProgressSystemMessage &&
275
+ (isLastMessageFromUser || loadingLabel) && (
276
+ <TextShimmer duration={1} spread={5} className={S.loader}>
277
+ {loadingLabel ?? 'Thinking...'}
278
+ </TextShimmer>
279
+ )}
267
280
  </Scroll>
268
281
  </div>
269
282
  )}
270
283
 
284
+ {isEmpty ? (
285
+ <div className={S.fixedPresets}>{renderPresets('fixed')}</div>
286
+ ) : null}
287
+
271
288
  <div className={cn(S.footer, footerClassName)}>
272
289
  {isEmpty ? (
273
290
  <div className={S.notice}>
@@ -38,6 +38,8 @@ export interface ChatChromeProps {
38
38
  scriptContinueLabel: string | undefined;
39
39
  onScriptContinue: (() => void) | undefined;
40
40
  renderMessageChart?: () => React.ReactNode;
41
+ /** When set, SYSTEM messages render via this callback instead of plain text. */
42
+ renderSystemMessage?: (message: Message) => React.ReactNode;
41
43
  showSyntheticBranchButtons: boolean;
42
44
  unusedBranchKeys: string[];
43
45
  showInlinePresets: boolean;
@@ -69,4 +71,6 @@ export interface ChatChromeProps {
69
71
  onSlashItemCommand?: SlashOnItemCommand;
70
72
  /** Composer placeholder forwarded to `Chat.Prompt`. */
71
73
  promptPlaceholder?: string;
74
+ /** When true, skip built-in header ChatSelector (e.g. external page-header slot). */
75
+ hideChatSelector?: boolean;
72
76
  }
@@ -4,9 +4,8 @@
4
4
  align-items center
5
5
  justify-content center
6
6
  gap var(--p-10)
7
-
8
- // height 450px
9
7
  flex 1
8
+ min-height 0
10
9
  padding var(--p-6)
11
10
 
12
11
  text-align center
@@ -22,6 +22,8 @@ export function ChatMessage({
22
22
  scriptContinue,
23
23
  onScriptContinue,
24
24
  renderMessageChart,
25
+ message,
26
+ renderSystemMessage,
25
27
  }: ChatMessageProps) {
26
28
  const fileAttachments = userTextFileAttachmentsFromMessage({
27
29
  userTextFileAttachments,
@@ -33,7 +35,13 @@ export function ChatMessage({
33
35
  <div className={cn(S.root, S[`role-${role}`])}>
34
36
  {isSystem ? (
35
37
  <div className={S.text}>
36
- {inProgress ? <TextShimmer as="span">{text}</TextShimmer> : text}
38
+ {inProgress ? (
39
+ <TextShimmer as="span">{text}</TextShimmer>
40
+ ) : renderSystemMessage && message ? (
41
+ renderSystemMessage(message)
42
+ ) : (
43
+ text
44
+ )}
37
45
  </div>
38
46
  ) : isAssistant ? (
39
47
  <AgentMessageContent
@@ -1,7 +1,7 @@
1
1
  .root
2
- // position fixed
2
+ position relative
3
+ flex-shrink 0
3
4
  width 100%
4
- bottom 160px
5
5
 
6
6
  .inlineRoot
7
7
  position relative
@@ -12,6 +12,7 @@
12
12
  display flex
13
13
  flex-wrap wrap
14
14
  gap 8px
15
+ min-width 0
15
16
  padding var(--p-2) var(--p-6) var(--p-3)
16
17
  background-color var(--background)
17
18
 
@@ -19,9 +20,9 @@
19
20
  background-color transparent
20
21
 
21
22
  .item
22
- flex-shrink 0
23
+ flex 0 1 auto
23
24
  min-width 0
24
- max-width 300px
25
+ max-width unquote('min(300px, 100%)')
25
26
  height auto
26
27
  min-height auto
27
28
  padding var(--p-3)
@@ -16,6 +16,7 @@ import S from './ChatSelector.styl';
16
16
 
17
17
  export interface ChatSelectorProps {
18
18
  id: string;
19
+ wrapperClassName?: string;
19
20
  className?: string;
20
21
  onChatDeleted?: (sessionId: string) => void;
21
22
  /** When set, used for "+ New Chat" instead of the default empty `newChat()`. */
@@ -24,6 +25,7 @@ export interface ChatSelectorProps {
24
25
 
25
26
  export function ChatSelector({
26
27
  id,
28
+ wrapperClassName,
27
29
  className,
28
30
  onChatDeleted,
29
31
  onNewChat,
@@ -65,7 +67,7 @@ export function ChatSelector({
65
67
  };
66
68
 
67
69
  return (
68
- <div className={S.wrapper}>
70
+ <div className={cn(S.wrapper, wrapperClassName)}>
69
71
  <div className={S.selectGrow}>
70
72
  <Select
71
73
  variant="clear"
@@ -44,6 +44,7 @@ export function ChatSheet({
44
44
  onMessage,
45
45
  onScriptComplete,
46
46
  renderMessageChart,
47
+ renderSystemMessage,
47
48
  emptyState,
48
49
  allowedAttachments,
49
50
  allowPdfAttachments,
@@ -60,6 +61,7 @@ export function ChatSheet({
60
61
  onMessage,
61
62
  onScriptComplete,
62
63
  renderMessageChart,
64
+ renderSystemMessage,
63
65
  emptyState,
64
66
  allowedAttachments,
65
67
  allowPdfAttachments,
@@ -3,6 +3,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
3
  import {
4
4
  ChatPreset,
5
5
  type ChatSendMessagePayload,
6
+ type Message,
6
7
  MessageRole,
7
8
  type ScriptCompletePayload,
8
9
  } from '#uilib/components/ui/Chat/Chat.types';
@@ -69,11 +70,17 @@ export type UseChatPanelChromeModelInput = {
69
70
  /** Composite chat scope (e.g. `${userId}-${datasetId}`, `${userId}-dashboard`, `${userId}-report-${reportId}`). */
70
71
  scopeId?: string | null;
71
72
  /** Fires after send; second arg is the assistant reply when the API call succeeded. */
72
- onMessage?: (displayText: string, assistantResponse?: string) => void;
73
+ onMessage?: (
74
+ displayText: string,
75
+ assistantResponse?: string,
76
+ chatSessionId?: string,
77
+ ) => void;
73
78
  /** Fires when a preset script has no further `[Label|branchKey]` steps (graph leaf or linear script end). */
74
79
  onScriptComplete?: (payload: ScriptCompletePayload) => void;
75
80
  /** Renders `[CHART]` tokens in assistant messages. */
76
81
  renderMessageChart?: () => React.ReactNode;
82
+ /** When set, SYSTEM messages render via this callback instead of plain text. */
83
+ renderSystemMessage?: (message: Message) => React.ReactNode;
77
84
  /** Forwarded to `ChatChrome` when the thread is empty. */
78
85
  emptyState?: ChatEmptyStateConfig;
79
86
  /** MIME types / extensions for text-only chat attachments (filtered by uilib allowlist). */
@@ -144,6 +151,7 @@ export function useChatPanelChromeModel({
144
151
  onMessage,
145
152
  onScriptComplete,
146
153
  renderMessageChart,
154
+ renderSystemMessage,
147
155
  emptyState,
148
156
  allowedAttachments,
149
157
  allowPdfAttachments,
@@ -557,8 +565,13 @@ export function useChatPanelChromeModel({
557
565
  }
558
566
  setOutboundLoadingLabel(loadingLabelFromSendPayload(payload));
559
567
  try {
560
- const assistantResponse = await sendMessage(payload);
561
- onMessage?.(displayTextFromSendPayload(payload), assistantResponse);
568
+ const { response: assistantResponse, sessionId } =
569
+ await sendMessage(payload);
570
+ onMessage?.(
571
+ displayTextFromSendPayload(payload),
572
+ assistantResponse,
573
+ sessionId,
574
+ );
562
575
  } finally {
563
576
  setOutboundLoadingLabel(undefined);
564
577
  }
@@ -720,8 +733,13 @@ export function useChatPanelChromeModel({
720
733
  }
721
734
  setOutboundLoadingLabel(loadingLabelFromSendPayload(payload));
722
735
  try {
723
- const assistantResponse = await sendMessage(payload);
724
- onMessage?.(displayTextFromSendPayload(payload), assistantResponse);
736
+ const { response: assistantResponse, sessionId } =
737
+ await sendMessage(payload);
738
+ onMessage?.(
739
+ displayTextFromSendPayload(payload),
740
+ assistantResponse,
741
+ sessionId,
742
+ );
725
743
  } finally {
726
744
  setOutboundLoadingLabel(undefined);
727
745
  }
@@ -1245,6 +1263,7 @@ export function useChatPanelChromeModel({
1245
1263
  scriptContinueLabel,
1246
1264
  onScriptContinue,
1247
1265
  renderMessageChart,
1266
+ renderSystemMessage,
1248
1267
  showSyntheticBranchButtons,
1249
1268
  unusedBranchKeys,
1250
1269
  showInlinePresets,
@@ -85,5 +85,6 @@ export function displayTextFromSendPayload(
85
85
  export function loadingLabelFromSendPayload(
86
86
  message: string | ChatSendMessagePayload,
87
87
  ): string | undefined {
88
- return typeof message === 'string' ? undefined : message.loadingLabel;
88
+ if (typeof message === 'string') return undefined;
89
+ return message.loadingLabel ?? message.systemProgressLabel;
89
90
  }
@@ -18,6 +18,8 @@ export {
18
18
  normalizeUserTextFileAttachments,
19
19
  } from './buildChatSendMessagePayload';
20
20
  export { sanitizeAttachmentFilename } from './sanitizeAttachmentFilename';
21
+ export { ChatSelector } from './ChatSheet/ChatSelector';
22
+ export type { ChatSelectorProps } from './ChatSheet/ChatSelector';
21
23
  export { ChatSheet } from './ChatSheet/ChatSheet';
22
24
  export { useChatPanelChromeModel } from './ChatSheet/useChatPanelChromeModel';
23
25
  export type { ChatSheetActions, ChatSheetProps } from './ChatSheet/ChatSheet';
@@ -45,6 +47,8 @@ export type {
45
47
  export type {
46
48
  Chat as ChatType,
47
49
  ChatAttachmentDropItem,
50
+ ChatMeta,
51
+ ChatMetaValue,
48
52
  ChatSendMessagePayload,
49
53
  ChatProps,
50
54
  ChatPreset as ChatPresetType,
@@ -3,6 +3,7 @@ import cn from 'classnames';
3
3
  import { CsvIcon } from '#uilib/components/icons/CsvIcon/CsvIcon';
4
4
  import { CardAction, CardHeader } from '#uilib/components/ui/Card';
5
5
  import { FileTextIcon, XIcon } from '@phosphor-icons/react';
6
+ import { LayoutDashboard } from 'lucide-react';
6
7
 
7
8
  import S from './FileChip.styl';
8
9
  import type { FileChipFormat, FileChipProps } from './FileChip.types';
@@ -14,6 +15,16 @@ function FormatIcon({ format }: { format: FileChipFormat }) {
14
15
  return <CsvIcon size={FORMAT_ICON_SIZE} />;
15
16
  }
16
17
 
18
+ if (format === 'dashboard') {
19
+ return (
20
+ <LayoutDashboard
21
+ size={FORMAT_ICON_SIZE}
22
+ aria-hidden
23
+ style={{ color: 'var(--muted-foreground)' }}
24
+ />
25
+ );
26
+ }
27
+
17
28
  return (
18
29
  <FileTextIcon
19
30
  size={FORMAT_ICON_SIZE}
@@ -1,4 +1,4 @@
1
- export type FileChipFormat = 'csv' | 'pdf' | 'text';
1
+ export type FileChipFormat = 'csv' | 'pdf' | 'text' | 'dashboard';
2
2
 
3
3
  export type FileChipProps = {
4
4
  name: string;
@@ -127,7 +127,16 @@ describe('applyDriversComparisonViewToPayload', () => {
127
127
  };
128
128
 
129
129
  it('returns original payload for lagged tab', () => {
130
- expect(applyDriversComparisonViewToPayload(payload, 'lagged')).toBe(payload);
130
+ expect(applyDriversComparisonViewToPayload(payload, 'lagged')).toBe(
131
+ payload,
132
+ );
133
+ });
134
+
135
+ it('returns original payload for overlapped tab when target series missing', () => {
136
+ const incomplete = { drivers: payload.drivers } as typeof payload;
137
+ expect(applyDriversComparisonViewToPayload(incomplete, 'overlapped')).toBe(
138
+ incomplete,
139
+ );
131
140
  });
132
141
 
133
142
  it('shifts driver series backward for overlapped tab without mutating source', () => {
@@ -327,8 +336,8 @@ describe('buildDriversComparisonChartData historical window floor', () => {
327
336
 
328
337
  expect(laggedChart[0]?.date).toBe('2014-10-01');
329
338
  expect(overlappedChart[0]?.date).toBe('2014-07-01');
330
- expect(
331
- overlappedChart[0]?.date!.localeCompare(laggedChart[0]?.date!),
332
- ).toBe(-1);
339
+ expect(overlappedChart[0]?.date!.localeCompare(laggedChart[0]?.date!)).toBe(
340
+ -1,
341
+ );
333
342
  });
334
343
  });
@@ -125,13 +125,14 @@ export function applyDriversComparisonViewToPayload(
125
125
  tab: DriversComparisonViewTab,
126
126
  ): BacktestsComponentPayload | null {
127
127
  if (!payload || tab === 'lagged') return payload;
128
+ if (!payload.target?.normalized_series) return payload;
128
129
 
129
130
  return {
130
131
  target: {
131
132
  ...payload.target,
132
133
  normalized_series: { ...payload.target.normalized_series },
133
134
  },
134
- drivers: payload.drivers.map(driver => {
135
+ drivers: (payload.drivers ?? []).map(driver => {
135
136
  const lagMonths = parseLagMonthsFromLabel(resolveDriverLagLabel(driver));
136
137
  const series = driver.normalized_series ?? {};
137
138
  const normalized_series =