@extrachill/chat 0.2.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.
Files changed (83) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +154 -0
  3. package/css/chat.css +552 -0
  4. package/dist/Chat.d.ts +73 -0
  5. package/dist/Chat.d.ts.map +1 -0
  6. package/dist/Chat.js +50 -0
  7. package/dist/api.d.ts +68 -0
  8. package/dist/api.d.ts.map +1 -0
  9. package/dist/api.js +93 -0
  10. package/dist/components/AvailabilityGate.d.ts +19 -0
  11. package/dist/components/AvailabilityGate.d.ts.map +1 -0
  12. package/dist/components/AvailabilityGate.js +32 -0
  13. package/dist/components/ChatInput.d.ts +21 -0
  14. package/dist/components/ChatInput.d.ts.map +1 -0
  15. package/dist/components/ChatInput.js +52 -0
  16. package/dist/components/ChatMessage.d.ts +23 -0
  17. package/dist/components/ChatMessage.d.ts.map +1 -0
  18. package/dist/components/ChatMessage.js +34 -0
  19. package/dist/components/ChatMessages.d.ts +28 -0
  20. package/dist/components/ChatMessages.d.ts.map +1 -0
  21. package/dist/components/ChatMessages.js +121 -0
  22. package/dist/components/ErrorBoundary.d.ts +27 -0
  23. package/dist/components/ErrorBoundary.d.ts.map +1 -0
  24. package/dist/components/ErrorBoundary.js +34 -0
  25. package/dist/components/SessionSwitcher.d.ts +25 -0
  26. package/dist/components/SessionSwitcher.d.ts.map +1 -0
  27. package/dist/components/SessionSwitcher.js +44 -0
  28. package/dist/components/ToolMessage.d.ts +34 -0
  29. package/dist/components/ToolMessage.d.ts.map +1 -0
  30. package/dist/components/ToolMessage.js +39 -0
  31. package/dist/components/TypingIndicator.d.ts +16 -0
  32. package/dist/components/TypingIndicator.d.ts.map +1 -0
  33. package/dist/components/TypingIndicator.js +14 -0
  34. package/dist/components/index.d.ts +9 -0
  35. package/dist/components/index.d.ts.map +1 -0
  36. package/dist/components/index.js +8 -0
  37. package/dist/hooks/index.d.ts +2 -0
  38. package/dist/hooks/index.d.ts.map +1 -0
  39. package/dist/hooks/index.js +1 -0
  40. package/dist/hooks/useChat.d.ts +102 -0
  41. package/dist/hooks/useChat.d.ts.map +1 -0
  42. package/dist/hooks/useChat.js +192 -0
  43. package/dist/index.d.ts +15 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +16 -0
  46. package/dist/normalizer.d.ts +24 -0
  47. package/dist/normalizer.d.ts.map +1 -0
  48. package/dist/normalizer.js +96 -0
  49. package/dist/types/adapter.d.ts +151 -0
  50. package/dist/types/adapter.d.ts.map +1 -0
  51. package/dist/types/adapter.js +11 -0
  52. package/dist/types/api.d.ts +137 -0
  53. package/dist/types/api.d.ts.map +1 -0
  54. package/dist/types/api.js +8 -0
  55. package/dist/types/index.d.ts +4 -0
  56. package/dist/types/index.d.ts.map +1 -0
  57. package/dist/types/index.js +1 -0
  58. package/dist/types/message.d.ts +62 -0
  59. package/dist/types/message.d.ts.map +1 -0
  60. package/dist/types/message.js +7 -0
  61. package/dist/types/session.d.ts +59 -0
  62. package/dist/types/session.d.ts.map +1 -0
  63. package/dist/types/session.js +7 -0
  64. package/package.json +61 -0
  65. package/src/Chat.tsx +157 -0
  66. package/src/api.ts +173 -0
  67. package/src/components/AvailabilityGate.tsx +85 -0
  68. package/src/components/ChatInput.tsx +114 -0
  69. package/src/components/ChatMessage.tsx +85 -0
  70. package/src/components/ChatMessages.tsx +193 -0
  71. package/src/components/ErrorBoundary.tsx +66 -0
  72. package/src/components/SessionSwitcher.tsx +129 -0
  73. package/src/components/ToolMessage.tsx +112 -0
  74. package/src/components/TypingIndicator.tsx +36 -0
  75. package/src/components/index.ts +8 -0
  76. package/src/hooks/index.ts +1 -0
  77. package/src/hooks/useChat.ts +310 -0
  78. package/src/index.ts +79 -0
  79. package/src/normalizer.ts +112 -0
  80. package/src/types/api.ts +146 -0
  81. package/src/types/index.ts +26 -0
  82. package/src/types/message.ts +66 -0
  83. package/src/types/session.ts +50 -0
