@sybilion/uilib 1.2.16 → 1.2.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.
Files changed (23) hide show
  1. package/dist/esm/components/ui/Chat/ChatEmptyState/ChatEmptyState.js +2 -2
  2. package/dist/esm/components/ui/Chat/ChatEmptyState/ChatEmptyState.styl.js +1 -1
  3. package/dist/esm/components/ui/Chat/ChatSheet/ChatSheet.js +2 -1
  4. package/dist/esm/components/ui/Chat/ChatSheet/useChatPanelChromeModel.js +5 -8
  5. package/dist/esm/components/ui/Page/PageContent/PageContent.styl.js +1 -1
  6. package/dist/esm/contexts/chat-context.js +11 -6
  7. package/dist/esm/index.js +1 -1
  8. package/dist/esm/types/src/components/ui/Chat/ChatEmptyState/ChatEmptyState.d.ts +1 -1
  9. package/dist/esm/types/src/components/ui/Chat/ChatEmptyState/ChatEmptyState.types.d.ts +2 -0
  10. package/dist/esm/types/src/components/ui/Chat/ChatSheet/ChatSheet.d.ts +1 -1
  11. package/dist/esm/types/src/components/ui/Chat/ChatSheet/useChatPanelChromeModel.d.ts +4 -1
  12. package/dist/esm/types/src/contexts/chat-context.d.ts +3 -0
  13. package/package.json +1 -1
  14. package/src/components/ui/Chat/ChatEmptyState/ChatEmptyState.styl +2 -1
  15. package/src/components/ui/Chat/ChatEmptyState/ChatEmptyState.tsx +2 -0
  16. package/src/components/ui/Chat/ChatEmptyState/ChatEmptyState.types.ts +2 -0
  17. package/src/components/ui/Chat/ChatSheet/ChatSheet.tsx +2 -0
  18. package/src/components/ui/Chat/ChatSheet/useChatPanelChromeModel.tsx +13 -8
  19. package/src/components/ui/Page/PageContent/PageContent.styl +0 -1
  20. package/src/contexts/chat-context.tsx +13 -3
  21. package/src/docs/docsHeaderActions.tsx +8 -0
  22. package/src/docs/pages/CardPage.tsx +1 -1
  23. package/src/docs/pages/ChatPage.tsx +3 -0
@@ -2,8 +2,8 @@ import { jsxs, jsx } from 'react/jsx-runtime';
2
2
  import S from './ChatEmptyState.styl.js';
3
3
  import SparklesIcon from './icons/sparkles.svg.js';
4
4
 
