@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
@@ -10,6 +10,7 @@ import {
10
10
 
11
11
  import {
12
12
  type Chat,
13
+ type ChatMeta,
13
14
  type ChatSendMessagePayload,
14
15
  type Message,
15
16
  MessageRole,
@@ -37,12 +38,14 @@ export type AddChatMessageOptions = {
37
38
  userTextFileAttachments?: UserTextFileAttachment[];
38
39
  /** SYSTEM-only: mark as transient progress placeholder (shimmer until removed). */
39
40
  inProgress?: boolean;
41
+ meta?: ChatMeta;
40
42
  };
41
43
 
42
44
  export type UpdateChatMessagePatch = {
43
45
  role?: MessageRole;
44
46
  text?: string;
45
47
  inProgress?: boolean;
48
+ meta?: ChatMeta;
46
49
  };
47
50
 
48
51
  export type NewChatOptions = {
@@ -50,6 +53,11 @@ export type NewChatOptions = {
50
53
  seedMessages?: readonly Message[];
51
54
  };
52
55
 
56
+ export type SendMessageResult = {
57
+ response: string;
58
+ sessionId: string;
59
+ };
60
+
53
61
  export interface ChatContextType {
54
62
  /** Returns the new session id, or undefined if no user / not created. */
55
63
  newChat: (scopeId: string, options?: NewChatOptions) => string | undefined;
@@ -78,11 +86,13 @@ export interface ChatContextType {
78
86
  chatId: string,
79
87
  messages: Message[],
80
88
  ) => void;
89
+ /** Shallow-merge keys into `chat.meta` and persist. */
90
+ updateChatMeta: (scopeId: string, chatId: string, patch: ChatMeta) => void;
81
91
  sendMessage: (
82
92
  scopeId: string,
83
93
  message: string | ChatSendMessagePayload,
84
94
  chatId?: string,
85
- ) => Promise<string>;
95
+ ) => Promise<SendMessageResult>;
86
96
  getChatsForScopeId: (scopeId: string) => Chat[];
87
97
  getCurrentChatId: (scopeId: string) => string | null;
88
98
  deleteChat: (scopeId: string, sessionId: string) => void;
@@ -184,6 +194,7 @@ function cloneMessagesForNewSession(messages: readonly Message[]): Message[] {
184
194
  .filter(message => !message.inProgress)
185
195
  .map(message => ({
186
196
  ...message,
197
+ ...(message.meta ? { meta: { ...message.meta } } : {}),
187
198
  ...(message.userTextFileAttachments
188
199
  ? {
189
200
  userTextFileAttachments: message.userTextFileAttachments.map(
@@ -372,6 +383,7 @@ export function ChatProvider({
372
383
  ? { userTextFileAttachments: attachments }
373
384
  : {}),
374
385
  ...(options?.inProgress ? { inProgress: true } : {}),
386
+ ...(options?.meta ? { meta: { ...options.meta } } : {}),
375
387
  };
376
388
 
377
389
  setChats(prev => {
@@ -444,6 +456,9 @@ export function ChatProvider({
444
456
  ) {
445
457
  delete next.inProgress;
446
458
  }
459
+ if (patch.meta != null) {
460
+ next.meta = { ...next.meta, ...patch.meta };
461
+ }
447
462
  return next;
448
463
  }),
449
464
  };
@@ -460,7 +475,10 @@ export function ChatProvider({
460
475
  (scopeId: string, chatId: string, messages: Message[]) => {
461
476
  if (userSwitchKey === null) return;
462
477
  addScopeIdToRegistry(scopeId);
463
- const cloned = messages.map(message => ({ ...message }));
478
+ const cloned = messages.map(message => ({
479
+ ...message,
480
+ ...(message.meta ? { meta: { ...message.meta } } : {}),
481
+ }));
464
482
 
465
483
  setChats(prev => {
466
484
  const scopeChats = prev[scopeId] ?? [];
@@ -478,12 +496,34 @@ export function ChatProvider({
478
496
  [userSwitchKey],
479
497
  );
480
498
 
499
+ const updateChatMeta = useCallback(
500
+ (scopeId: string, chatId: string, patch: ChatMeta) => {
501
+ if (userSwitchKey === null) return;
502
+ setChats(prev => {
503
+ const scopeChats = prev[scopeId] ?? [];
504
+ const updatedChats = scopeChats.map(chat => {
505
+ if (chat.session_id !== chatId) return chat;
506
+ return {
507
+ ...chat,
508
+ meta: { ...chat.meta, ...patch },
509
+ };
510
+ });
511
+
512
+ const chatsKey = getChatsKey(scopeId);
513
+ LS.set(chatsKey, updatedChats);
514
+
515
+ return { ...prev, [scopeId]: updatedChats };
516
+ });
517
+ },
518
+ [userSwitchKey],
519
+ );
520
+
481
521
  const sendMessage = useCallback(
482
522
  async (
483
523
  scopeId: string,
484
524
  message: string | ChatSendMessagePayload,
485
525
  chatId?: string,
486
- ): Promise<string> => {
526
+ ): Promise<SendMessageResult> => {
487
527
  const targetChatId = chatId ?? getCurrentChatId(scopeId);
488
528
  if (targetChatId === null || targetChatId === '') {
489
529
  throw new Error('No chat selected');
@@ -506,11 +546,28 @@ export function ChatProvider({
506
546
  );
507
547
  }
508
548
 
549
+ let progressMessageId: string | undefined;
550
+ if (typeof message !== 'string' && message.systemProgressLabel) {
551
+ progressMessageId = addMessage(
552
+ scopeId,
553
+ targetChatId,
554
+ MessageRole.SYSTEM,
555
+ message.systemProgressLabel,
556
+ { inProgress: true },
557
+ );
558
+ }
559
+
509
560
  const pendingChatSessionId = targetChatId;
510
561
  beginOutboundPending(scopeId, pendingChatSessionId);
562
+ let effectiveSessionId = pendingChatSessionId;
511
563
  try {
512
564
  const data = await sendChatMessageFn(apiPayload, pendingChatSessionId);
513
565
 
566
+ effectiveSessionId =
567
+ data.session_id && data.session_id !== pendingChatSessionId
568
+ ? data.session_id
569
+ : pendingChatSessionId;
570
+
514
571
  if (data.session_id && data.session_id !== pendingChatSessionId) {
515
572
  setChats(prev => {
516
573
  const scopeChats = prev[scopeId] ?? [];
@@ -528,15 +585,24 @@ export function ChatProvider({
528
585
  setCurrentChatId(scopeId, data.session_id);
529
586
  }
530
587
 
588
+ if (progressMessageId) {
589
+ removeMessageById(scopeId, effectiveSessionId, progressMessageId);
590
+ progressMessageId = undefined;
591
+ }
592
+
531
593
  addMessage(
532
594
  scopeId,
533
- data.session_id ? data.session_id : pendingChatSessionId,
595
+ effectiveSessionId,
534
596
  MessageRole.ASSISTANT,
535
597
  data.response,
536
598
  );
537
599
 
538
- return data.response;
600
+ return { response: data.response, sessionId: effectiveSessionId };
539
601
  } catch (error) {
602
+ if (progressMessageId) {
603
+ removeMessageById(scopeId, effectiveSessionId, progressMessageId);
604
+ }
605
+
540
606
  const errorMessage =
541
607
  error instanceof Error
542
608
  ? error.message
@@ -544,7 +610,7 @@ export function ChatProvider({
544
610
 
545
611
  addMessage(
546
612
  scopeId,
547
- pendingChatSessionId,
613
+ effectiveSessionId,
548
614
  MessageRole.ASSISTANT,
549
615
  errorMessage,
550
616
  );
@@ -555,6 +621,7 @@ export function ChatProvider({
555
621
  },
556
622
  [
557
623
  addMessage,
624
+ removeMessageById,
558
625
  beginOutboundPending,
559
626
  endOutboundPending,
560
627
  getCurrentChatId,
@@ -597,6 +664,7 @@ export function ChatProvider({
597
664
  removeMessageById,
598
665
  updateMessageById,
599
666
  setChatMessages,
667
+ updateChatMeta,
600
668
  sendMessage,
601
669
  getChatsForScopeId,
602
670
  getCurrentChatId,
@@ -683,6 +751,8 @@ export function useChatsForScopeId(scopeId: string) {
683
751
  addMessage,
684
752
  removeMessageById,
685
753
  updateMessageById,
754
+ updateChatMeta,
755
+ setChatMessages,
686
756
  sendMessage,
687
757
  deleteChat,
688
758
  } = useChats();
@@ -713,6 +783,10 @@ export function useChatsForScopeId(scopeId: string) {
713
783
  ) => updateMessageById(scopeId, chatId, messageId, patch),
714
784
  sendMessage: (message: string | ChatSendMessagePayload, chatId?: string) =>
715
785
  sendMessage(scopeId, message, chatId),
786
+ updateChatMeta: (chatId: string, patch: ChatMeta) =>
787
+ updateChatMeta(scopeId, chatId, patch),
788
+ setChatMessages: (chatId: string, messages: Message[]) =>
789
+ setChatMessages(scopeId, chatId, messages),
716
790
  deleteChat: (sessionId: string) => deleteChat(scopeId, sessionId),
717
791
  };
718
792
  }
@@ -0,0 +1,6 @@
1
+ .chatDemoHost
2
+ display flex
3
+ flex-direction column
4
+ flex 1
5
+ // Bounded height so ChatChrome flex column can place presets above the prompt
6
+ min-height 480px
@@ -0,0 +1,7 @@
1
+ // This file is automatically generated.
2
+ // Please do not change this file!
3
+ interface CssExports {
4
+ 'chatDemoHost': string;
5
+ }
6
+ export const cssExports: CssExports;
7
+ export default cssExports;
@@ -1,106 +1,49 @@
1
- import { useCallback, useEffect, useRef, useState } from 'react';
2
-
3
- import { ChatChrome, Message, MessageRole } from '#uilib/components/ui/Chat';
1
+ import { ChatChrome, useChatPanelChromeModel } from '#uilib/components/ui/Chat';
4
2
  import { PageContentSection } from '#uilib/components/ui/Page';
5
- import { ScrollRef } from '@homecode/ui';
6
3
 
4
+ import {
5
+ DOCS_DATASET_PRESETS,
6
+ DOCS_SAMPLE_DATASET_NAME,
7
+ } from '../chatPresets.sample';
7
8
  import { AppPageHeader } from '../components/AppPageHeader/AppPageHeader';
9
+ import { DOCS_CHAT_USER_KEY } from '../docsConstants';
8
10
  import { DocsHeaderActions } from '../docsHeaderActions';
11
+ import S from './ChatPage.styl';
9
12
 
10
- const NO_QUICK_REPLY_KEYS: ReadonlySet<string> = new Set();
11
-
12
- const ASSISTANT_REPLY_TEXT =
13
- 'This is a generic assistant reply for the docs preview. Wire your app to a real model here.';
14
-
15
- function makeMessage(role: MessageRole, text: string): Message {
16
- return {
17
- id: crypto.randomUUID(),
18
- role,
19
- text,
20
- timestamp: Date.now(),
21
- };
22
- }
13
+ const DOCS_CHAT_INLINE_SCOPE_ID = `${DOCS_CHAT_USER_KEY}-docs-chat-inline`;
23
14
 
24
15
  export default function ChatPage() {
25
- const [messages, setMessages] = useState<Message[]>([]);
26
- const [isLoading, setIsLoading] = useState(false);
27
- const replyTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
28
- const scrollRef = useRef<ScrollRef>(null);
29
-
30
- useEffect(() => {
31
- return () => {
32
- if (replyTimeoutRef.current != null) {
33
- clearTimeout(replyTimeoutRef.current);
34
- }
35
- };
36
- }, []);
37
-
38
- const isEmpty = messages.length === 0 && !isLoading;
39
- const isLastMessageFromUser =
40
- messages.length > 0 &&
41
- messages[messages.length - 1]?.role === MessageRole.USER;
42
-
43
- const onSubmit = useCallback(
44
- (raw: string) => {
45
- const text = raw.trim();
46
- if (!text || isLoading) return;
47
-
48
- setMessages(prev => [...prev, makeMessage(MessageRole.USER, text)]);
49
- setIsLoading(true);
50
-
51
- if (replyTimeoutRef.current != null) {
52
- clearTimeout(replyTimeoutRef.current);
53
- }
54
- replyTimeoutRef.current = setTimeout(() => {
55
- replyTimeoutRef.current = null;
56
- setMessages(prev => [
57
- ...prev,
58
- makeMessage(MessageRole.ASSISTANT, ASSISTANT_REPLY_TEXT),
59
- ]);
60
- setIsLoading(false);
61
- }, 900);
16
+ const { chromeProps } = useChatPanelChromeModel({
17
+ embedAsPage: true,
18
+ scopeId: DOCS_CHAT_INLINE_SCOPE_ID,
19
+ presets: DOCS_DATASET_PRESETS,
20
+ emptyState: {
21
+ title: 'Start a conversation',
22
+ description:
23
+ 'Send a message below or pick a preset chip. The demo echoes your text after a short delay.',
24
+ additionalContent: (
25
+ <p>Optional empty-state slot via additionalContent.</p>
26
+ ),
62
27
  },
63
- [isLoading],
64
- );
28
+ });
65
29
 
66
30
  return (
67
31
  <>
68
32
  <AppPageHeader
69
33
  breadcrumbs={[{ label: 'Chat' }]}
70
34
  title="Chat"
71
- subheader="Chat shell with message list and prompt."
72
- actions={<DocsHeaderActions />}
35
+ subheader={`Chat shell with message list, prompt, and dataset-scoped preset bubbles for "${DOCS_SAMPLE_DATASET_NAME}".`}
36
+ actions={<DocsHeaderActions presets={DOCS_DATASET_PRESETS} />}
73
37
  />
74
38
  <PageContentSection>
75
- <ChatChrome
76
- showResizeHandle={false}
77
- resizeHandle={undefined}
78
- onClose={undefined}
79
- isEmpty={isEmpty}
80
- renderPresets={() => null}
81
- messages={messages}
82
- onQuickReply={() => {}}
83
- suppressedQuickReplyKeys={NO_QUICK_REPLY_KEYS}
84
- isLoading={isLoading}
85
- scriptContinueLabel={undefined}
86
- onScriptContinue={undefined}
87
- showSyntheticBranchButtons={false}
88
- unusedBranchKeys={[]}
89
- showInlinePresets={false}
90
- isLastMessageFromUser={isLastMessageFromUser}
91
- scrollRef={scrollRef}
92
- effectiveScopeId="docs-chat-inline"
93
- onPromptSubmit={onSubmit}
94
- onChatDeleted={() => {}}
95
- emptyState={{
96
- title: 'Start a conversation',
97
- description:
98
- 'Send a message below. This demo appends a canned reply after a short delay.',
99
- additionalContent: (
100
- <p>Optional empty-state slot via additionalContent.</p>
101
- ),
102
- }}
103
- />
39
+ <p style={{ marginBottom: 16, fontSize: 14, lineHeight: 1.5 }}>
40
+ Preset chips render above the prompt in the empty state (fixed layout)
41
+ and inline after the assistant reply. Click a chip to send its text;
42
+ used chips hide for the session.
43
+ </p>
44
+ <div className={S.chatDemoHost}>
45
+ <ChatChrome {...chromeProps} />
46
+ </div>
104
47
  </PageContentSection>
105
48
  </>
106
49
  );
@@ -18,9 +18,9 @@ const NO_QUICK_REPLY_KEYS: ReadonlySet<string> = new Set();
18
18
  /** Sample items — default TipTap mention insert; optional `className` / `color` style the chip. */
19
19
  const DOCS_SLASH_ITEMS: SlashCommandItem[] = [
20
20
  {
21
- id: 'generate-dashboard',
22
- label: 'Generate dashboard',
23
- description: 'Insert /generate-dashboard',
21
+ id: 'generate-report',
22
+ label: 'Generate report',
23
+ description: 'Insert /generate-report',
24
24
  color: 'var(--brand-color-600)',
25
25
  },
26
26
  {
@@ -48,7 +48,7 @@ function makeMessage(role: MessageRole, text: string): Message {
48
48
  }
49
49
 
50
50
  const ASSISTANT_REPLY_TEXT =
51
- 'Demo reply. Slash picks insert inline mention chips; plain text on send uses each item id (e.g. /generate-dashboard).';
51
+ 'Demo reply. Slash picks insert inline mention chips; plain text on send uses each item id (e.g. /generate-report).';
52
52
 
53
53
  export default function ChatSlashCommandsPage() {
54
54
  const [messages, setMessages] = useState<Message[]>([]);