package/dist/Chat.d.ts ADDED
@@ -0,0 +1,73 @@
1
+ import { type ReactNode } from 'react';
2
+ import type { ChatMessage as ChatMessageType, ContentFormat } from './types/index.ts';
3
+ import type { FetchFn } from './api.ts';
4
+ import { type UseChatOptions } from './hooks/useChat.ts';
5
+ export interface ChatProps {
6
+ /**
7
+ * Base path for the chat REST endpoints.
8
+ * e.g. '/datamachine/v1/chat'
9
+ */
10
+ basePath: string;
11
+ /**
12
+ * Fetch function for API calls. Must accept { path, method?, data? }
13
+ * and return parsed JSON. @wordpress/api-fetch works directly.
14
+ */
15
+ fetchFn: FetchFn;
16
+ /**
17
+ * Agent ID to scope the chat to.
18
+ */
19
+ agentId?: number;
20
+ /** Content format for message rendering. Defaults to 'markdown'. */
21
+ contentFormat?: ContentFormat;
22
+ /** Custom content renderer for messages. */
23
+ renderContent?: (content: string, role: ChatMessageType['role']) => ReactNode;
24
+ /** Whether to display tool call/result messages. Defaults to true. */
25
+ showTools?: boolean;
26
+ /** Map of tool function names to friendly display labels. */
27
+ toolNames?: Record<string, string>;
28
+ /** Placeholder text for the input. */
29
+ placeholder?: string;
30
+ /** Content shown when conversation is empty. */
31
+ emptyState?: ReactNode;
32
+ /** Initial messages (hydrated from server). */
33
+ initialMessages?: ChatMessageType[];
34
+ /** Initial session ID. */
35
+ initialSessionId?: string;
36
+ /** Maximum continuation turns. */
37
+ maxContinueTurns?: number;
38
+ /** Called when an error occurs. */
39
+ onError?: UseChatOptions['onError'];
40
+ /** Called when a new message is added. */
41
+ onMessage?: UseChatOptions['onMessage'];
42
+ /** Additional CSS class name on the root element. */
43
+ className?: string;
44
+ /** Whether to show the session switcher. Defaults to true. */
45
+ showSessions?: boolean;
46
+ /** Label shown during multi-turn processing. */
47
+ processingLabel?: (turnCount: number) => string;
48
+ }
49
+ /**
50
+ * Ready-to-use chat component.
51
+ *
52
+ * Composes all the primitives (messages, input, typing, sessions, etc.)
53
+ * into a complete chat experience. For full control, use the individual
54
+ * components and `useChat` hook directly.
55
+ *
56
+ * @example
57
+ * ```tsx
58
+ * import { Chat } from '@extrachill/chat';
59
+ * import apiFetch from '@wordpress/api-fetch';
60
+ *
61
+ * function StudioChat() {
62
+ * return (
63
+ * <Chat
64
+ * basePath="/datamachine/v1/chat"
65
+ * fetchFn={apiFetch}
66
+ * agentId={5}
67
+ * />
68
+ * );
69
+ * }
70
+ * ```
71
+ */
72
+ export declare function Chat({ basePath, fetchFn, agentId, contentFormat, renderContent, showTools, toolNames, placeholder, emptyState, initialMessages, initialSessionId, maxContinueTurns, onError, onMessage, className, showSessions, processingLabel, }: ChatProps): import("react/jsx-runtime").JSX.Element;
73
+ //# sourceMappingURL=Chat.d.ts.map
@@ -0,0 +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,EAAW,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAQlE,MAAM,WAAW,SAAS;IACzB;;;OAGG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,OAAO,EAAE,OAAO,CAAC;IACjB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,oEAAoE;IACpE,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,4CAA4C;IAC5C,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,CAAC,MAAM,CAAC,KAAK,SAAS,CAAC;IAC9E,sEAAsE;IACtE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,6DAA6D;IAC7D,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,sCAAsC;IACtC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gDAAgD;IAChD,UAAU,CAAC,EAAE,SAAS,CAAC;IACvB,+CAA+C;IAC/C,eAAe,CAAC,EAAE,eAAe,EAAE,CAAC;IACpC,0BAA0B;IAC1B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,kCAAkC;IAClC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,mCAAmC;IACnC,OAAO,CAAC,EAAE,cAAc,CAAC,SAAS,CAAC,CAAC;IACpC,0CAA0C;IAC1C,SAAS,CAAC,EAAE,cAAc,CAAC,WAAW,CAAC,CAAC;IACxC,qDAAqD;IACrD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8DAA8D;IAC9D,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,gDAAgD;IAChD,eAAe,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC;CAChD;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,IAAI,CAAC,EACpB,QAAQ,EACR,OAAO,EACP,OAAO,EACP,aAA0B,EAC1B,aAAa,EACb,SAAgB,EAChB,SAAS,EACT,WAAW,EACX,UAAU,EACV,eAAe,EACf,gBAAgB,EAChB,gBAAgB,EAChB,OAAO,EACP,SAAS,EACT,SAAS,EACT,YAAmB,EACnB,eAAe,GACf,EAAE,SAAS,2CA2DX"}
package/dist/Chat.js ADDED
@@ -0,0 +1,50 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useChat } from "./hooks/useChat.js";
3
+ import { ErrorBoundary } from "./components/ErrorBoundary.js";
4
+ import { AvailabilityGate } from "./components/AvailabilityGate.js";
5
+ import { ChatMessages } from "./components/ChatMessages.js";
6
+ import { ChatInput } from "./components/ChatInput.js";
7
+ import { TypingIndicator } from "./components/TypingIndicator.js";
8
+ import { SessionSwitcher } from "./components/SessionSwitcher.js";
9
+ /**
10
+ * Ready-to-use chat component.
11
+ *
12
+ * Composes all the primitives (messages, input, typing, sessions, etc.)
13
+ * into a complete chat experience. For full control, use the individual
14
+ * components and `useChat` hook directly.
15
+ *
16
+ * @example
17
+ * ```tsx
18
+ * import { Chat } from '@extrachill/chat';
19
+ * import apiFetch from '@wordpress/api-fetch';
20
+ *
21
+ * function StudioChat() {
22
+ * return (
23
+ * <Chat
24
+ * basePath="/datamachine/v1/chat"
25
+ * fetchFn={apiFetch}
26
+ * agentId={5}
27
+ * />
28
+ * );
29
+ * }
30
+ * ```
31
+ */
32
+ export function Chat({ basePath, fetchFn, agentId, contentFormat = 'markdown', renderContent, showTools = true, toolNames, placeholder, emptyState, initialMessages, initialSessionId, maxContinueTurns, onError, onMessage, className, showSessions = true, processingLabel, }) {
33
+ const chat = useChat({
34
+ basePath,
35
+ fetchFn,
36
+ agentId,
37
+ initialMessages,
38
+ initialSessionId,
39
+ maxContinueTurns,
40
+ onError,
41
+ onMessage,
42
+ });
43
+ const baseClass = 'ec-chat';
44
+ const classes = [baseClass, className].filter(Boolean).join(' ');
45
+ return (_jsx(ErrorBoundary, { onError: onError ? (err) => onError(err) : undefined, children: _jsx("div", { className: classes, children: _jsxs(AvailabilityGate, { availability: chat.availability, children: [showSessions && (_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, emptyState: emptyState }), _jsx(TypingIndicator, { visible: chat.isLoading, label: chat.turnCount > 0
46
+ ? (processingLabel
47
+ ? processingLabel(chat.turnCount)
48
+ : `Processing turn ${chat.turnCount}...`)
49
+ : undefined }), _jsx(ChatInput, { onSend: chat.sendMessage, disabled: chat.isLoading, placeholder: placeholder })] }) }) }));
50
+ }
package/dist/api.d.ts ADDED
@@ -0,0 +1,68 @@
1
+ /**
2
+ * REST API client for the chat package.
3
+ *
4
+ * Speaks the standard chat REST contract natively. Any backend that
5
+ * implements the same endpoint shapes works out of the box.
6
+ *
7
+ * The `fetchFn` parameter allows consumers to plug in their own
8
+ * fetch implementation (e.g. @wordpress/api-fetch for cookie auth).
9
+ */
10
+ import type { ChatMessage } from './types/message.ts';
11
+ import type { ChatSession } from './types/session.ts';
12
+ /**
13
+ * A fetch-like function. Accepts path + options, returns parsed JSON.
14
+ *
15
+ * This matches @wordpress/api-fetch signature:
16
+ * apiFetch({ path: '/datamachine/v1/chat', method: 'POST', data: {...} })
17
+ *
18
+ * For non-WordPress contexts, consumers wrap native fetch:
19
+ * (opts) => fetch(baseUrl + opts.path, { method: opts.method, body: JSON.stringify(opts.data) }).then(r => r.json())
20
+ */
21
+ export interface FetchOptions {
22
+ path: string;
23
+ method?: string;
24
+ data?: Record<string, unknown>;
25
+ }
26
+ export type FetchFn = (options: FetchOptions) => Promise<unknown>;
27
+ export interface ChatApiConfig {
28
+ /** Base path for the chat endpoints (e.g. '/datamachine/v1/chat'). */
29
+ basePath: string;
30
+ /** The fetch function to use for requests. */
31
+ fetchFn: FetchFn;
32
+ /** Agent ID to scope sessions to. */
33
+ agentId?: number;
34
+ }
35
+ export interface SendResult {
36
+ sessionId: string;
37
+ messages: ChatMessage[];
38
+ completed: boolean;
39
+ turnNumber: number;
40
+ maxTurnsReached: boolean;
41
+ }
42
+ export interface ContinueResult {
43
+ messages: ChatMessage[];
44
+ completed: boolean;
45
+ turnNumber: number;
46
+ maxTurnsReached: boolean;
47
+ }
48
+ /**
49
+ * Send a user message (create or continue a session).
50
+ */
51
+ export declare function sendMessage(config: ChatApiConfig, content: string, sessionId?: string): Promise<SendResult>;
52
+ /**
53
+ * Continue a multi-turn response.
54
+ */
55
+ export declare function continueResponse(config: ChatApiConfig, sessionId: string): Promise<ContinueResult>;
56
+ /**
57
+ * List sessions for the current user.
58
+ */
59
+ export declare function listSessions(config: ChatApiConfig, limit?: number): Promise<ChatSession[]>;
60
+ /**
61
+ * Load a single session's conversation.
62
+ */
63
+ export declare function loadSession(config: ChatApiConfig, sessionId: string): Promise<ChatMessage[]>;
64
+ /**
65
+ * Delete a session.
66
+ */
67
+ export declare function deleteSession(config: ChatApiConfig, sessionId: string): Promise<void>;
68
+ //# sourceMappingURL=api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAUtD;;;;;;;;GAQG;AACH,MAAM,WAAW,YAAY;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B;AAED,MAAM,MAAM,OAAO,GAAG,CAAC,OAAO,EAAE,YAAY,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;AAElE,MAAM,WAAW,aAAa;IAC7B,sEAAsE;IACtE,QAAQ,EAAE,MAAM,CAAC;IACjB,8CAA8C;IAC9C,OAAO,EAAE,OAAO,CAAC;IACjB,qCAAqC;IACrC,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,cAAc;IAC9B,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,OAAO,CAAC;CACzB;AAED;;GAEG;AACH,wBAAsB,WAAW,CAChC,MAAM,EAAE,aAAa,EACrB,OAAO,EAAE,MAAM,EACf,SAAS,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,UAAU,CAAC,CAsBrB;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CACrC,MAAM,EAAE,aAAa,EACrB,SAAS,EAAE,MAAM,GACf,OAAO,CAAC,cAAc,CAAC,CAiBzB;AAED;;GAEG;AACH,wBAAsB,YAAY,CACjC,MAAM,EAAE,aAAa,EACrB,KAAK,SAAK,GACR,OAAO,CAAC,WAAW,EAAE,CAAC,CAaxB;AAED;;GAEG;AACH,wBAAsB,WAAW,CAChC,MAAM,EAAE,aAAa,EACrB,SAAS,EAAE,MAAM,GACf,OAAO,CAAC,WAAW,EAAE,CAAC,CAUxB;AAED;;GAEG;AACH,wBAAsB,aAAa,CAClC,MAAM,EAAE,aAAa,EACrB,SAAS,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC,CASf"}
package/dist/api.js ADDED
@@ -0,0 +1,93 @@
1
+ /**
2
+ * REST API client for the chat package.
3
+ *
4
+ * Speaks the standard chat REST contract natively. Any backend that
5
+ * implements the same endpoint shapes works out of the box.
6
+ *
7
+ * The `fetchFn` parameter allows consumers to plug in their own
8
+ * fetch implementation (e.g. @wordpress/api-fetch for cookie auth).
9
+ */
10
+ import { normalizeConversation, normalizeMessage, normalizeSession } from "./normalizer.js";
11
+ /**
12
+ * Send a user message (create or continue a session).
13
+ */
14
+ export async function sendMessage(config, content, sessionId) {
15
+ const body = { message: content };
16
+ if (sessionId)
17
+ body.session_id = sessionId;
18
+ if (config.agentId)
19
+ body.agent_id = config.agentId;
20
+ const raw = await config.fetchFn({
21
+ path: config.basePath,
22
+ method: 'POST',
23
+ data: body,
24
+ });
25
+ if (!raw.success) {
26
+ throw new Error(raw.message ?? 'Failed to send message');
27
+ }
28
+ return {
29
+ sessionId: raw.data.session_id,
30
+ messages: normalizeConversation(raw.data.conversation),
31
+ completed: raw.data.completed,
32
+ turnNumber: raw.data.turn_number,
33
+ maxTurnsReached: raw.data.max_turns_reached,
34
+ };
35
+ }
36
+ /**
37
+ * Continue a multi-turn response.
38
+ */
39
+ export async function continueResponse(config, sessionId) {
40
+ const raw = await config.fetchFn({
41
+ path: `${config.basePath}/continue`,
42
+ method: 'POST',
43
+ data: { session_id: sessionId },
44
+ });
45
+ if (!raw.success) {
46
+ throw new Error(raw.message ?? 'Failed to continue');
47
+ }
48
+ return {
49
+ messages: raw.data.new_messages.map(normalizeMessage),
50
+ completed: raw.data.completed,
51
+ turnNumber: raw.data.turn_number,
52
+ maxTurnsReached: raw.data.max_turns_reached,
53
+ };
54
+ }
55
+ /**
56
+ * List sessions for the current user.
57
+ */
58
+ export async function listSessions(config, limit = 20) {
59
+ const params = new URLSearchParams({ limit: String(limit) });
60
+ if (config.agentId)
61
+ params.set('agent_id', String(config.agentId));
62
+ const raw = await config.fetchFn({
63
+ path: `${config.basePath}/sessions?${params.toString()}`,
64
+ });
65
+ if (!raw.success) {
66
+ throw new Error('Failed to list sessions');
67
+ }
68
+ return raw.data.sessions.map(normalizeSession);
69
+ }
70
+ /**
71
+ * Load a single session's conversation.
72
+ */
73
+ export async function loadSession(config, sessionId) {
74
+ const raw = await config.fetchFn({
75
+ path: `${config.basePath}/${sessionId}`,
76
+ });
77
+ if (!raw.success) {
78
+ throw new Error('Failed to load session');
79
+ }
80
+ return normalizeConversation(raw.data.conversation);
81
+ }
82
+ /**
83
+ * Delete a session.
84
+ */
85
+ export async function deleteSession(config, sessionId) {
86
+ const raw = await config.fetchFn({
87
+ path: `${config.basePath}/${sessionId}`,
88
+ method: 'DELETE',
89
+ });
90
+ if (!raw.success) {
91
+ throw new Error('Failed to delete session');
92
+ }
93
+ }
@@ -0,0 +1,19 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { ChatAvailability } from '../types/index.ts';
3
+ export interface AvailabilityGateProps {
4
+ /** Current availability state. */
5
+ availability: ChatAvailability;
6
+ /** Custom renderer for non-ready states. */
7
+ renderState?: (availability: ChatAvailability) => ReactNode;
8
+ /** Children to render when ready. */
9
+ children: ReactNode;
10
+ }
11
+ /**
12
+ * Gates chat UI behind availability states.
13
+ *
14
+ * When the adapter reports a non-ready state, this component renders
15
+ * an appropriate message instead of the chat. Consumers can override
16
+ * the default rendering with `renderState`.
17
+ */
18
+ export declare function AvailabilityGate({ availability, renderState, children, }: AvailabilityGateProps): import("react/jsx-runtime").JSX.Element;
19
+ //# sourceMappingURL=AvailabilityGate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AvailabilityGate.d.ts","sourceRoot":"","sources":["../../src/components/AvailabilityGate.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAE1D,MAAM,WAAW,qBAAqB;IACrC,kCAAkC;IAClC,YAAY,EAAE,gBAAgB,CAAC;IAC/B,4CAA4C;IAC5C,WAAW,CAAC,EAAE,CAAC,YAAY,EAAE,gBAAgB,KAAK,SAAS,CAAC;IAC5D,qCAAqC;IACrC,QAAQ,EAAE,SAAS,CAAC;CACpB;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,EAChC,YAAY,EACZ,WAAW,EACX,QAAQ,GACR,EAAE,qBAAqB,2CAUvB"}
@@ -0,0 +1,32 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Gates chat UI behind availability states.
4
+ *
5
+ * When the adapter reports a non-ready state, this component renders
6
+ * an appropriate message instead of the chat. Consumers can override
7
+ * the default rendering with `renderState`.
8
+ */
9
+ export function AvailabilityGate({ availability, renderState, children, }) {
10
+ if (availability.status === 'ready') {
11
+ return _jsx(_Fragment, { children: children });
12
+ }
13
+ if (renderState) {
14
+ return _jsx(_Fragment, { children: renderState(availability) });
15
+ }
16
+ return _jsx(DefaultAvailabilityMessage, { availability: availability });
17
+ }
18
+ function DefaultAvailabilityMessage({ availability }) {
19
+ const baseClass = 'ec-chat-availability';
20
+ switch (availability.status) {
21
+ case 'login-required':
22
+ return (_jsxs("div", { className: `${baseClass} ${baseClass}--login`, children: [_jsx("p", { children: "Please log in to use the chat." }), availability.loginUrl && (_jsx("a", { href: availability.loginUrl, className: `${baseClass}__action`, children: "Log in" }))] }));
23
+ case 'unavailable':
24
+ return (_jsx("div", { className: `${baseClass} ${baseClass}--unavailable`, children: _jsx("p", { children: availability.reason ?? 'Chat is currently unavailable.' }) }));
25
+ case 'provisioning':
26
+ return (_jsx("div", { className: `${baseClass} ${baseClass}--provisioning`, children: _jsx("p", { children: availability.message ?? 'Setting up your chat...' }) }));
27
+ case 'upgrade-required':
28
+ return (_jsxs("div", { className: `${baseClass} ${baseClass}--upgrade`, children: [_jsx("p", { children: availability.message ?? 'Upgrade required to access chat.' }), availability.upgradeUrl && (_jsx("a", { href: availability.upgradeUrl, className: `${baseClass}__action`, children: "Upgrade" }))] }));
29
+ case 'error':
30
+ return (_jsx("div", { className: `${baseClass} ${baseClass}--error`, children: _jsx("p", { children: availability.message }) }));
31
+ }
32
+ }
@@ -0,0 +1,21 @@
1
+ export interface ChatInputProps {
2
+ /** Called when the user submits a message. */
3
+ onSend: (content: string) => void;
4
+ /** Whether input is disabled (e.g. while waiting for response). */
5
+ disabled?: boolean;
6
+ /** Placeholder text. Defaults to 'Type a message...'. */
7
+ placeholder?: string;
8
+ /** Maximum number of rows the textarea auto-grows to. Defaults to 6. */
9
+ maxRows?: number;
10
+ /** Additional CSS class name. */
11
+ className?: string;
12
+ }
13
+ /**
14
+ * Chat input with auto-growing textarea and keyboard shortcuts.
15
+ *
16
+ * - Enter sends the message
17
+ * - Shift+Enter adds a newline
18
+ * - Textarea auto-grows up to `maxRows`
19
+ */
20
+ export declare function ChatInput({ onSend, disabled, placeholder, maxRows, className, }: ChatInputProps): import("react/jsx-runtime").JSX.Element;
21
+ //# sourceMappingURL=ChatInput.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ChatInput.d.ts","sourceRoot":"","sources":["../../src/components/ChatInput.tsx"],"names":[],"mappings":"AAEA,MAAM,WAAW,cAAc;IAC9B,8CAA8C;IAC9C,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,mEAAmE;IACnE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,yDAAyD;IACzD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wEAAwE;IACxE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iCAAiC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,EACzB,MAAM,EACN,QAAgB,EAChB,WAAiC,EACjC,OAAW,EACX,SAAS,GACT,EAAE,cAAc,2CAkEhB"}
@@ -0,0 +1,52 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useRef, useCallback } from 'react';
3
+ /**
4
+ * Chat input with auto-growing textarea and keyboard shortcuts.
5
+ *
6
+ * - Enter sends the message
7
+ * - Shift+Enter adds a newline
8
+ * - Textarea auto-grows up to `maxRows`
9
+ */
10
+ export function ChatInput({ onSend, disabled = false, placeholder = 'Type a message...', maxRows = 6, className, }) {
11
+ const [value, setValue] = useState('');
12
+ const textareaRef = useRef(null);
13
+ const cooldownRef = useRef(false);
14
+ const resize = useCallback(() => {
15
+ const el = textareaRef.current;
16
+ if (!el)
17
+ return;
18
+ el.style.height = 'auto';
19
+ const lineHeight = parseInt(getComputedStyle(el).lineHeight) || 20;
20
+ const maxHeight = lineHeight * maxRows;
21
+ el.style.height = `${Math.min(el.scrollHeight, maxHeight)}px`;
22
+ }, [maxRows]);
23
+ const handleSubmit = useCallback((e) => {
24
+ e?.preventDefault();
25
+ const trimmed = value.trim();
26
+ if (!trimmed || disabled || cooldownRef.current)
27
+ return;
28
+ // Debounce to prevent double-submit
29
+ cooldownRef.current = true;
30
+ setTimeout(() => { cooldownRef.current = false; }, 300);
31
+ onSend(trimmed);
32
+ setValue('');
33
+ // Reset textarea height after clearing
34
+ requestAnimationFrame(() => {
35
+ const el = textareaRef.current;
36
+ if (el)
37
+ el.style.height = 'auto';
38
+ });
39
+ }, [value, disabled, onSend]);
40
+ const handleKeyDown = useCallback((e) => {
41
+ if (e.key === 'Enter' && !e.shiftKey) {
42
+ e.preventDefault();
43
+ handleSubmit();
44
+ }
45
+ }, [handleSubmit]);
46
+ const baseClass = 'ec-chat-input';
47
+ const classes = [baseClass, className].filter(Boolean).join(' ');
48
+ return (_jsxs("form", { className: classes, onSubmit: handleSubmit, children: [_jsx("textarea", { ref: textareaRef, className: `${baseClass}__textarea`, value: value, onChange: (e) => { setValue(e.target.value); resize(); }, onKeyDown: handleKeyDown, placeholder: placeholder, disabled: disabled, rows: 1, "aria-label": placeholder }), _jsx("button", { className: `${baseClass}__send`, type: "submit", disabled: disabled || !value.trim(), "aria-label": "Send message", children: _jsx(SendIcon, {}) })] }));
49
+ }
50
+ function SendIcon() {
51
+ return (_jsxs("svg", { className: "ec-chat-input__send-icon", viewBox: "0 0 24 24", width: "20", height: "20", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("line", { x1: "22", y1: "2", x2: "11", y2: "13" }), _jsx("polygon", { points: "22 2 15 22 11 13 2 9 22 2" })] }));
52
+ }
@@ -0,0 +1,23 @@
1
+ import { type ReactNode } from 'react';
2
+ import type { ChatMessage as ChatMessageType, ContentFormat } from '../types/index.ts';
3
+ export interface ChatMessageProps {
4
+ /** The message to render. */
5
+ message: ChatMessageType;
6
+ /** How to render message content. Defaults to 'markdown'. */
7
+ contentFormat?: ContentFormat;
8
+ /**
9
+ * Custom content renderer. When provided, overrides contentFormat.
10
+ * Use this to plug in your own markdown renderer (react-markdown, etc.).
11
+ */
12
+ renderContent?: (content: string, role: ChatMessageType['role']) => ReactNode;
13
+ /** Additional CSS class name on the outer wrapper. */
14
+ className?: string;
15
+ }
16
+ /**
17
+ * Renders a single chat message bubble.
18
+ *
19
+ * User messages align right, assistant messages align left.
20
+ * Content rendering is pluggable via `renderContent` or `contentFormat`.
21
+ */
22
+ export declare function ChatMessage({ message, contentFormat, renderContent, className, }: ChatMessageProps): import("react/jsx-runtime").JSX.Element;
23
+ //# sourceMappingURL=ChatMessage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ChatMessage.d.ts","sourceRoot":"","sources":["../../src/components/ChatMessage.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,WAAW,IAAI,eAAe,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvF,MAAM,WAAW,gBAAgB;IAChC,6BAA6B;IAC7B,OAAO,EAAE,eAAe,CAAC;IACzB,6DAA6D;IAC7D,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B;;;OAGG;IACH,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,CAAC,MAAM,CAAC,KAAK,SAAS,CAAC;IAC9E,sDAAsD;IACtD,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,EAC3B,OAAO,EACP,aAA0B,EAC1B,aAAa,EACb,SAAS,GACT,EAAE,gBAAgB,2CAyBlB"}
@@ -0,0 +1,34 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /**
3
+ * Renders a single chat message bubble.
4
+ *
5
+ * User messages align right, assistant messages align left.
6
+ * Content rendering is pluggable via `renderContent` or `contentFormat`.
7
+ */
8
+ export function ChatMessage({ message, contentFormat = 'markdown', renderContent, className, }) {
9
+ const isUser = message.role === 'user';
10
+ const baseClass = 'ec-chat-message';
11
+ const roleClass = isUser ? `${baseClass}--user` : `${baseClass}--assistant`;
12
+ const classes = [baseClass, roleClass, className].filter(Boolean).join(' ');
13
+ return (_jsxs("div", { className: classes, "data-message-id": message.id, children: [_jsx("div", { className: `${baseClass}__bubble`, children: renderContent
14
+ ? renderContent(message.content, message.role)
15
+ : _jsx(DefaultContent, { content: message.content, format: contentFormat }) }), message.timestamp && (_jsx("time", { className: `${baseClass}__timestamp`, dateTime: message.timestamp, title: new Date(message.timestamp).toLocaleString(), children: formatTime(message.timestamp) }))] }));
16
+ }
17
+ function DefaultContent({ content, format }) {
18
+ if (format === 'html') {
19
+ return _jsx("div", { dangerouslySetInnerHTML: { __html: content } });
20
+ }
21
+ // For 'text' and 'markdown' (without a custom renderer), render as text
22
+ // with basic paragraph splitting. Consumers should provide renderContent
23
+ // for proper markdown support.
24
+ return (_jsx(_Fragment, { children: content.split('\n\n').map((paragraph, i) => (_jsx("p", { children: paragraph }, i))) }));
25
+ }
26
+ function formatTime(iso) {
27
+ try {
28
+ const date = new Date(iso);
29
+ return date.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' });
30
+ }
31
+ catch {
32
+ return '';
33
+ }
34
+ }
@@ -0,0 +1,28 @@
1
+ import { type ReactNode } from 'react';
2
+ import type { ChatMessage as ChatMessageType, ContentFormat } from '../types/index.ts';
3
+ export interface ChatMessagesProps {
4
+ /** All messages in the conversation. */
5
+ messages: ChatMessageType[];
6
+ /** How to render message content. Defaults to 'markdown'. */
7
+ contentFormat?: ContentFormat;
8
+ /** Custom content renderer passed through to ChatMessage. */
9
+ renderContent?: (content: string, role: ChatMessageType['role']) => ReactNode;
10
+ /** Whether to show tool call/result messages. Defaults to false. */
11
+ showTools?: boolean;
12
+ /** Custom tool name display map. Maps tool function names to friendly labels. */
13
+ toolNames?: Record<string, string>;
14
+ /** Whether to auto-scroll to bottom on new messages. Defaults to true. */
15
+ autoScroll?: boolean;
16
+ /** Placeholder content shown when there are no messages. */
17
+ emptyState?: ReactNode;
18
+ /** Additional CSS class name. */
19
+ className?: string;
20
+ }
21
+ /**
22
+ * Scrollable message list with auto-scroll behavior.
23
+ *
24
+ * Filters system messages, groups tool_call/tool_result pairs,
25
+ * and renders user/assistant messages as ChatMessage components.
26
+ */
27
+ export declare function ChatMessages({ messages, contentFormat, renderContent, showTools, toolNames, autoScroll, emptyState, className, }: ChatMessagesProps): import("react/jsx-runtime").JSX.Element;
28
+ //# sourceMappingURL=ChatMessages.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ChatMessages.d.ts","sourceRoot":"","sources":["../../src/components/ChatMessages.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAqB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAC1D,OAAO,KAAK,EAAE,WAAW,IAAI,eAAe,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAIvF,MAAM,WAAW,iBAAiB;IACjC,wCAAwC;IACxC,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,6DAA6D;IAC7D,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,6DAA6D;IAC7D,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,CAAC,MAAM,CAAC,KAAK,SAAS,CAAC;IAC9E,oEAAoE;IACpE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,iFAAiF;IACjF,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,0EAA0E;IAC1E,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,4DAA4D;IAC5D,UAAU,CAAC,EAAE,SAAS,CAAC;IACvB,iCAAiC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,EAC5B,QAAQ,EACR,aAAa,EACb,aAAa,EACb,SAAiB,EACjB,SAAS,EACT,UAAiB,EACjB,UAAU,EACV,SAAS,GACT,EAAE,iBAAiB,2CAqDnB"}
@@ -0,0 +1,121 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef } from 'react';
3
+ import { ChatMessage } from "./ChatMessage.js";
4
+ import { ToolMessage } from "./ToolMessage.js";
5
+ /**
6
+ * Scrollable message list with auto-scroll behavior.
7
+ *
8
+ * Filters system messages, groups tool_call/tool_result pairs,
9
+ * and renders user/assistant messages as ChatMessage components.
10
+ */
11
+ export function ChatMessages({ messages, contentFormat, renderContent, showTools = false, toolNames, autoScroll = true, emptyState, className, }) {
12
+ const bottomRef = useRef(null);
13
+ const containerRef = useRef(null);
14
+ useEffect(() => {
15
+ if (autoScroll && bottomRef.current) {
16
+ bottomRef.current.scrollIntoView({ behavior: 'smooth' });
17
+ }
18
+ }, [messages, autoScroll]);
19
+ const displayItems = buildDisplayItems(messages, showTools);
20
+ const baseClass = 'ec-chat-messages';
21
+ const classes = [baseClass, className].filter(Boolean).join(' ');
22
+ if (displayItems.length === 0 && emptyState) {
23
+ return (_jsx("div", { className: classes, ref: containerRef, children: _jsx("div", { className: `${baseClass}__empty`, children: emptyState }) }));
24
+ }
25
+ return (_jsxs("div", { className: classes, ref: containerRef, children: [displayItems.map((item) => {
26
+ if (item.type === 'message') {
27
+ return (_jsx(ChatMessage, { message: item.message, contentFormat: contentFormat, renderContent: renderContent }, item.message.id));
28
+ }
29
+ if (item.type === 'tool-group' && showTools) {
30
+ return (_jsx(ToolMessage, { group: item.group, toolNames: toolNames }, item.group.callMessage.id));
31
+ }
32
+ return null;
33
+ }), _jsx("div", { ref: bottomRef })] }));
34
+ }
35
+ /**
36
+ * Build display items from raw messages.
37
+ *
38
+ * - Filters out system messages
39
+ * - Groups tool_call + tool_result pairs
40
+ * - Returns ordered display items
41
+ */
42
+ function buildDisplayItems(messages, showTools) {
43
+ const items = [];
44
+ const toolResultMap = new Map();
45
+ // Pre-index tool results by tool name for pairing
46
+ if (showTools) {
47
+ for (const msg of messages) {
48
+ if (msg.role === 'tool_result' && msg.toolResult?.toolName) {
49
+ toolResultMap.set(msg.toolResult.toolName, msg);
50
+ }
51
+ }
52
+ }
53
+ const processedToolResults = new Set();
54
+ for (const msg of messages) {
55
+ // Skip system messages
56
+ if (msg.role === 'system')
57
+ continue;
58
+ // Skip tool messages when tools are hidden
59
+ if (!showTools && (msg.role === 'tool_call' || msg.role === 'tool_result'))
60
+ continue;
61
+ // Handle user/assistant messages (assistant messages with toolCalls get both treatments)
62
+ if (msg.role === 'user' || msg.role === 'assistant') {
63
+ // If assistant message has tool calls, render the text part as a message
64
+ // and the tool calls as tool groups
65
+ if (msg.role === 'assistant' && msg.toolCalls?.length && showTools) {
66
+ // Only render text bubble if there's actual text content
67
+ if (msg.content.trim()) {
68
+ items.push({ type: 'message', message: msg });
69
+ }
70
+ for (const call of msg.toolCalls) {
71
+ const resultMsg = toolResultMap.get(call.name);
72
+ if (resultMsg) {
73
+ processedToolResults.add(resultMsg.id);
74
+ }
75
+ items.push({
76
+ type: 'tool-group',
77
+ group: {
78
+ callMessage: {
79
+ ...msg,
80
+ content: '',
81
+ toolCalls: [call],
82
+ },
83
+ resultMessage: resultMsg ?? null,
84
+ toolName: call.name,
85
+ parameters: call.parameters,
86
+ success: resultMsg?.toolResult?.success ?? null,
87
+ },
88
+ });
89
+ }
90
+ }
91
+ else {
92
+ items.push({ type: 'message', message: msg });
93
+ }
94
+ continue;
95
+ }
96
+ // Handle standalone tool_call messages
97
+ if (msg.role === 'tool_call' && showTools) {
98
+ const toolName = msg.toolCalls?.[0]?.name ?? 'unknown';
99
+ const resultMsg = toolResultMap.get(toolName);
100
+ if (resultMsg) {
101
+ processedToolResults.add(resultMsg.id);
102
+ }
103
+ items.push({
104
+ type: 'tool-group',
105
+ group: {
106
+ callMessage: msg,
107
+ resultMessage: resultMsg ?? null,
108
+ toolName,
109
+ parameters: msg.toolCalls?.[0]?.parameters ?? {},
110
+ success: resultMsg?.toolResult?.success ?? null,
111
+ },
112
+ });
113
+ continue;
114
+ }
115
+ // Handle orphaned tool_result messages
116
+ if (msg.role === 'tool_result' && showTools && !processedToolResults.has(msg.id)) {
117
+ items.push({ type: 'message', message: msg });
118
+ }
119
+ }
120
+ return items;
121
+ }