@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 +15 -0
- package/css/chat.css +29 -52
- package/dist/Chat.d.ts +17 -4
- package/dist/Chat.d.ts.map +1 -1
- package/dist/Chat.js +11 -4
- package/dist/components/SessionSwitcher.d.ts +1 -2
- package/dist/components/SessionSwitcher.d.ts.map +1 -1
- package/dist/components/SessionSwitcher.js +3 -13
- package/dist/components/TypingIndicator.d.ts +3 -1
- package/dist/components/TypingIndicator.d.ts.map +1 -1
- package/dist/components/TypingIndicator.js +32 -2
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/useLoadingMessages.d.ts +65 -0
- package/dist/hooks/useLoadingMessages.d.ts.map +1 -0
- package/dist/hooks/useLoadingMessages.js +117 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/package.json +1 -1
- package/src/Chat.tsx +31 -15
- package/src/components/SessionSwitcher.tsx +31 -50
- package/src/components/TypingIndicator.tsx +38 -2
- package/src/hooks/index.ts +6 -0
- package/src/hooks/useLoadingMessages.ts +161 -0
- package/src/index.ts +8 -1
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-
|
|
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-
|
|
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-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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-
|
|
583
|
-
|
|
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-
|
|
599
|
-
color: var(--ec-chat-
|
|
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
|
-
/**
|
|
83
|
+
/** Deprecated: built-in copy transcript button UI is no longer rendered. */
|
|
71
84
|
showCopyTranscript?: boolean;
|
|
72
|
-
/**
|
|
85
|
+
/** Deprecated legacy prop retained for compatibility. */
|
|
73
86
|
copyTranscriptLabel?: string;
|
|
74
|
-
/**
|
|
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
|
package/dist/Chat.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Chat.d.ts","sourceRoot":"","sources":["../src/Chat.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,WAAW,IAAI,eAAe,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtF,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AAC9D,OAAO,EAAW,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAC;
|
|
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),
|
|
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
|
-
:
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
12
|
-
|
|
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":"
|
|
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
|
-
|
|
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
|
}
|
package/dist/hooks/index.d.ts
CHANGED
|
@@ -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"}
|
package/dist/hooks/index.js
CHANGED
|
@@ -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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
-
//
|
|
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
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
|
-
/**
|
|
91
|
+
/** Deprecated: built-in copy transcript button UI is no longer rendered. */
|
|
80
92
|
showCopyTranscript?: boolean;
|
|
81
|
-
/**
|
|
93
|
+
/** Deprecated legacy prop retained for compatibility. */
|
|
82
94
|
copyTranscriptLabel?: string;
|
|
83
|
-
/**
|
|
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
|
-
:
|
|
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
|
-
*
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
67
|
-
<
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
{
|
|
69
|
+
{displayLabel && <span className={labelClasses}>{displayLabel}</span>}
|
|
34
70
|
</div>
|
|
35
71
|
);
|
|
36
72
|
}
|
package/src/hooks/index.ts
CHANGED
|
@@ -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
|
-
//
|
|
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';
|