@extrachill/chat 0.6.0 → 0.8.0

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.8.0] - 2026-03-29
4
+
5
+ ### Added
6
+ - add cycling loading messages with extensible pool
7
+
8
+ ## [0.7.0] - 2026-03-26
9
+
10
+ ### Added
11
+ - standardize canonical diff chat primitives
12
+ - add native chat transcript copy support
13
+ - add shared client context injection api
14
+
15
+ ### Fixed
16
+ - restore list-style on ul/ol inside message bubbles
17
+
3
18
  ## [0.6.0] - 2026-03-25
4
19
 
5
20
  ### Added
package/css/chat.css CHANGED
@@ -397,6 +397,12 @@
397
397
  .ec-chat-typing__label {
398
398
  font-size: 12px;
399
399
  color: var(--ec-chat-text-muted);
400
+ transition: opacity 150ms ease;
401
+ opacity: 1;
402
+ }
403
+
404
+ .ec-chat-typing__label--fading {
405
+ opacity: 0;
400
406
  }
401
407
 
402
408
  /* ============================================
@@ -531,72 +537,43 @@
531
537
  font-size: 13px;
532
538
  }
533
539
 
534
- .ec-chat-sessions__list {
535
- list-style: none;
536
- margin: 0;
537
- padding: 0;
538
- max-height: 200px;
539
- overflow-y: auto;
540
- }
541
-
542
- .ec-chat-sessions__item {
540
+ .ec-chat-sessions__controls {
543
541
  display: flex;
542
+ gap: 8px;
544
543
  align-items: center;
544
+ flex-wrap: wrap;
545
+ padding: 0 var(--ec-chat-padding) 8px;
545
546
  }
546
547
 
547
- .ec-chat-sessions__item--active {
548
- background: var(--ec-chat-session-active-bg);
549
- }
550
-
551
- .ec-chat-sessions__item-button {
552
- flex: 1;
553
- display: flex;
554
- flex-direction: column;
555
- gap: 2px;
556
- padding: 8px var(--ec-chat-padding);
557
- border: none;
558
- background: transparent;
559
- cursor: pointer;
560
- text-align: left;
561
- font-family: inherit;
562
- color: inherit;
548
+ .ec-chat-sessions__select-wrap {
563
549
  min-width: 0;
550
+ flex: 1 1 220px;
564
551
  }
565
552
 
566
- .ec-chat-sessions__item-button:hover {
567
- background: var(--ec-chat-session-hover-bg);
568
- }
569
-
570
- .ec-chat-sessions__item-title {
571
- font-size: 13px;
572
- white-space: nowrap;
573
- overflow: hidden;
574
- text-overflow: ellipsis;
575
- }
576
-
577
- .ec-chat-sessions__item-date {
578
- font-size: 11px;
579
- color: var(--ec-chat-text-muted);
553
+ .ec-chat-sessions__select {
554
+ width: 100%;
555
+ min-height: 36px;
556
+ border: 1px solid var(--ec-chat-border);
557
+ border-radius: 999px;
558
+ padding: 8px 12px;
559
+ font: inherit;
560
+ background: var(--ec-chat-assistant-bg);
561
+ color: inherit;
580
562
  }
581
563
 
582
- .ec-chat-sessions__item-delete {
583
- padding: 4px 8px;
584
- border: none;
564
+ .ec-chat-sessions__delete {
565
+ border: 1px solid var(--ec-chat-border);
585
566
  background: transparent;
586
567
  color: var(--ec-chat-text-muted);
568
+ border-radius: 999px;
569
+ padding: 8px 12px;
570
+ font: inherit;
587
571
  cursor: pointer;
588
- font-size: 16px;
589
- line-height: 1;
590
- opacity: 0;
591
- transition: opacity 0.15s;
592
- }
593
-
594
- .ec-chat-sessions__item:hover .ec-chat-sessions__item-delete {
595
- opacity: 1;
596
572
  }
597
573
 
598
- .ec-chat-sessions__item-delete:hover {
599
- color: var(--ec-chat-tool-error);
574
+ .ec-chat-sessions__delete:hover {
575
+ color: var(--ec-chat-text);
576
+ background: var(--ec-chat-session-hover-bg);
600
577
  }
601
578
 
602
579
  /* ============================================
package/dist/Chat.d.ts CHANGED
@@ -3,6 +3,7 @@ import type { ChatMessage as ChatMessageType, ContentFormat } from './types/inde
3
3
  import type { FetchFn } from './api.ts';
4
4
  import type { ToolGroup } from './components/ToolMessage.tsx';
5
5
  import { type UseChatOptions } from './hooks/useChat.ts';
6
+ import { type LoadingMessagesConfig } from './hooks/useLoadingMessages.ts';
6
7
  import type { UseChatReturn } from './hooks/useChat.ts';
7
8
  export type ChatSessionUi = 'list' | 'none';
8
9
  export interface ChatProps {
@@ -58,6 +59,18 @@ export interface ChatProps {
58
59
  sessionUi?: ChatSessionUi;
59
60
  /** Label shown during multi-turn processing. */
60
61
  processingLabel?: (turnCount: number) => string;
62
+ /**
63
+ * Cycling loading messages shown while the assistant is thinking.
64
+ *
65
+ * - `true` — enable with built-in defaults.
66
+ * - `LoadingMessagesConfig` — extend or override the default pool.
67
+ * - `false` / `undefined` — disabled (dots only, original behavior).
68
+ *
69
+ * When enabled alongside `processingLabel`, loading messages are shown
70
+ * on the initial turn (turnCount === 0) and `processingLabel` takes
71
+ * over during multi-turn continuation.
72
+ */
73
+ loadingMessages?: boolean | LoadingMessagesConfig;
61
74
  /** Whether to show the attachment button in the input. Defaults to true. */
62
75
  allowAttachments?: boolean;
63
76
  /** Accepted file types for attachments. Defaults to 'image/*,video/*'. */