5
- function ChatEmptyState({ icon = jsx(SparklesIcon, {}), title = 'Start a conversation', description, children, }) {
6
- return (jsxs("div", { className: S.root, children: [icon && jsx("div", { className: S.icon, children: icon }), title && jsx("h2", { children: title }), description && jsx("p", { children: description }), children] }));
5
+ function ChatEmptyState({ icon = jsx(SparklesIcon, {}), title = 'Start a conversation', description, additionalContent, children, }) {
6
+ return (jsxs("div", { className: S.root, children: [icon && jsx("div", { className: S.icon, children: icon }), title && jsx("h2", { children: title }), description && jsx("p", { children: description }), additionalContent, children] }));
7
7
  }
8
8
 
9
9
  export { ChatEmptyState };
@@ -1,6 +1,6 @@
1
1
  import styleInject from 'style-inject';
2
2
 
3
- var css_248z = ".ChatEmptyState_root__j1n-C{align-items:center;display:flex;flex:1;flex-direction:column;gap:var(--p-10);justify-content:center;padding:var(--p-6);text-align:center;text-size:var(--text-sm);color:var(--text-secondary)}.ChatEmptyState_icon__YSDgv,.ChatEmptyState_icon__YSDgv>svg{height:32px;width:32px}";
3
+ var css_248z = ".ChatEmptyState_root__j1n-C{align-items:center;color:var(--text-secondary);display:flex;flex:1;flex-direction:column;font-size:var(--text-sm);gap:var(--p-10);justify-content:center;padding:var(--p-6);text-align:center;text-wrap:balance}.ChatEmptyState_icon__YSDgv,.ChatEmptyState_icon__YSDgv>svg{height:32px;width:32px}";
4
4
  var S = {"root":"ChatEmptyState_root__j1n-C","icon":"ChatEmptyState_icon__YSDgv"};
5
5
  styleInject(css_248z);
6
6
 
@@ -4,7 +4,7 @@ import { Button } from '../../Button/Button.js';
4
4
  import { ChatChrome } from '../ChatChrome/ChatChrome.js';
5
5
  import { useChatPanelChromeModel } from './useChatPanelChromeModel.js';
6
6
 
7
- function ChatSheet({ triggerLabel = 'Open Chat', triggerAriaLabel, actionsRef, renderTrigger, presets, scopeId, onMessage, onScriptComplete, onGenerateDashboard, renderMessageChart, inline = false, }) {
7
+ function ChatSheet({ triggerLabel = 'Open Chat', triggerAriaLabel, actionsRef, renderTrigger, presets, scopeId, onMessage, onScriptComplete, onGenerateDashboard, renderMessageChart, emptyState, inline = false, }) {
8
8
  const model = useChatPanelChromeModel({
9
9
  embedAsPage: inline,
10
10
  presets,
@@ -13,6 +13,7 @@ function ChatSheet({ triggerLabel = 'Open Chat', triggerAriaLabel, actionsRef, r
13
13
  onScriptComplete,
14
14
  onGenerateDashboard,
15
15
  renderMessageChart,
16
+ emptyState,
16
17
  });
17
18
  if (actionsRef) {
18
19
  actionsRef.current = {
@@ -3,7 +3,7 @@ import { useState, useRef, useEffect, useMemo, useCallback } from 'react';
3
3
  import { MessageRole, GENERATING_DASHBOARD_SYSTEM_TEXT } from '../Chat.types.js';
4
4
  import { isGraphIntakeAssistantStepComplete, matchUserTextToQuickReply, parseScriptLine, textHasQuickReplyMarkers, branchKeysUsedFromChatHistory, branchKeysUsedByUserMessages, extractQuickReplyLabelKeyPairsFromText, entryBranchKeyBeforeLastAssistant, isPresetScriptGraph, branchesFromPresetScriptGraph } from '../ChatMessage/presetScript.js';
5
5
  import { usedPresetIdsFromMessages, formatChatTranscript } from '../chat-preset-utils.js';
6
- import { useChatsForScopeId, useChat } from '../../../../contexts/chat-context.js';
6
+ import { useChatsForScopeId, useChat, isChatEmpty } from '../../../../contexts/chat-context.js';
7
7
  import useEvent from '../../../../hooks/useEvent.js';
8
8
  import { useIsMobile } from '../../../../hooks/useIsMobile.js';
9
9
  import { useQueryParams } from '../../../../hooks/useQueryParams.js';
@@ -21,7 +21,7 @@ const CHAT_NAV_COLLAPSE_BREAKPOINT_PX = 1400;
21
21
  const CHAT_QUERY_PARAM = 'chat';
22
22
  const CHAT_OPEN_VALUE = 'open';
23
23
  const PROMPT_QUERY_PARAM = 'prompt';
24
- function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onScriptComplete, onGenerateDashboard, renderMessageChart, }) {
24
+ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onScriptComplete, onGenerateDashboard, renderMessageChart, emptyState, }) {
25
25
  const effectiveScopeId = scopeId ?? NO_SCOPE_FALLBACK;
26
26
  const isMobile = useIsMobile();
27
27
  const { chatPanelContainer, isOpen: sidebarNavOpen, setOpen: setSidebarNavOpen, chatWidthPx, setChatWidthPx, getShellWidth, setChatPanelOpen, } = useSidebar();
@@ -145,6 +145,7 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
145
145
  removeSearchParams(CHAT_QUERY_PARAM);
146
146
  }
147
147
  };
148
+ const isEmpty = isChatEmpty(chat) && !isLoading;
148
149
  /**
149
150
  * App link: `?prompt=…` — open panel, pre-fill composer, strip param (read once on mount
150
151
  * from `location.search`). If the selected session already has messages, `newChat()` first.
@@ -168,12 +169,8 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
168
169
  return;
169
170
  }
170
171
  promptParamHandledInEffectRef.current = true;
171
- const selected = currentChatId
172
- ? chats.find(c => c.session_id === currentChatId)
173
- : undefined;
174
- const selectedHasMessages = (selected?.messages?.length ?? 0) > 0;
175
172
  const needsFirstSession = chats.length === 0;
176
- if (selectedHasMessages || needsFirstSession) {
173
+ if (!isEmpty || needsFirstSession) {
177
174
  const sessionId = newChat();
178
175
  if (sessionId == null) {
179
176
  logger.warn('Chat prompt link: sign in to use the assistant.');
@@ -641,7 +638,6 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
641
638
  };
642
639
  const isLastMessageFromUser = chat?.messages.length > 0 &&
643
640
  chat.messages[chat.messages.length - 1]?.role === MessageRole.USER;
644
- const isEmpty = !chat?.messages?.length && !isLoading;
645
641
  const linearScriptActive = Boolean(currentChatId && scriptByChatId[currentChatId]);
646
642
  const quickBranches = currentChatId
647
643
  ? quickReplyBranchesByChat[currentChatId]
@@ -771,6 +767,7 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
771
767
  onPromptSubmit: handlePromptSubmit,
772
768
  onChatDeleted: endLocalDemoFlow,
773
769
  promptPrefill: promptLinkPrefill,
770
+ emptyState,
774
771
  };
775
772
  const toggleOpen = () => onOpenChange(!isOpen);
776
773
  return {
@@ -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)}}.PageContent_root__caExB{display:flex;flex:1;flex-direction:column;max-width:var(--page-width);min-height:0;padding-bottom:var(--page-y-padding);width:100%}.PageContent_section__Wve-w{display:flex;flex-direction:column;min-height:0;padding-left:var(--page-x-padding);padding-right:var(--page-x-padding)}.PageContent_grow__Nkfqk{flex-grow:1}";
3
+ var css_248z = "@media (max-width:768px){:root{--page-x-padding:var(--p-6);--page-y-padding:var(--p-6)}}.PageContent_root__caExB{display:flex;flex:1;flex-direction:column;max-width:var(--page-width);min-height:0;width:100%}.PageContent_section__Wve-w{display:flex;flex-direction:column;min-height:0;padding-left:var(--page-x-padding);padding-right:var(--page-x-padding)}.PageContent_grow__Nkfqk{flex-grow:1}";
4
4
  var S = {"root":"PageContent_root__caExB","section":"PageContent_section__Wve-w","grow":"PageContent_grow__Nkfqk"};
5
5
  styleInject(css_248z);
6
6
 
@@ -1,5 +1,5 @@
1
1
  import { jsx } from 'react/jsx-runtime';
2
- import { createContext, useState, useCallback, useEffect, useContext } from 'react';
2
+ import { createContext, useState, useCallback, useEffect, useContext, useMemo } from 'react';
3
3
  import { MessageRole } from '../components/ui/Chat/Chat.types.js';
4
4
  import { stripJsonDashboardFences } from '../lib/dashboard-spec/stripJsonDashboardFences.js';
5
5
  import { LS } from '@homecode/ui';
@@ -255,6 +255,7 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
255
255
  deleteChat,
256
256
  }, children: children }));
257
257
  }
258
+ const isChatEmpty = (chat) => chat?.messages.length === 0;
258
259
  function useChats() {
259
260
  const context = useContext(ChatContext);
260
261
  if (context === undefined) {
@@ -264,17 +265,21 @@ function useChats() {
264
265
  }
265
266
  function useChat(scopeId, chatId) {
266
267
  const { getChatsForScopeId } = useChats();
267
- if (!scopeId || !chatId)
268
- return null;
269
- const list = getChatsForScopeId(scopeId);
270
- return list.find(chat => chat.session_id === chatId) ?? null;
268
+ return useMemo(() => {
269
+ if (!scopeId || !chatId)
270
+ return null;
271
+ return (getChatsForScopeId(scopeId)?.find(chat => chat.session_id === chatId) ??
272
+ null);
273
+ }, [scopeId, chatId, getChatsForScopeId]);
271
274
  }
272
275
  function useChatsForScopeId(scopeId) {
273
276
  const { getChatsForScopeId, getCurrentChatId, setCurrentChatId, newChat, addMessage, removeMessageById, sendMessage, deleteChat, } = useChats();
274
277
  const chats = getChatsForScopeId(scopeId);
275
278
  const currentChatId = getCurrentChatId(scopeId);
279
+ const currentChat = useChat(scopeId, currentChatId ?? undefined);
276
280
  return {
277
281
  chats,
282
+ currentChat,
278
283
  currentChatId,
279
284
  setCurrentChatId: (targetId) => setCurrentChatId(scopeId, targetId),
280
285
  newChat: () => newChat(scopeId),
@@ -294,4 +299,4 @@ function useCurrentChat(scopeId) {
294
299
  return useChat(scopeId, chatId ?? undefined);
295
300
  }
296
301
 
297
- export { ChatContext, ChatProvider, useChat, useChats, useChatsForDataset, useChatsForScopeId, useCurrentChat };
302
+ export { ChatContext, ChatProvider, isChatEmpty, useChat, useChats, useChatsForDataset, useChatsForScopeId, useCurrentChat };
package/dist/esm/index.js CHANGED
@@ -4,7 +4,7 @@ export { DEFAULT_THEME_ACTIVE_COLOR } from './docs/lib/theme.js';
4
4
  export { SybilionAuthProvider, createSybilionApiFetch, getSybilionApiOriginFromSdk, sybilionApiFetch, useSybilionApiFetch, useSybilionAuth } from './sybilion-auth/SybilionAuthProvider.js';
5
5
  export { SYBILION_AUTH_LOGIN_PATH, normalizeApiBaseUrl } from './sybilion-auth/authPaths.js';
6
6
  export { exchangeAuth0AccessTokenForSybilionJwt } from './sybilion-auth/exchangeSybilionToken.js';
7
- export { ChatContext, ChatProvider, useChat, useChats, useChatsForDataset, useChatsForScopeId, useCurrentChat } from './contexts/chat-context.js';
7
+ export { ChatContext, ChatProvider, isChatEmpty, useChat, useChats, useChatsForDataset, useChatsForScopeId, useCurrentChat } from './contexts/chat-context.js';
8
8
  export { AnalysesSelector } from './components/ui/AnalysesSelector/AnalysesSelector.js';
9
9
  export { AnalysisLineIcon } from './components/ui/AnalysisLineIcon/AnalysisLineIcon.js';
10
10
  export { AppHeaderHost, AppHeaderPortal } from './components/ui/AppHeader/AppHeader.js';
@@ -1,3 +1,3 @@
1
1
  import { PropsWithChildren } from 'react';
2
2
  import type { ChatEmptyStateProps } from './ChatEmptyState.types';
3
- export declare function ChatEmptyState({ icon, title, description, children, }: PropsWithChildren<ChatEmptyStateProps>): import("react/jsx-runtime").JSX.Element;
3
+ export declare function ChatEmptyState({ icon, title, description, additionalContent, children, }: PropsWithChildren<ChatEmptyStateProps>): import("react/jsx-runtime").JSX.Element;
@@ -2,4 +2,6 @@ export interface ChatEmptyStateProps {
2
2
  icon?: React.ReactNode;
3
3
  title?: string;
4
4
  description?: string;
5
+ /** Extra block below description (works when passed via `ChatChrome` / `ChatSheet` `emptyState`). */
6
+ additionalContent?: React.ReactNode;
5
7
  }
@@ -19,4 +19,4 @@ export interface ChatSheetProps extends Omit<UseChatPanelChromeModelInput, 'embe
19
19
  */
20
20
  inline?: boolean;
21
21
  }
22
- export declare function ChatSheet({ triggerLabel, triggerAriaLabel, actionsRef, renderTrigger, presets, scopeId, onMessage, onScriptComplete, onGenerateDashboard, renderMessageChart, inline, }: ChatSheetProps): import("react/jsx-runtime").JSX.Element;
22
+ export declare function ChatSheet({ triggerLabel, triggerAriaLabel, actionsRef, renderTrigger, presets, scopeId, onMessage, onScriptComplete, onGenerateDashboard, renderMessageChart, emptyState, inline, }: ChatSheetProps): import("react/jsx-runtime").JSX.Element;
@@ -1,5 +1,6 @@
1
1
  import { ChatPreset, type ScriptCompletePayload } from '#uilib/components/ui/Chat/Chat.types';
2
2
  import type { ChatChromeProps } from '../ChatChrome';
3
+ import type { ChatEmptyStateProps } from '../ChatEmptyState/ChatEmptyState.types';
3
4
  export type UseChatPanelChromeModelInput = {
4
5
  /** When true, skip sidebar chat slot, URL `?chat=`, and portal behavior (e.g. page main content). */
5
6
  embedAsPage: boolean;
@@ -13,6 +14,8 @@ export type UseChatPanelChromeModelInput = {
13
14
  onGenerateDashboard?: (transcript: string) => void | Promise<void>;
14
15
  /** Renders `[CHART]` tokens in assistant messages. */
15
16
  renderMessageChart?: () => React.ReactNode;
17
+ /** Forwarded to `ChatChrome` when the thread is empty. */
18
+ emptyState?: ChatEmptyStateProps;
16
19
  };
17
20
  export type UseChatPanelChromeModelResult = {
18
21
  chromeProps: ChatChromeProps;
@@ -22,4 +25,4 @@ export type UseChatPanelChromeModelResult = {
22
25
  newChat: () => void;
23
26
  chatPanelContainer: HTMLElement | null;
24
27
  };
25
- export declare function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onScriptComplete, onGenerateDashboard, renderMessageChart, }: UseChatPanelChromeModelInput): UseChatPanelChromeModelResult;
28
+ export declare function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onScriptComplete, onGenerateDashboard, renderMessageChart, emptyState, }: UseChatPanelChromeModelInput): UseChatPanelChromeModelResult;
@@ -21,10 +21,12 @@ export interface ChatProviderProps {
21
21
  sendChatMessage: SendChatMessageFn;
22
22
  }