@@ -67,11 +80,11 @@ export interface ChatProps {
67
80
  * Use for client-side context injection (e.g. `{ client_context: { tab: 'compose', postId: 123 } }`).
68
81
  */
69
82
  metadata?: Record<string, unknown>;
70
- /** Whether to show a built-in copy transcript button. Defaults to false. */
83
+ /** Deprecated: built-in copy transcript button UI is no longer rendered. */
71
84
  showCopyTranscript?: boolean;
72
- /** Label for the built-in copy transcript button. */
85
+ /** Deprecated legacy prop retained for compatibility. */
73
86
  copyTranscriptLabel?: string;
74
- /** Label shown after the transcript is copied. */
87
+ /** Deprecated legacy prop retained for compatibility. */
75
88
  copyTranscriptCopiedLabel?: string;
76
89
  /** Optional custom header/actions area rendered above messages with live chat state. */
77
90
  renderHeader?: (chat: UseChatReturn) => ReactNode;
@@ -99,5 +112,5 @@ export interface ChatProps {
99
112
  * }
100
113
  * ```
101
114
  */
102
- export declare function Chat({ basePath, fetchFn, agentId, contentFormat, renderContent, showTools, toolNames, toolRenderers, placeholder, emptyState, initialMessages, initialSessionId, maxContinueTurns, onError, onMessage, className, showSessions, sessionUi, processingLabel, allowAttachments, acceptFileTypes, metadata, showCopyTranscript, copyTranscriptLabel, copyTranscriptCopiedLabel, renderHeader, }: ChatProps): import("react/jsx-runtime").JSX.Element;
115
+ export declare function Chat({ basePath, fetchFn, agentId, contentFormat, renderContent, showTools, toolNames, toolRenderers, placeholder, emptyState, initialMessages, initialSessionId, maxContinueTurns, onError, onMessage, className, showSessions, sessionUi, processingLabel, loadingMessages, allowAttachments, acceptFileTypes, metadata, showCopyTranscript, copyTranscriptLabel, copyTranscriptCopiedLabel, renderHeader, }: ChatProps): import("react/jsx-runtime").JSX.Element;
103
116
  //# sourceMappingURL=Chat.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Chat.d.ts","sourceRoot":"","sources":["../src/Chat.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"Chat.d.ts","sourceRoot":"","sources":["../src/Chat.tsx"],"names":[],"mappings":"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"}
package/dist/Chat.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useChat } from "./hooks/useChat.js";
3
+ import { useLoadingMessages } from "./hooks/useLoadingMessages.js";
3
4
  import { ErrorBoundary } from "./components/ErrorBoundary.js";
4
5
  import { AvailabilityGate } from "./components/AvailabilityGate.js";
5
6
  import { ChatMessages } from "./components/ChatMessages.js";
6
7
  import { ChatInput } from "./components/ChatInput.js";
7
8
  import { TypingIndicator } from "./components/TypingIndicator.js";
8
9
  import { SessionSwitcher } from "./components/SessionSwitcher.js";
9
- import { CopyTranscriptButton } from "./components/CopyTranscriptButton.js";
10
10
  /**
11
11
  * Ready-to-use chat component.
12
12
  *
@@ -30,7 +30,7 @@ import { CopyTranscriptButton } from "./components/CopyTranscriptButton.js";
30
30
  * }
31
31
  * ```
32
32
  */
33
- export function Chat({ basePath, fetchFn, agentId, contentFormat = 'markdown', renderContent, showTools = true, toolNames, toolRenderers, placeholder, emptyState, initialMessages, initialSessionId, maxContinueTurns, onError, onMessage, className, showSessions = true, sessionUi = 'list', processingLabel, allowAttachments = true, acceptFileTypes, metadata, showCopyTranscript = false, copyTranscriptLabel, copyTranscriptCopiedLabel, renderHeader, }) {
33
+ export function Chat({ basePath, fetchFn, agentId, contentFormat = 'markdown', renderContent, showTools = true, toolNames, toolRenderers, placeholder, emptyState, initialMessages, initialSessionId, maxContinueTurns, onError, onMessage, className, showSessions = true, sessionUi = 'list', processingLabel, loadingMessages, allowAttachments = true, acceptFileTypes, metadata, showCopyTranscript = false, copyTranscriptLabel, copyTranscriptCopiedLabel, renderHeader, }) {
34
34
  const chat = useChat({
35
35
  basePath,
36
36
  fetchFn,
@@ -42,11 +42,18 @@ export function Chat({ basePath, fetchFn, agentId, contentFormat = 'markdown', r
42
42
  onMessage,
43
43
  metadata,
44
44
  });
45
+ // Resolve loading messages config.
46
+ const loadingMessagesConfig = loadingMessages === true ? {} :
47
+ loadingMessages ? loadingMessages :
48
+ undefined;
49
+ const cycling = useLoadingMessages(chat.isLoading && !!loadingMessagesConfig, loadingMessagesConfig);
45
50
  const baseClass = 'ec-chat';
46
51
  const classes = [baseClass, className].filter(Boolean).join(' ');
47
- return (_jsx(ErrorBoundary, { onError: onError ? (err) => onError(err) : undefined, children: _jsx("div", { className: classes, children: _jsxs(AvailabilityGate, { availability: chat.availability, children: [renderHeader?.(chat), showCopyTranscript && (_jsx("div", { className: "ec-chat__actions", children: _jsx(CopyTranscriptButton, { messages: chat.messages, label: copyTranscriptLabel, copiedLabel: copyTranscriptCopiedLabel }) })), showSessions && sessionUi === 'list' && (_jsx(SessionSwitcher, { sessions: chat.sessions, activeSessionId: chat.sessionId ?? undefined, onSelect: chat.switchSession, onNew: chat.newSession, onDelete: chat.deleteSession, loading: chat.sessionsLoading })), _jsx(ChatMessages, { messages: chat.messages, contentFormat: contentFormat, renderContent: renderContent, showTools: showTools, toolNames: toolNames, toolRenderers: toolRenderers, emptyState: emptyState }), _jsx(TypingIndicator, { visible: chat.isLoading, label: chat.turnCount > 0
52
+ return (_jsx(ErrorBoundary, { onError: onError ? (err) => onError(err) : undefined, children: _jsx("div", { className: classes, children: _jsxs(AvailabilityGate, { availability: chat.availability, children: [renderHeader?.(chat), showSessions && sessionUi === 'list' && (_jsx(SessionSwitcher, { sessions: chat.sessions, activeSessionId: chat.sessionId ?? undefined, onSelect: chat.switchSession, onNew: chat.newSession, onDelete: chat.deleteSession, loading: chat.sessionsLoading })), _jsx(ChatMessages, { messages: chat.messages, contentFormat: contentFormat, renderContent: renderContent, showTools: showTools, toolNames: toolNames, toolRenderers: toolRenderers, emptyState: emptyState }), _jsx(TypingIndicator, { visible: chat.isLoading, label: chat.turnCount > 0
48
53
  ? (processingLabel
49
54
  ? processingLabel(chat.turnCount)
50
55
  : `Processing turn ${chat.turnCount}...`)
51
- : undefined }), _jsx(ChatInput, { onSend: chat.sendMessage, disabled: chat.isLoading, placeholder: placeholder, allowAttachments: allowAttachments, accept: acceptFileTypes })] }) }) }));
56
+ : loadingMessagesConfig
57
+ ? cycling.message
58
+ : undefined }), _jsx(ChatInput, { onSend: chat.sendMessage, disabled: chat.isLoading, placeholder: placeholder, allowAttachments: allowAttachments, accept: acceptFileTypes })] }) }) }));
52
59
  }
@@ -18,8 +18,7 @@ export interface SessionSwitcherProps {
18
18
  /**
19
19
  * Session switcher dropdown.
20
20
  *
21
- * Renders a list of sessions with the active one highlighted.
22
- * Only rendered when the adapter declares `capabilities.sessions = true`.
21
+ * Shared default for browsing saved chat history without noisy horizontal chips.
23
22
  */
24
23
  export declare function SessionSwitcher({ sessions, activeSessionId, onSelect, onNew, onDelete, loading, className, }: SessionSwitcherProps): import("react/jsx-runtime").JSX.Element;
25
24
  //# sourceMappingURL=SessionSwitcher.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"SessionSwitcher.d.ts","sourceRoot":"","sources":["../../src/components/SessionSwitcher.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,MAAM,WAAW,oBAAoB;IACpC,kCAAkC;IAClC,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,mCAAmC;IACnC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,yCAAyC;IACzC,QAAQ,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,0DAA0D;IAC1D,KAAK,CAAC,EAAE,MAAM,IAAI,CAAC;IACnB,sDAAsD;IACtD,QAAQ,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,8CAA8C;IAC9C,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,iCAAiC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,EAC/B,QAAQ,EACR,eAAe,EACf,QAAQ,EACR,KAAK,EACL,QAAQ,EACR,OAAe,EACf,SAAS,GACT,EAAE,oBAAoB,2CAyEtB"}
1
+ {"version":3,"file":"SessionSwitcher.d.ts","sourceRoot":"","sources":["../../src/components/SessionSwitcher.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,MAAM,WAAW,oBAAoB;IACpC,kCAAkC;IAClC,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,mCAAmC;IACnC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,yCAAyC;IACzC,QAAQ,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,0DAA0D;IAC1D,KAAK,CAAC,EAAE,MAAM,IAAI,CAAC;IACnB,sDAAsD;IACtD,QAAQ,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,8CAA8C;IAC9C,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,iCAAiC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,EAC/B,QAAQ,EACR,eAAe,EACf,QAAQ,EACR,KAAK,EACL,QAAQ,EACR,OAAe,EACf,SAAS,GACT,EAAE,oBAAoB,2CAuDtB"}
@@ -2,23 +2,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /**
3
3
  * Session switcher dropdown.
4
4
  *
5
- * Renders a list of sessions with the active one highlighted.
6
- * Only rendered when the adapter declares `capabilities.sessions = true`.
5
+ * Shared default for browsing saved chat history without noisy horizontal chips.
7
6
  */
8
7
  export function SessionSwitcher({ sessions, activeSessionId, onSelect, onNew, onDelete, loading = false, className, }) {
9
8
  const baseClass = 'ec-chat-sessions';
10
9
  const classes = [baseClass, className].filter(Boolean).join(' ');
11
- return (_jsxs("div", { className: classes, children: [_jsxs("div", { className: `${baseClass}__header`, children: [_jsx("span", { className: `${baseClass}__title`, children: "Conversations" }), onNew && (_jsx("button", { className: `${baseClass}__new`, onClick: onNew, "aria-label": "New conversation", type: "button", children: "+" }))] }), loading && (_jsx("div", { className: `${baseClass}__loading`, children: "Loading..." })), _jsx("ul", { className: `${baseClass}__list`, role: "listbox", "aria-label": "Chat sessions", children: sessions.map((session) => {
12
- const isActive = session.id === activeSessionId;
13
- const itemClass = [
14
- `${baseClass}__item`,
15
- isActive ? `${baseClass}__item--active` : '',
16
- ].filter(Boolean).join(' ');
17
- return (_jsxs("li", { className: itemClass, role: "option", "aria-selected": isActive, children: [_jsxs("button", { className: `${baseClass}__item-button`, onClick: () => onSelect(session.id), type: "button", children: [_jsx("span", { className: `${baseClass}__item-title`, children: session.title ?? `Session ${session.id.slice(0, 8)}` }), _jsx("time", { className: `${baseClass}__item-date`, dateTime: session.updatedAt, children: formatRelativeDate(session.updatedAt) })] }), onDelete && (_jsx("button", { className: `${baseClass}__item-delete`, onClick: (e) => {
18
- e.stopPropagation();
19
- onDelete(session.id);
20
- }, "aria-label": `Delete session ${session.title ?? session.id}`, type: "button", children: "\\u00D7" }))] }, session.id));
21
- }) })] }));
10
+ const currentSessionId = activeSessionId ?? sessions[0]?.id ?? '';
11
+ return (_jsxs("div", { className: classes, children: [_jsxs("div", { className: `${baseClass}__header`, children: [_jsx("span", { className: `${baseClass}__title`, children: "Conversations" }), onNew && (_jsx("button", { className: `${baseClass}__new`, onClick: onNew, "aria-label": "New conversation", type: "button", children: "+" }))] }), loading && _jsx("div", { className: `${baseClass}__loading`, children: "Loading..." }), sessions.length > 0 && (_jsxs("div", { className: `${baseClass}__controls`, children: [_jsxs("label", { className: `${baseClass}__select-wrap`, children: [_jsx("span", { className: "screen-reader-text", children: "Select conversation" }), _jsx("select", { className: `${baseClass}__select`, value: currentSessionId, onChange: (event) => onSelect(event.target.value), disabled: loading, children: sessions.map((session) => (_jsxs("option", { value: session.id, children: [session.title ?? `Session ${session.id.slice(0, 8)}`, " \u2014 ", formatRelativeDate(session.updatedAt)] }, session.id))) })] }), onDelete && currentSessionId && (_jsx("button", { className: `${baseClass}__delete`, onClick: () => onDelete(currentSessionId), "aria-label": "Delete selected conversation", type: "button", children: "Delete" }))] }))] }));
22
12
  }
23
13
  function formatRelativeDate(iso) {
24
14
  try {
@@ -10,7 +10,9 @@ export interface TypingIndicatorProps {
10
10
  * Animated typing indicator with three bouncing dots.
11
11
  *
12
12
  * Renders as an assistant-style message bubble.
13
- * The animation is pure CSS — no JS timers.
13
+ * The dot animation is pure CSS — no JS timers.
14
+ *
15
+ * When `label` changes, the text crossfades out/in via CSS transition.
14
16
  */
15
17
  export declare function TypingIndicator({ visible, label, className, }: TypingIndicatorProps): import("react/jsx-runtime").JSX.Element | null;
16
18
  //# sourceMappingURL=TypingIndicator.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"TypingIndicator.d.ts","sourceRoot":"","sources":["../../src/components/TypingIndicator.tsx"],"names":[],"mappings":"AAAA,MAAM,WAAW,oBAAoB;IACpC,wCAAwC;IACxC,OAAO,EAAE,OAAO,CAAC;IACjB,yDAAyD;IACzD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iCAAiC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,EAC/B,OAAO,EACP,KAAK,EACL,SAAS,GACT,EAAE,oBAAoB,kDAgBtB"}
1
+ {"version":3,"file":"TypingIndicator.d.ts","sourceRoot":"","sources":["../../src/components/TypingIndicator.tsx"],"names":[],"mappings":"AAEA,MAAM,WAAW,oBAAoB;IACpC,wCAAwC;IACxC,OAAO,EAAE,OAAO,CAAC;IACjB,yDAAyD;IACzD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iCAAiC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,EAC/B,OAAO,EACP,KAAK,EACL,SAAS,GACT,EAAE,oBAAoB,kDAgDtB"}
@@ -1,14 +1,44 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useEffect, useRef } from 'react';
2
3
  /**
3
4
  * Animated typing indicator with three bouncing dots.
4
5
  *
5
6
  * Renders as an assistant-style message bubble.
6
- * The animation is pure CSS — no JS timers.
7
+ * The dot animation is pure CSS — no JS timers.
8
+ *
9
+ * When `label` changes, the text crossfades out/in via CSS transition.
7
10
  */
8
11
  export function TypingIndicator({ visible, label, className, }) {
12
+ // Track displayed label separately so we can fade out/in on change.
13
+ const [displayLabel, setDisplayLabel] = useState(label);
14
+ const [fading, setFading] = useState(false);
15
+ const timeoutRef = useRef();
16
+ useEffect(() => {
17
+ if (label === displayLabel)
18
+ return;
19
+ // Start fade-out, then swap text and fade-in.
20
+ setFading(true);
21
+ clearTimeout(timeoutRef.current);
22
+ timeoutRef.current = setTimeout(() => {
23
+ setDisplayLabel(label);
24
+ setFading(false);
25
+ }, 150); // matches CSS transition duration
26
+ return () => clearTimeout(timeoutRef.current);
27
+ }, [label, displayLabel]);
28
+ // Reset when indicator hides.
29
+ useEffect(() => {
30
+ if (!visible) {
31
+ setDisplayLabel(undefined);
32
+ setFading(false);
33
+ }
34
+ }, [visible]);
9
35
  if (!visible)
10
36
  return null;
11
37
  const baseClass = 'ec-chat-typing';
12
38
  const classes = [baseClass, className].filter(Boolean).join(' ');
13
- return (_jsxs("div", { className: classes, role: "status", "aria-label": "Assistant is typing", children: [_jsxs("div", { className: `${baseClass}__dots`, children: [_jsx("span", { className: `${baseClass}__dot` }), _jsx("span", { className: `${baseClass}__dot` }), _jsx("span", { className: `${baseClass}__dot` })] }), label && _jsx("span", { className: `${baseClass}__label`, children: label })] }));
39
+ const labelClasses = [
40
+ `${baseClass}__label`,
41
+ fading ? `${baseClass}__label--fading` : '',
42
+ ].filter(Boolean).join(' ');
43
+ return (_jsxs("div", { className: classes, role: "status", "aria-label": "Assistant is typing", children: [_jsxs("div", { className: `${baseClass}__dots`, children: [_jsx("span", { className: `${baseClass}__dot` }), _jsx("span", { className: `${baseClass}__dot` }), _jsx("span", { className: `${baseClass}__dot` })] }), displayLabel && _jsx("span", { className: labelClasses, children: displayLabel })] }));
14
44
  }
@@ -1,2 +1,3 @@
1
1
  export { useChat, type UseChatOptions, type UseChatReturn } from './useChat.ts';
2
+ export { useLoadingMessages, DEFAULT_LOADING_MESSAGES, type LoadingMessagesConfig, type UseLoadingMessagesReturn, } from './useLoadingMessages.ts';
2
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,aAAa,EAAE,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,aAAa,EAAE,MAAM,cAAc,CAAC;AAChF,OAAO,EACN,kBAAkB,EAClB,wBAAwB,EACxB,KAAK,qBAAqB,EAC1B,KAAK,wBAAwB,GAC7B,MAAM,yBAAyB,CAAC"}
@@ -1 +1,2 @@
1
1
  export { useChat } from "./useChat.js";
2
+ export { useLoadingMessages, DEFAULT_LOADING_MESSAGES, } from "./useLoadingMessages.js";
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Default pool of loading messages.
3
+ *
4
+ * Music-forward, Extra-Chill-flavored, but generic enough for any consumer.
5
+ * Consumers can extend or replace this list entirely.
6
+ */
7
+ export declare const DEFAULT_LOADING_MESSAGES: readonly string[];
8
+ /**
9
+ * Configuration for loading message behavior.
10
+ */
11
+ export interface LoadingMessagesConfig {
12
+ /**
13
+ * How to handle the provided messages relative to the defaults.
14
+ *
15
+ * - `'default'` — Use only the built-in pool (ignore `messages`).
16
+ * - `'extend'` — Merge `messages` into the built-in pool.
17
+ * - `'override'` — Replace the built-in pool entirely with `messages`.
18
+ *
19
+ * @default 'default'
20
+ */
21
+ mode?: 'default' | 'extend' | 'override';
22
+ /**
23
+ * Custom messages to add or replace the defaults with.
24
+ * Ignored when `mode` is `'default'`.
25
+ */
26
+ messages?: string[];
27
+ /**
28
+ * How often (ms) to cycle to the next message.
29
+ * @default 3000
30
+ */
31
+ interval?: number;
32
+ }
33
+ export interface UseLoadingMessagesReturn {
34
+ /** The current loading message to display. Changes on each cycle. */
35
+ message: string;
36
+ }
37
+ /**
38
+ * Cycles through a pool of loading messages while `active` is true.
39
+ *
40
+ * On activation, the pool is shuffled and the first message is shown
41
+ * immediately. Every `interval` ms, the next message in the shuffled
42
+ * order is displayed. When the pool is exhausted, it re-shuffles and
43
+ * continues from the top.
44
+ *
45
+ * ## Usage modes
46
+ *
47
+ * ```tsx
48
+ * // 1. Defaults — built-in pool
49
+ * const { message } = useLoadingMessages(chat.isLoading);
50
+ *
51
+ * // 2. Extend — add your own on top of defaults
52
+ * const { message } = useLoadingMessages(chat.isLoading, {
53
+ * mode: 'extend',
54
+ * messages: ['Summoning the muse…', 'Consulting the oracle…'],
55
+ * });
56
+ *
57
+ * // 3. Override — your pool only
58
+ * const { message } = useLoadingMessages(chat.isLoading, {
59
+ * mode: 'override',
60
+ * messages: ['Searching…', 'Analyzing…', 'Compiling…'],
61
+ * });
62
+ * ```
63
+ */
64
+ export declare function useLoadingMessages(active: boolean, config?: LoadingMessagesConfig): UseLoadingMessagesReturn;
65
+ //# sourceMappingURL=useLoadingMessages.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useLoadingMessages.d.ts","sourceRoot":"","sources":["../../src/hooks/useLoadingMessages.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,EAAE,SAAS,MAAM,EAkBrD,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACrC;;;;;;;;OAQG;IACH,IAAI,CAAC,EAAE,SAAS,GAAG,QAAQ,GAAG,UAAU,CAAC;IACzC;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,wBAAwB;IACxC,qEAAqE;IACrE,OAAO,EAAE,MAAM,CAAC;CAChB;AAeD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,kBAAkB,CACjC,MAAM,EAAE,OAAO,EACf,MAAM,CAAC,EAAE,qBAAqB,GAC5B,wBAAwB,CA0D1B"}
@@ -0,0 +1,117 @@
1
+ import { useState, useEffect, useRef, useCallback } from 'react';
2
+ /**
3
+ * Default pool of loading messages.
4
+ *
5
+ * Music-forward, Extra-Chill-flavored, but generic enough for any consumer.
6
+ * Consumers can extend or replace this list entirely.
7
+ */
8
+ export const DEFAULT_LOADING_MESSAGES = [
9
+ 'Thinking…',
10
+ 'Working on it…',
11
+ 'Tuning in…',
12
+ 'Spinning the record…',
13
+ 'Vibing on it…',
14
+ 'Getting in the groove…',
15
+ 'Mixing it together…',
16
+ 'Letting it marinate…',
17
+ 'Finding the right note…',
18
+ 'Warming up…',
19
+ 'Loading the setlist…',
20
+ 'Checking the tracklist…',
21
+ 'Cueing up…',
22
+ 'On it…',
23
+ 'One sec…',
24
+ 'Almost there…',
25
+ 'Cooking something up…',
26
+ ];
27
+ /**
28
+ * Shuffle an array using Fisher-Yates.
29
+ * Returns a new array — does not mutate the input.
30
+ */
31
+ function shuffle(array) {
32
+ const result = [...array];
33
+ for (let i = result.length - 1; i > 0; i--) {
34
+ const j = Math.floor(Math.random() * (i + 1));
35
+ [result[i], result[j]] = [result[j], result[i]];
36
+ }
37
+ return result;
38
+ }
39
+ /**
40
+ * Cycles through a pool of loading messages while `active` is true.
41
+ *
42
+ * On activation, the pool is shuffled and the first message is shown
43
+ * immediately. Every `interval` ms, the next message in the shuffled
44
+ * order is displayed. When the pool is exhausted, it re-shuffles and
45
+ * continues from the top.
46
+ *
47
+ * ## Usage modes
48
+ *
49
+ * ```tsx
50
+ * // 1. Defaults — built-in pool
51
+ * const { message } = useLoadingMessages(chat.isLoading);
52
+ *
53
+ * // 2. Extend — add your own on top of defaults
54
+ * const { message } = useLoadingMessages(chat.isLoading, {
55
+ * mode: 'extend',
56
+ * messages: ['Summoning the muse…', 'Consulting the oracle…'],
57
+ * });
58
+ *
59
+ * // 3. Override — your pool only
60
+ * const { message } = useLoadingMessages(chat.isLoading, {
61
+ * mode: 'override',
62
+ * messages: ['Searching…', 'Analyzing…', 'Compiling…'],
63
+ * });
64
+ * ```
65
+ */
66
+ export function useLoadingMessages(active, config) {
67
+ const mode = config?.mode ?? 'default';
68
+ const customMessages = config?.messages;
69
+ const interval = config?.interval ?? 3000;
70
+ // Build the resolved pool once per config change.
71
+ const poolRef = useRef(DEFAULT_LOADING_MESSAGES);
72
+ // Track the shuffled queue and current index.
73
+ const queueRef = useRef([]);
74
+ const indexRef = useRef(0);
75
+ const [message, setMessage] = useState('');
76
+ // Resolve pool when config changes.
77
+ useEffect(() => {
78
+ if (mode === 'override' && customMessages?.length) {
79
+ poolRef.current = customMessages;
80
+ }
81
+ else if (mode === 'extend' && customMessages?.length) {
82
+ // Deduplicate: defaults first, then custom entries not already present.
83
+ const set = new Set(DEFAULT_LOADING_MESSAGES);
84
+ const extras = customMessages.filter((m) => !set.has(m));
85
+ poolRef.current = [...DEFAULT_LOADING_MESSAGES, ...extras];
86
+ }
87
+ else {
88
+ poolRef.current = DEFAULT_LOADING_MESSAGES;
89
+ }
90
+ }, [mode, customMessages]);
91
+ /**
92
+ * Get the next message from the shuffled queue.
93
+ * Re-shuffles when the queue is exhausted.
94
+ */
95
+ const next = useCallback(() => {
96
+ if (queueRef.current.length === 0 || indexRef.current >= queueRef.current.length) {
97
+ queueRef.current = shuffle(poolRef.current);
98
+ indexRef.current = 0;
99
+ }
100
+ const msg = queueRef.current[indexRef.current];
101
+ indexRef.current++;
102
+ return msg;
103
+ }, []);
104
+ // When active flips on, show the first message immediately and start cycling.
105
+ // When active flips off, stop the timer.
106
+ useEffect(() => {
107
+ if (!active)
108
+ return;
109
+ // Show first message immediately.
110
+ setMessage(next());
111
+ const timer = setInterval(() => {
112
+ setMessage(next());
113
+ }, interval);
114
+ return () => clearInterval(timer);
115
+ }, [active, interval, next]);
116
+ return { message };
117
+ }
package/dist/index.d.ts CHANGED
@@ -17,5 +17,6 @@ export { SessionSwitcher, type SessionSwitcherProps, } from './components/Sessio
17
17
  export { ErrorBoundary, type ErrorBoundaryProps, } from './components/ErrorBoundary.tsx';
18
18
  export { AvailabilityGate, type AvailabilityGateProps, } from './components/AvailabilityGate.tsx';
19
19
  export { useChat, type UseChatOptions, type UseChatReturn, } from './hooks/useChat.ts';
20
+ export { useLoadingMessages, DEFAULT_LOADING_MESSAGES, type LoadingMessagesConfig, type UseLoadingMessagesReturn, } from './hooks/useLoadingMessages.ts';
20
21
  export { Chat, type ChatProps, type ChatSessionUi } from './Chat.tsx';
21
22
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,YAAY,EACX,WAAW,EACX,QAAQ,EACR,cAAc,EACd,eAAe,EACf,WAAW,EACX,aAAa,EACb,WAAW,EACX,gBAAgB,EAChB,gBAAgB,EAChB,aAAa,EACb,UAAU,EACV,UAAU,EACV,eAAe,GACf,MAAM,kBAAkB,CAAC;AAG1B,YAAY,EAAE,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AACjH,OAAO,EACN,WAAW,EACX,gBAAgB,EAChB,YAAY,EACZ,WAAW,EACX,aAAa,GACb,MAAM,UAAU,CAAC;AAGlB,OAAO,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAG5F,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAG/C,OAAO,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAG3E,OAAO,EACN,kBAAkB,EAClB,0BAA0B,EAC1B,+BAA+B,EAC/B,KAAK,iBAAiB,EACtB,KAAK,uBAAuB,EAC5B,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,GACtB,MAAM,WAAW,CAAC;AAGnB,OAAO,EACN,gCAAgC,EAChC,6BAA6B,EAC7B,wBAAwB,EACxB,wBAAwB,EACxB,KAAK,qBAAqB,EAC1B,KAAK,6BAA6B,EAClC,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,GAC1B,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EACN,WAAW,IAAI,oBAAoB,EACnC,KAAK,gBAAgB,GACrB,MAAM,8BAA8B,CAAC;AAEtC,OAAO,EACN,YAAY,EACZ,KAAK,iBAAiB,GACtB,MAAM,+BAA+B,CAAC;AAEvC,OAAO,EACN,SAAS,EACT,KAAK,cAAc,GACnB,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EACN,oBAAoB,EACpB,KAAK,yBAAyB,GAC9B,MAAM,uCAAuC,CAAC;AAE/C,OAAO,EACN,WAAW,EACX,KAAK,gBAAgB,EACrB,KAAK,SAAS,GACd,MAAM,8BAA8B,CAAC;AAEtC,OAAO,EACN,QAAQ,EACR,KAAK,aAAa,EAClB,KAAK,QAAQ,GACb,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EACN,eAAe,EACf,KAAK,oBAAoB,GACzB,MAAM,kCAAkC,CAAC;AAE1C,OAAO,EACN,eAAe,EACf,KAAK,oBAAoB,GACzB,MAAM,kCAAkC,CAAC;AAE1C,OAAO,EACN,aAAa,EACb,KAAK,kBAAkB,GACvB,MAAM,gCAAgC,CAAC;AAExC,OAAO,EACN,gBAAgB,EAChB,KAAK,qBAAqB,GAC1B,MAAM,mCAAmC,CAAC;AAG3C,OAAO,EACN,OAAO,EACP,KAAK,cAAc,EACnB,KAAK,aAAa,GAClB,MAAM,oBAAoB,CAAC;AAG5B,OAAO,EAAE,IAAI,EAAE,KAAK,SAAS,EAAE,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,YAAY,EACX,WAAW,EACX,QAAQ,EACR,cAAc,EACd,eAAe,EACf,WAAW,EACX,aAAa,EACb,WAAW,EACX,gBAAgB,EAChB,gBAAgB,EAChB,aAAa,EACb,UAAU,EACV,UAAU,EACV,eAAe,GACf,MAAM,kBAAkB,CAAC;AAG1B,YAAY,EAAE,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AACjH,OAAO,EACN,WAAW,EACX,gBAAgB,EAChB,YAAY,EACZ,WAAW,EACX,aAAa,GACb,MAAM,UAAU,CAAC;AAGlB,OAAO,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAG5F,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAG/C,OAAO,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAG3E,OAAO,EACN,kBAAkB,EAClB,0BAA0B,EAC1B,+BAA+B,EAC/B,KAAK,iBAAiB,EACtB,KAAK,uBAAuB,EAC5B,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,GACtB,MAAM,WAAW,CAAC;AAGnB,OAAO,EACN,gCAAgC,EAChC,6BAA6B,EAC7B,wBAAwB,EACxB,wBAAwB,EACxB,KAAK,qBAAqB,EAC1B,KAAK,6BAA6B,EAClC,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,GAC1B,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EACN,WAAW,IAAI,oBAAoB,EACnC,KAAK,gBAAgB,GACrB,MAAM,8BAA8B,CAAC;AAEtC,OAAO,EACN,YAAY,EACZ,KAAK,iBAAiB,GACtB,MAAM,+BAA+B,CAAC;AAEvC,OAAO,EACN,SAAS,EACT,KAAK,cAAc,GACnB,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EACN,oBAAoB,EACpB,KAAK,yBAAyB,GAC9B,MAAM,uCAAuC,CAAC;AAE/C,OAAO,EACN,WAAW,EACX,KAAK,gBAAgB,EACrB,KAAK,SAAS,GACd,MAAM,8BAA8B,CAAC;AAEtC,OAAO,EACN,QAAQ,EACR,KAAK,aAAa,EAClB,KAAK,QAAQ,GACb,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EACN,eAAe,EACf,KAAK,oBAAoB,GACzB,MAAM,kCAAkC,CAAC;AAE1C,OAAO,EACN,eAAe,EACf,KAAK,oBAAoB,GACzB,MAAM,kCAAkC,CAAC;AAE1C,OAAO,EACN,aAAa,EACb,KAAK,kBAAkB,GACvB,MAAM,gCAAgC,CAAC;AAExC,OAAO,EACN,gBAAgB,EAChB,KAAK,qBAAqB,GAC1B,MAAM,mCAAmC,CAAC;AAG3C,OAAO,EACN,OAAO,EACP,KAAK,cAAc,EACnB,KAAK,aAAa,GAClB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACN,kBAAkB,EAClB,wBAAwB,EACxB,KAAK,qBAAqB,EAC1B,KAAK,wBAAwB,GAC7B,MAAM,+BAA+B,CAAC;AAGvC,OAAO,EAAE,IAAI,EAAE,KAAK,SAAS,EAAE,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -20,7 +20,8 @@ export { TypingIndicator, } from "./components/TypingIndicator.js";
20
20
  export { SessionSwitcher, } from "./components/SessionSwitcher.js";
21
21
  export { ErrorBoundary, } from "./components/ErrorBoundary.js";
22
22
  export { AvailabilityGate, } from "./components/AvailabilityGate.js";
23
- // Hook
23
+ // Hooks
24
24
  export { useChat, } from "./hooks/useChat.js";
25
+ export { useLoadingMessages, DEFAULT_LOADING_MESSAGES, } from "./hooks/useLoadingMessages.js";
25
26
  // Composed
26
27
  export { Chat } from "./Chat.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@extrachill/chat",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Chat UI components with built-in REST API client. Speaks the standard chat message format natively.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/src/Chat.tsx CHANGED
@@ -3,13 +3,13 @@ import type { ChatMessage as ChatMessageType, ContentFormat } from './types/inde
3
3
  import type { FetchFn } from './api.ts';
4
4
  import type { ToolGroup } from './components/ToolMessage.tsx';
5
5
  import { useChat, type UseChatOptions } from './hooks/useChat.ts';
6
+ import { useLoadingMessages, type LoadingMessagesConfig } from './hooks/useLoadingMessages.ts';
6
7
  import { ErrorBoundary } from './components/ErrorBoundary.tsx';
7
8
  import { AvailabilityGate } from './components/AvailabilityGate.tsx';
8
9
  import { ChatMessages } from './components/ChatMessages.tsx';
9
10
  import { ChatInput } from './components/ChatInput.tsx';
10
11
  import { TypingIndicator } from './components/TypingIndicator.tsx';
11
12
  import { SessionSwitcher } from './components/SessionSwitcher.tsx';
12
- import { CopyTranscriptButton } from './components/CopyTranscriptButton.tsx';
13
13
  import type { UseChatReturn } from './hooks/useChat.ts';
14
14
 
15
15
  export type ChatSessionUi = 'list' | 'none';
@@ -67,6 +67,18 @@ export interface ChatProps {
67
67
  sessionUi?: ChatSessionUi;
68
68
  /** Label shown during multi-turn processing. */
69
69
  processingLabel?: (turnCount: number) => string;
70
+ /**
71
+ * Cycling loading messages shown while the assistant is thinking.
72
+ *
73
+ * - `true` — enable with built-in defaults.
74
+ * - `LoadingMessagesConfig` — extend or override the default pool.
75
+ * - `false` / `undefined` — disabled (dots only, original behavior).
76
+ *
77
+ * When enabled alongside `processingLabel`, loading messages are shown
78
+ * on the initial turn (turnCount === 0) and `processingLabel` takes
79
+ * over during multi-turn continuation.
80
+ */
81
+ loadingMessages?: boolean | LoadingMessagesConfig;
70
82
  /** Whether to show the attachment button in the input. Defaults to true. */
71
83
  allowAttachments?: boolean;
72
84
  /** Accepted file types for attachments. Defaults to 'image/*,video/*'. */
@@ -76,11 +88,11 @@ export interface ChatProps {
76
88
  * Use for client-side context injection (e.g. `{ client_context: { tab: 'compose', postId: 123 } }`).
77
89
  */
78
90
  metadata?: Record<string, unknown>;
79
- /** Whether to show a built-in copy transcript button. Defaults to false. */
91
+ /** Deprecated: built-in copy transcript button UI is no longer rendered. */
80
92
  showCopyTranscript?: boolean;
81
- /** Label for the built-in copy transcript button. */
93
+ /** Deprecated legacy prop retained for compatibility. */
82
94
  copyTranscriptLabel?: string;
83
- /** Label shown after the transcript is copied. */
95
+ /** Deprecated legacy prop retained for compatibility. */
84
96
  copyTranscriptCopiedLabel?: string;
85
97
  /** Optional custom header/actions area rendered above messages with live chat state. */
86
98
  renderHeader?: ( chat: UseChatReturn ) => ReactNode;
@@ -129,6 +141,7 @@ export function Chat({
129
141
  showSessions = true,
130
142
  sessionUi = 'list',
131
143
  processingLabel,
144
+ loadingMessages,
132
145
  allowAttachments = true,
133
146
  acceptFileTypes,
134
147
  metadata,
@@ -149,6 +162,17 @@ export function Chat({
149
162
  metadata,
150
163
  });
151
164
 
165
+ // Resolve loading messages config.
166
+ const loadingMessagesConfig: LoadingMessagesConfig | undefined =
167
+ loadingMessages === true ? {} :
168
+ loadingMessages ? loadingMessages :
169
+ undefined;
170
+
171
+ const cycling = useLoadingMessages(
172
+ chat.isLoading && !!loadingMessagesConfig,
173
+ loadingMessagesConfig,
174
+ );
175
+
152
176
  const baseClass = 'ec-chat';
153
177
  const classes = [baseClass, className].filter(Boolean).join(' ');
154
178
 
@@ -158,16 +182,6 @@ export function Chat({
158
182
  <AvailabilityGate availability={chat.availability}>
159
183
  {renderHeader?.( chat )}
160
184
 
161
- {showCopyTranscript && (
162
- <div className="ec-chat__actions">
163
- <CopyTranscriptButton
164
- messages={chat.messages}
165
- label={copyTranscriptLabel}
166
- copiedLabel={copyTranscriptCopiedLabel}
167
- />
168
- </div>
169
- )}
170
-
171
185
  {showSessions && sessionUi === 'list' && (
172
186
  <SessionSwitcher
173
187
  sessions={chat.sessions}
@@ -196,7 +210,9 @@ export function Chat({
196
210
  ? (processingLabel
197
211
  ? processingLabel(chat.turnCount)
198
212
  : `Processing turn ${chat.turnCount}...`)
199
- : undefined
213
+ : loadingMessagesConfig
214
+ ? cycling.message
215
+ : undefined
200
216
  }
201
217
  />
202
218
 
@@ -20,8 +20,7 @@ export interface SessionSwitcherProps {
20
20
  /**
21
21
  * Session switcher dropdown.
22
22
  *
23
- * Renders a list of sessions with the active one highlighted.
24
- * Only rendered when the adapter declares `capabilities.sessions = true`.
23
+ * Shared default for browsing saved chat history without noisy horizontal chips.
25
24
  */
26
25
  export function SessionSwitcher({
27
26
  sessions,
@@ -34,6 +33,7 @@ export function SessionSwitcher({
34
33
  }: SessionSwitcherProps) {
35
34
  const baseClass = 'ec-chat-sessions';
36
35
  const classes = [baseClass, className].filter(Boolean).join(' ');
36
+ const currentSessionId = activeSessionId ?? sessions[0]?.id ?? '';
37
37
 
38
38
  return (
39
39
  <div className={classes}>
@@ -51,57 +51,38 @@ export function SessionSwitcher({
51
51
  )}
52
52
  </div>
53
53
 
54
- {loading && (
55
- <div className={`${baseClass}__loading`}>Loading...</div>
56
- )}
54
+ {loading && <div className={`${baseClass}__loading`}>Loading...</div>}
57
55
 
58
- <ul className={`${baseClass}__list`} role="listbox" aria-label="Chat sessions">
59
- {sessions.map((session) => {
60
- const isActive = session.id === activeSessionId;
61
- const itemClass = [
62
- `${baseClass}__item`,
63
- isActive ? `${baseClass}__item--active` : '',
64
- ].filter(Boolean).join(' ');
56
+ {sessions.length > 0 && (
57
+ <div className={`${baseClass}__controls`}>
58
+ <label className={`${baseClass}__select-wrap`}>
59
+ <span className="screen-reader-text">Select conversation</span>
60
+ <select
61
+ className={`${baseClass}__select`}
62
+ value={currentSessionId}
63
+ onChange={(event) => onSelect(event.target.value)}
64
+ disabled={loading}
65
+ >
66
+ {sessions.map((session) => (
67
+ <option key={session.id} value={session.id}>
68
+ {session.title ?? `Session ${session.id.slice(0, 8)}`} — {formatRelativeDate(session.updatedAt)}
69
+ </option>
70
+ ))}
71
+ </select>
72
+ </label>
65
73
 
66
- return (
67
- <li
68
- key={session.id}
69
- className={itemClass}
70
- role="option"
71
- aria-selected={isActive}
74
+ {onDelete && currentSessionId && (
75
+ <button
76
+ className={`${baseClass}__delete`}
77
+ onClick={() => onDelete(currentSessionId)}
78
+ aria-label="Delete selected conversation"
79
+ type="button"
72
80
  >
73
- <button
74
- className={`${baseClass}__item-button`}
75
- onClick={() => onSelect(session.id)}
76
- type="button"
77
- >
78
- <span className={`${baseClass}__item-title`}>
79
- {session.title ?? `Session ${session.id.slice(0, 8)}`}
80
- </span>
81
- <time
82
- className={`${baseClass}__item-date`}
83
- dateTime={session.updatedAt}
84
- >
85
- {formatRelativeDate(session.updatedAt)}
86
- </time>
87
- </button>
88
- {onDelete && (
89
- <button
90
- className={`${baseClass}__item-delete`}
91
- onClick={(e) => {
92
- e.stopPropagation();
93
- onDelete(session.id);
94
- }}
95
- aria-label={`Delete session ${session.title ?? session.id}`}
96
- type="button"
97
- >
98
- \u00D7
99
- </button>
100
- )}
101
- </li>
102
- );
103
- })}
104
- </ul>
81
+ Delete
82
+ </button>
83
+ )}
84
+ </div>
85
+ )}
105
86
  </div>
106
87
  );
107
88
  }
@@ -1,3 +1,5 @@
1
+ import { useState, useEffect, useRef } from 'react';
2
+
1
3
  export interface TypingIndicatorProps {
2
4
  /** Whether the indicator is visible. */
3
5
  visible: boolean;
@@ -11,17 +13,51 @@ export interface TypingIndicatorProps {
11
13
  * Animated typing indicator with three bouncing dots.
12
14
  *
13
15
  * Renders as an assistant-style message bubble.
14
- * The animation is pure CSS — no JS timers.
16
+ * The dot animation is pure CSS — no JS timers.
17
+ *
18
+ * When `label` changes, the text crossfades out/in via CSS transition.
15
19
  */
16
20
  export function TypingIndicator({
17
21
  visible,
18
22
  label,
19
23
  className,
20
24
  }: TypingIndicatorProps) {
25
+ // Track displayed label separately so we can fade out/in on change.
26
+ const [displayLabel, setDisplayLabel] = useState(label);
27
+ const [fading, setFading] = useState(false);
28
+ const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
29
+
30
+ useEffect(() => {
31
+ if (label === displayLabel) return;
32
+
33
+ // Start fade-out, then swap text and fade-in.
34
+ setFading(true);
35
+
36
+ clearTimeout(timeoutRef.current);
37
+ timeoutRef.current = setTimeout(() => {
38
+ setDisplayLabel(label);
39
+ setFading(false);
40
+ }, 150); // matches CSS transition duration
41
+
42
+ return () => clearTimeout(timeoutRef.current);
43
+ }, [label, displayLabel]);
44
+
45
+ // Reset when indicator hides.
46
+ useEffect(() => {
47
+ if (!visible) {
48
+ setDisplayLabel(undefined);
49
+ setFading(false);
50
+ }
51
+ }, [visible]);
52
+
21
53
  if (!visible) return null;
22
54
 
23
55
  const baseClass = 'ec-chat-typing';
24
56
  const classes = [baseClass, className].filter(Boolean).join(' ');
57
+ const labelClasses = [
58
+ `${baseClass}__label`,
59
+ fading ? `${baseClass}__label--fading` : '',
60
+ ].filter(Boolean).join(' ');
25
61
 
26
62
  return (
27
63
  <div className={classes} role="status" aria-label="Assistant is typing">
@@ -30,7 +66,7 @@ export function TypingIndicator({
30
66
  <span className={`${baseClass}__dot`} />
31
67
  <span className={`${baseClass}__dot`} />
32
68
  </div>
33
- {label && <span className={`${baseClass}__label`}>{label}</span>}
69
+ {displayLabel && <span className={labelClasses}>{displayLabel}</span>}
34
70
  </div>
35
71
  );
36
72
  }
@@ -1 +1,7 @@
1
1
  export { useChat, type UseChatOptions, type UseChatReturn } from './useChat.ts';
2
+ export {
3
+ useLoadingMessages,
4
+ DEFAULT_LOADING_MESSAGES,
5
+ type LoadingMessagesConfig,
6
+ type UseLoadingMessagesReturn,
7
+ } from './useLoadingMessages.ts';
@@ -0,0 +1,161 @@
1
+ import { useState, useEffect, useRef, useCallback } from 'react';
2
+
3
+ /**
4
+ * Default pool of loading messages.
5
+ *
6
+ * Music-forward, Extra-Chill-flavored, but generic enough for any consumer.
7
+ * Consumers can extend or replace this list entirely.
8
+ */
9
+ export const DEFAULT_LOADING_MESSAGES: readonly string[] = [
10
+ 'Thinking…',
11
+ 'Working on it…',
12
+ 'Tuning in…',
13
+ 'Spinning the record…',
14
+ 'Vibing on it…',
15
+ 'Getting in the groove…',
16
+ 'Mixing it together…',
17
+ 'Letting it marinate…',
18
+ 'Finding the right note…',
19
+ 'Warming up…',
20
+ 'Loading the setlist…',
21
+ 'Checking the tracklist…',
22
+ 'Cueing up…',
23
+ 'On it…',
24
+ 'One sec…',
25
+ 'Almost there…',
26
+ 'Cooking something up…',
27
+ ];
28
+
29
+ /**
30
+ * Configuration for loading message behavior.
31
+ */
32
+ export interface LoadingMessagesConfig {
33
+ /**
34
+ * How to handle the provided messages relative to the defaults.
35
+ *
36
+ * - `'default'` — Use only the built-in pool (ignore `messages`).
37
+ * - `'extend'` — Merge `messages` into the built-in pool.
38
+ * - `'override'` — Replace the built-in pool entirely with `messages`.
39
+ *
40
+ * @default 'default'
41
+ */
42
+ mode?: 'default' | 'extend' | 'override';
43
+ /**
44
+ * Custom messages to add or replace the defaults with.
45
+ * Ignored when `mode` is `'default'`.
46
+ */
47
+ messages?: string[];
48
+ /**
49
+ * How often (ms) to cycle to the next message.
50
+ * @default 3000
51
+ */
52
+ interval?: number;
53
+ }
54
+
55
+ export interface UseLoadingMessagesReturn {
56
+ /** The current loading message to display. Changes on each cycle. */
57
+ message: string;
58
+ }
59
+
60
+ /**
61
+ * Shuffle an array using Fisher-Yates.
62
+ * Returns a new array — does not mutate the input.
63
+ */
64
+ function shuffle<T>(array: readonly T[]): T[] {
65
+ const result = [...array];
66
+ for (let i = result.length - 1; i > 0; i--) {
67
+ const j = Math.floor(Math.random() * (i + 1));
68
+ [result[i], result[j]] = [result[j], result[i]];
69
+ }
70
+ return result;
71
+ }
72
+
73
+ /**
74
+ * Cycles through a pool of loading messages while `active` is true.
75
+ *
76
+ * On activation, the pool is shuffled and the first message is shown
77
+ * immediately. Every `interval` ms, the next message in the shuffled
78
+ * order is displayed. When the pool is exhausted, it re-shuffles and
79
+ * continues from the top.
80
+ *
81
+ * ## Usage modes
82
+ *
83
+ * ```tsx
84
+ * // 1. Defaults — built-in pool
85
+ * const { message } = useLoadingMessages(chat.isLoading);
86
+ *
87
+ * // 2. Extend — add your own on top of defaults
88
+ * const { message } = useLoadingMessages(chat.isLoading, {
89
+ * mode: 'extend',
90
+ * messages: ['Summoning the muse…', 'Consulting the oracle…'],
91
+ * });
92
+ *
93
+ * // 3. Override — your pool only
94
+ * const { message } = useLoadingMessages(chat.isLoading, {
95
+ * mode: 'override',
96
+ * messages: ['Searching…', 'Analyzing…', 'Compiling…'],
97
+ * });
98
+ * ```
99
+ */
100
+ export function useLoadingMessages(
101
+ active: boolean,
102
+ config?: LoadingMessagesConfig,
103
+ ): UseLoadingMessagesReturn {
104
+ const mode = config?.mode ?? 'default';
105
+ const customMessages = config?.messages;
106
+ const interval = config?.interval ?? 3000;
107
+
108
+ // Build the resolved pool once per config change.
109
+ const poolRef = useRef<readonly string[]>(DEFAULT_LOADING_MESSAGES);
110
+
111
+ // Track the shuffled queue and current index.
112
+ const queueRef = useRef<string[]>([]);
113
+ const indexRef = useRef(0);
114
+
115
+ const [message, setMessage] = useState('');
116
+
117
+ // Resolve pool when config changes.
118
+ useEffect(() => {
119
+ if (mode === 'override' && customMessages?.length) {
120
+ poolRef.current = customMessages;
121
+ } else if (mode === 'extend' && customMessages?.length) {
122
+ // Deduplicate: defaults first, then custom entries not already present.
123
+ const set = new Set(DEFAULT_LOADING_MESSAGES);
124
+ const extras = customMessages.filter((m) => !set.has(m));
125
+ poolRef.current = [...DEFAULT_LOADING_MESSAGES, ...extras];
126
+ } else {
127
+ poolRef.current = DEFAULT_LOADING_MESSAGES;
128
+ }
129
+ }, [mode, customMessages]);
130
+
131
+ /**
132
+ * Get the next message from the shuffled queue.
133
+ * Re-shuffles when the queue is exhausted.
134
+ */
135
+ const next = useCallback((): string => {
136
+ if (queueRef.current.length === 0 || indexRef.current >= queueRef.current.length) {
137
+ queueRef.current = shuffle(poolRef.current);
138
+ indexRef.current = 0;
139
+ }
140
+ const msg = queueRef.current[indexRef.current];
141
+ indexRef.current++;
142
+ return msg;
143
+ }, []);
144
+
145
+ // When active flips on, show the first message immediately and start cycling.
146
+ // When active flips off, stop the timer.
147
+ useEffect(() => {
148
+ if (!active) return;
149
+
150
+ // Show first message immediately.
151
+ setMessage(next());
152
+
153
+ const timer = setInterval(() => {
154
+ setMessage(next());
155
+ }, interval);
156
+
157
+ return () => clearInterval(timer);
158
+ }, [active, interval, next]);
159
+
160
+ return { message };
161
+ }
package/src/index.ts CHANGED
@@ -111,12 +111,19 @@ export {
111
111
  type AvailabilityGateProps,
112
112
  } from './components/AvailabilityGate.tsx';
113
113
 
114
- // Hook
114
+ // Hooks
115
115
  export {
116
116
  useChat,
117
117
  type UseChatOptions,
118
118
  type UseChatReturn,
119
119
  } from './hooks/useChat.ts';
120
120
 
121
+ export {
122
+ useLoadingMessages,
123
+ DEFAULT_LOADING_MESSAGES,
124
+ type LoadingMessagesConfig,
125
+ type UseLoadingMessagesReturn,
126
+ } from './hooks/useLoadingMessages.ts';
127
+
121
128
  // Composed
122
129
  export { Chat, type ChatProps, type ChatSessionUi } from './Chat.tsx';