23
23
  export declare function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessageFn, }: ChatProviderProps): import("react/jsx-runtime").JSX.Element;
24
+ export declare const isChatEmpty: (chat: Chat | null) => boolean;
24
25
  export declare function useChats(): ChatContextType;
25
26
  export declare function useChat(scopeId: string | undefined, chatId: string | undefined): Chat | null;
26
27
  export declare function useChatsForScopeId(scopeId: string): {
27
28
  chats: Chat[];
29
+ currentChat: Chat;
28
30
  currentChatId: string;
29
31
  setCurrentChatId: (targetId: string) => void;
30
32
  newChat: () => string;
@@ -36,6 +38,7 @@ export declare function useChatsForScopeId(scopeId: string): {
36
38
  /** @deprecated Use useChatsForScopeId */
37
39
  export declare function useChatsForDataset(scopeId: string): {
38
40
  chats: Chat[];
41
+ currentChat: Chat;
39
42
  currentChatId: string;
40
43
  setCurrentChatId: (targetId: string) => void;
41
44
  newChat: () => string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sybilion/uilib",
3
- "version": "1.2.16",
3
+ "version": "1.2.18",
4
4
  "description": "Sybilion Design System — React UI components (Webpack + Stylus)",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -10,7 +10,8 @@
10
10
  padding var(--p-6)
11
11
 
12
12
  text-align center
13
- text-size var(--text-sm)
13
+ text-wrap balance
14
+ font-size var(--text-sm)
14
15
  color var(--text-secondary)
15
16
 
16
17
  .icon
@@ -8,6 +8,7 @@ export function ChatEmptyState({
8
8
  icon = <SparklesIcon />,
9
9
  title = 'Start a conversation',
10
10
  description,
11
+ additionalContent,
11
12
  children,
12
13
  }: PropsWithChildren<ChatEmptyStateProps>) {
13
14
  return (
@@ -15,6 +16,7 @@ export function ChatEmptyState({
15
16
  {icon && <div className={S.icon}>{icon}</div>}
16
17
  {title && <h2>{title}</h2>}
17
18
  {description && <p>{description}</p>}
19
+ {additionalContent}
18
20
  {children}
19
21
  </div>
20
22
  );
@@ -2,4 +2,6 @@ export interface ChatEmptyStateProps {
2
2
  icon?: React.ReactNode;
3
3
  title?: string;
4
4
  description?: string;
5
+ /** Extra block below description (works when passed via `ChatChrome` / `ChatSheet` `emptyState`). */
6
+ additionalContent?: React.ReactNode;
5
7
  }
@@ -43,6 +43,7 @@ export function ChatSheet({
43
43
  onScriptComplete,
44
44
  onGenerateDashboard,
45
45
  renderMessageChart,
46
+ emptyState,
46
47
  inline = false,
47
48
  }: ChatSheetProps) {
48
49
  const model = useChatPanelChromeModel({
@@ -53,6 +54,7 @@ export function ChatSheet({
53
54
  onScriptComplete,
54
55
  onGenerateDashboard,
55
56
  renderMessageChart,
57
+ emptyState,
56
58
  });
57
59
 
58
60
  if (actionsRef) {
@@ -23,7 +23,11 @@ import {
23
23
  formatChatTranscript,
24
24
  usedPresetIdsFromMessages,
25
25
  } from '#uilib/components/ui/Chat/chat-preset-utils';
26
- import { useChat, useChatsForScopeId } from '#uilib/contexts/chat-context';
26
+ import {
27
+ isChatEmpty,
28
+ useChat,
29
+ useChatsForScopeId,
30
+ } from '#uilib/contexts/chat-context';
27
31
  import useEvent from '#uilib/hooks/useEvent';
28
32
  import { useIsMobile } from '#uilib/hooks/useIsMobile';
29
33
  import { useQueryParams } from '#uilib/hooks/useQueryParams';
@@ -34,6 +38,7 @@ import { ScrollRef } from '@homecode/ui';
34
38
  import { useSidebar } from '../../Sidebar/Sidebar';
35
39
  import { Chat } from '../Chat';
36
40
  import type { ChatChromeProps } from '../ChatChrome';
41
+ import type { ChatEmptyStateProps } from '../ChatEmptyState/ChatEmptyState.types';
37
42
 
38
43
  export type UseChatPanelChromeModelInput = {
39
44
  /** When true, skip sidebar chat slot, URL `?chat=`, and portal behavior (e.g. page main content). */
@@ -48,6 +53,8 @@ export type UseChatPanelChromeModelInput = {
48
53
  onGenerateDashboard?: (transcript: string) => void | Promise<void>;
49
54
  /** Renders `[CHART]` tokens in assistant messages. */
50
55
  renderMessageChart?: () => React.ReactNode;
56
+ /** Forwarded to `ChatChrome` when the thread is empty. */
57
+ emptyState?: ChatEmptyStateProps;
51
58
  };
52
59
 
53
60
  export type UseChatPanelChromeModelResult = {
@@ -92,6 +99,7 @@ export function useChatPanelChromeModel({
92
99
  onScriptComplete,
93
100
  onGenerateDashboard,
94
101
  renderMessageChart,
102
+ emptyState,
95
103
  }: UseChatPanelChromeModelInput): UseChatPanelChromeModelResult {
96
104
  const effectiveScopeId = scopeId ?? NO_SCOPE_FALLBACK;
97
105
  const isMobile = useIsMobile();
@@ -264,6 +272,8 @@ export function useChatPanelChromeModel({
264
272
  }
265
273
  };
266
274
 
275
+ const isEmpty = isChatEmpty(chat) && !isLoading;
276
+
267
277
  /**
268
278
  * App link: `?prompt=…` — open panel, pre-fill composer, strip param (read once on mount
269
279
  * from `location.search`). If the selected session already has messages, `newChat()` first.
@@ -290,13 +300,9 @@ export function useChatPanelChromeModel({
290
300
  }
291
301
  promptParamHandledInEffectRef.current = true;
292
302
 
293
- const selected = currentChatId
294
- ? chats.find(c => c.session_id === currentChatId)
295
- : undefined;
296
- const selectedHasMessages = (selected?.messages?.length ?? 0) > 0;
297
303
  const needsFirstSession = chats.length === 0;
298
304
 
299
- if (selectedHasMessages || needsFirstSession) {
305
+ if (!isEmpty || needsFirstSession) {
300
306
  const sessionId = newChat();
301
307
  if (sessionId == null) {
302
308
  logger.warn('Chat prompt link: sign in to use the assistant.');
@@ -816,8 +822,6 @@ export function useChatPanelChromeModel({
816
822
  chat?.messages.length > 0 &&
817
823
  chat.messages[chat.messages.length - 1]?.role === MessageRole.USER;
818
824
 
819
- const isEmpty = !chat?.messages?.length && !isLoading;
820
-
821
825
  const linearScriptActive = Boolean(
822
826
  currentChatId && scriptByChatId[currentChatId],
823
827
  );
@@ -993,6 +997,7 @@ export function useChatPanelChromeModel({
993
997
  onPromptSubmit: handlePromptSubmit,
994
998
  onChatDeleted: endLocalDemoFlow,
995
999
  promptPrefill: promptLinkPrefill,
1000
+ emptyState,
996
1001
  };
997
1002
 
998
1003
  const toggleOpen = () => onOpenChange(!isOpen);
@@ -7,7 +7,6 @@
7
7
  min-height 0
8
8
  width 100%
9
9
  max-width var(--page-width)
10
- padding-bottom var(--page-y-padding)
11
10
 
12
11
  .section
13
12
  pageXPadding()
@@ -4,6 +4,7 @@ import {
4
4
  useCallback,
5
5
  useContext,
6
6
  useEffect,
7
+ useMemo,
7
8
  useState,
8
9
  } from 'react';
9
10
 
@@ -383,6 +384,9 @@ export function ChatProvider({
383
384
  );
384
385
  }
385
386
 
387
+ export const isChatEmpty = (chat: Chat | null): boolean =>
388
+ chat?.messages.length === 0;
389
+
386
390
  export function useChats() {
387
391
  const context = useContext(ChatContext);
388
392
  if (context === undefined) {
@@ -396,9 +400,13 @@ export function useChat(
396
400
  chatId: string | undefined,
397
401
  ): Chat | null {
398
402
  const { getChatsForScopeId } = useChats();
399
- if (!scopeId || !chatId) return null;
400
- const list = getChatsForScopeId(scopeId);
401
- return list.find(chat => chat.session_id === chatId) ?? null;
403
+ return useMemo(() => {
404
+ if (!scopeId || !chatId) return null;
405
+ return (
406
+ getChatsForScopeId(scopeId)?.find(chat => chat.session_id === chatId) ??
407
+ null
408
+ );
409
+ }, [scopeId, chatId, getChatsForScopeId]);
402
410
  }
403
411
 
404
412
  export function useChatsForScopeId(scopeId: string) {
@@ -414,9 +422,11 @@ export function useChatsForScopeId(scopeId: string) {
414
422
  } = useChats();
415
423
  const chats = getChatsForScopeId(scopeId);
416
424
  const currentChatId = getCurrentChatId(scopeId);
425
+ const currentChat = useChat(scopeId, currentChatId ?? undefined);
417
426
 
418
427
  return {
419
428
  chats,
429
+ currentChat,
420
430
  currentChatId,
421
431
  setCurrentChatId: (targetId: string) => setCurrentChatId(scopeId, targetId),
422
432
  newChat: () => newChat(scopeId),
@@ -13,6 +13,14 @@ export function DocsHeaderActions() {
13
13
  </>
14
14
  }
15
15
  scopeId={DOCS_CHAT_SCOPE_ID}
16
+ emptyState={{
17
+ title: 'Start a conversation',
18
+ description:
19
+ 'Send a message below. This demo appends a canned reply after a short delay.',
20
+ additionalContent: (
21
+ <p>Optional empty-state slot via additionalContent.</p>
22
+ ),
23
+ }}
16
24
  />
17
25
  );
18
26
  }
@@ -22,7 +22,7 @@ export default function CardPage() {
22
22
  actions={<DocsHeaderActions />}
23
23
  />
24
24
  <PageContentSection>
25
- <Card style={{ maxWidth: 360 }}>
25
+ <Card paddingSize="s" style={{ maxWidth: 360 }}>
26
26
  <CardHeader>
27
27
  <CardTitle>Card title</CardTitle>
28
28
  <CardDescription>Supporting description text.</CardDescription>
@@ -101,6 +101,9 @@ export default function ChatPage() {
101
101
  title: 'Start a conversation',
102
102
  description:
103
103
  'Send a message below. This demo appends a canned reply after a short delay.',
104
+ additionalContent: (
105
+ <p>Optional empty-state slot via additionalContent.</p>
106
+ ),
104
107
  }}
105
108
  />
106
109
  </PageContentSection>