@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
@@ -0,0 +1,27 @@
1
+ import { Component, type ErrorInfo, type ReactNode } from 'react';
2
+ export interface ErrorBoundaryProps {
3
+ /** Content to render when an error occurs. */
4
+ fallback?: ReactNode | ((error: Error, reset: () => void) => ReactNode);
5
+ /** Called when an error is caught. */
6
+ onError?: (error: Error, errorInfo: ErrorInfo) => void;
7
+ /** Children to render. */
8
+ children: ReactNode;
9
+ }
10
+ interface ErrorBoundaryState {
11
+ error: Error | null;
12
+ }
13
+ /**
14
+ * Error boundary for the chat UI.
15
+ *
16
+ * Catches React render errors and shows a fallback UI with retry.
17
+ * Resets automatically when children change via key prop.
18
+ */
19
+ export declare class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
20
+ state: ErrorBoundaryState;
21
+ static getDerivedStateFromError(error: Error): ErrorBoundaryState;
22
+ componentDidCatch(error: Error, errorInfo: ErrorInfo): void;
23
+ reset: () => void;
24
+ render(): string | number | boolean | Iterable<ReactNode> | import("react/jsx-runtime").JSX.Element | null | undefined;
25
+ }
26
+ export {};
27
+ //# sourceMappingURL=ErrorBoundary.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ErrorBoundary.d.ts","sourceRoot":"","sources":["../../src/components/ErrorBoundary.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAElE,MAAM,WAAW,kBAAkB;IAClC,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,SAAS,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,IAAI,KAAK,SAAS,CAAC,CAAC;IACxE,sCAAsC;IACtC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,KAAK,IAAI,CAAC;IACvD,0BAA0B;IAC1B,QAAQ,EAAE,SAAS,CAAC;CACpB;AAED,UAAU,kBAAkB;IAC3B,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CACpB;AAED;;;;;GAKG;AACH,qBAAa,aAAc,SAAQ,SAAS,CAAC,kBAAkB,EAAE,kBAAkB,CAAC;IAC1E,KAAK,EAAE,kBAAkB,CAAmB;IAErD,MAAM,CAAC,wBAAwB,CAAC,KAAK,EAAE,KAAK,GAAG,kBAAkB;IAIxD,iBAAiB,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS;IAI7D,KAAK,aAEH;IAEO,MAAM;CA6Bf"}
@@ -0,0 +1,34 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Component } from 'react';
3
+ /**
4
+ * Error boundary for the chat UI.
5
+ *
6
+ * Catches React render errors and shows a fallback UI with retry.
7
+ * Resets automatically when children change via key prop.
8
+ */
9
+ export class ErrorBoundary extends Component {
10
+ state = { error: null };
11
+ static getDerivedStateFromError(error) {
12
+ return { error };
13
+ }
14
+ componentDidCatch(error, errorInfo) {
15
+ this.props.onError?.(error, errorInfo);
16
+ }
17
+ reset = () => {
18
+ this.setState({ error: null });
19
+ };
20
+ render() {
21
+ const { error } = this.state;
22
+ const { fallback, children } = this.props;
23
+ if (error) {
24
+ if (typeof fallback === 'function') {
25
+ return fallback(error, this.reset);
26
+ }
27
+ if (fallback) {
28
+ return fallback;
29
+ }
30
+ return (_jsxs("div", { className: "ec-chat-error", children: [_jsx("p", { className: "ec-chat-error__message", children: "Something went wrong in the chat." }), _jsx("button", { className: "ec-chat-error__retry", onClick: this.reset, type: "button", children: "Try again" })] }));
31
+ }
32
+ return children;
33
+ }
34
+ }
@@ -0,0 +1,25 @@
1
+ import type { ChatSession } from '../types/index.ts';
2
+ export interface SessionSwitcherProps {
3
+ /** List of available sessions. */
4
+ sessions: ChatSession[];
5
+ /** Currently active session ID. */
6
+ activeSessionId?: string;
7
+ /** Called when a session is selected. */
8
+ onSelect: (sessionId: string) => void;
9
+ /** Called when the user wants to create a new session. */
10
+ onNew?: () => void;
11
+ /** Called when the user wants to delete a session. */
12
+ onDelete?: (sessionId: string) => void;
13
+ /** Whether sessions are currently loading. */
14
+ loading?: boolean;
15
+ /** Additional CSS class name. */
16
+ className?: string;
17
+ }
18
+ /**
19
+ * Session switcher dropdown.
20
+ *
21
+ * Renders a list of sessions with the active one highlighted.
22
+ * Only rendered when the adapter declares `capabilities.sessions = true`.
23
+ */
24
+ export declare function SessionSwitcher({ sessions, activeSessionId, onSelect, onNew, onDelete, loading, className, }: SessionSwitcherProps): import("react/jsx-runtime").JSX.Element;
25
+ //# sourceMappingURL=SessionSwitcher.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,44 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Session switcher dropdown.
4
+ *
5
+ * Renders a list of sessions with the active one highlighted.
6
+ * Only rendered when the adapter declares `capabilities.sessions = true`.
7
+ */
8
+ export function SessionSwitcher({ sessions, activeSessionId, onSelect, onNew, onDelete, loading = false, className, }) {
9
+ const baseClass = 'ec-chat-sessions';
10
+ 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
+ }) })] }));
22
+ }
23
+ function formatRelativeDate(iso) {
24
+ try {
25
+ const date = new Date(iso);
26
+ const now = new Date();
27
+ const diffMs = now.getTime() - date.getTime();
28
+ const diffMins = Math.floor(diffMs / 60_000);
29
+ if (diffMins < 1)
30
+ return 'just now';
31
+ if (diffMins < 60)
32
+ return `${diffMins}m ago`;
33
+ const diffHours = Math.floor(diffMins / 60);
34
+ if (diffHours < 24)
35
+ return `${diffHours}h ago`;
36
+ const diffDays = Math.floor(diffHours / 24);
37
+ if (diffDays < 7)
38
+ return `${diffDays}d ago`;
39
+ return date.toLocaleDateString();
40
+ }
41
+ catch {
42
+ return '';
43
+ }
44
+ }
@@ -0,0 +1,34 @@
1
+ import type { ChatMessage } from '../types/index.ts';
2
+ /**
3
+ * A paired tool call + result for display.
4
+ */
5
+ export interface ToolGroup {
6
+ /** The message containing the tool call. */
7
+ callMessage: ChatMessage;
8
+ /** The result message (null if still pending). */
9
+ resultMessage: ChatMessage | null;
10
+ /** Tool function name. */
11
+ toolName: string;
12
+ /** Parameters passed to the tool. */
13
+ parameters: Record<string, unknown>;
14
+ /** Whether the tool succeeded (null if pending). */
15
+ success: boolean | null;
16
+ }
17
+ export interface ToolMessageProps {
18
+ /** The tool call/result group to display. */
19
+ group: ToolGroup;
20
+ /** Map of tool names to friendly display labels. */
21
+ toolNames?: Record<string, string>;
22
+ /** Whether the tool details are initially expanded. Defaults to false. */
23
+ defaultExpanded?: boolean;
24
+ /** Additional CSS class name. */
25
+ className?: string;
26
+ }
27
+ /**
28
+ * Renders a collapsible tool call/result pair.
29
+ *
30
+ * Shows a summary line with the tool name (or friendly label),
31
+ * success/error indicator, and expandable details section.
32
+ */
33
+ export declare function ToolMessage({ group, toolNames, defaultExpanded, className, }: ToolMessageProps): import("react/jsx-runtime").JSX.Element;
34
+ //# sourceMappingURL=ToolMessage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ToolMessage.d.ts","sourceRoot":"","sources":["../../src/components/ToolMessage.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD;;GAEG;AACH,MAAM,WAAW,SAAS;IACzB,4CAA4C;IAC5C,WAAW,EAAE,WAAW,CAAC;IACzB,kDAAkD;IAClD,aAAa,EAAE,WAAW,GAAG,IAAI,CAAC;IAClC,0BAA0B;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,qCAAqC;IACrC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpC,oDAAoD;IACpD,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;CACxB;AAED,MAAM,WAAW,gBAAgB;IAChC,6CAA6C;IAC7C,KAAK,EAAE,SAAS,CAAC;IACjB,oDAAoD;IACpD,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,0EAA0E;IAC1E,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,iCAAiC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,EAC3B,KAAK,EACL,SAAS,EACT,eAAuB,EACvB,SAAS,GACT,EAAE,gBAAgB,2CAkDlB"}
@@ -0,0 +1,39 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ /**
4
+ * Renders a collapsible tool call/result pair.
5
+ *
6
+ * Shows a summary line with the tool name (or friendly label),
7
+ * success/error indicator, and expandable details section.
8
+ */
9
+ export function ToolMessage({ group, toolNames, defaultExpanded = false, className, }) {
10
+ const [expanded, setExpanded] = useState(defaultExpanded);
11
+ const displayName = toolNames?.[group.toolName] ?? formatToolName(group.toolName);
12
+ const baseClass = 'ec-chat-tool';
13
+ const statusClass = group.success === true
14
+ ? `${baseClass}--success`
15
+ : group.success === false
16
+ ? `${baseClass}--error`
17
+ : `${baseClass}--pending`;
18
+ const classes = [baseClass, statusClass, className].filter(Boolean).join(' ');
19
+ return (_jsxs("div", { className: classes, children: [_jsxs("button", { className: `${baseClass}__header`, onClick: () => setExpanded(!expanded), "aria-expanded": expanded, type: "button", children: [_jsx("span", { className: `${baseClass}__icon`, children: group.success === true ? '\u2713' : group.success === false ? '\u2717' : '\u2026' }), _jsx("span", { className: `${baseClass}__name`, children: displayName }), _jsx("span", { className: `${baseClass}__chevron`, children: expanded ? '\u25B2' : '\u25BC' })] }), expanded && (_jsxs("div", { className: `${baseClass}__details`, children: [Object.keys(group.parameters).length > 0 && (_jsxs("div", { className: `${baseClass}__section`, children: [_jsx("div", { className: `${baseClass}__section-label`, children: "Parameters" }), _jsx("pre", { className: `${baseClass}__json`, children: JSON.stringify(group.parameters, null, 2) })] })), group.resultMessage && (_jsxs("div", { className: `${baseClass}__section`, children: [_jsx("div", { className: `${baseClass}__section-label`, children: "Result" }), _jsx("pre", { className: `${baseClass}__json`, children: formatResult(group.resultMessage.content) })] }))] }))] }));
20
+ }
21
+ /**
22
+ * Convert snake_case tool name to a readable label.
23
+ */
24
+ function formatToolName(name) {
25
+ return name
26
+ .replace(/_/g, ' ')
27
+ .replace(/\b\w/g, (c) => c.toUpperCase());
28
+ }
29
+ /**
30
+ * Try to pretty-print JSON content, fall back to raw string.
31
+ */
32
+ function formatResult(content) {
33
+ try {
34
+ return JSON.stringify(JSON.parse(content), null, 2);
35
+ }
36
+ catch {
37
+ return content;
38
+ }
39
+ }
@@ -0,0 +1,16 @@
1
+ export interface TypingIndicatorProps {
2
+ /** Whether the indicator is visible. */
3
+ visible: boolean;
4
+ /** Optional label text. Defaults to none (dots only). */
5
+ label?: string;
6
+ /** Additional CSS class name. */
7
+ className?: string;
8
+ }
9
+ /**
10
+ * Animated typing indicator with three bouncing dots.
11
+ *
12
+ * Renders as an assistant-style message bubble.
13
+ * The animation is pure CSS — no JS timers.
14
+ */
15
+ export declare function TypingIndicator({ visible, label, className, }: TypingIndicatorProps): import("react/jsx-runtime").JSX.Element | null;
16
+ //# sourceMappingURL=TypingIndicator.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Animated typing indicator with three bouncing dots.
4
+ *
5
+ * Renders as an assistant-style message bubble.
6
+ * The animation is pure CSS — no JS timers.
7
+ */
8
+ export function TypingIndicator({ visible, label, className, }) {
9
+ if (!visible)
10
+ return null;
11
+ const baseClass = 'ec-chat-typing';
12
+ 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 })] }));
14
+ }
@@ -0,0 +1,9 @@
1
+ export { ChatMessage, type ChatMessageProps } from './ChatMessage.tsx';
2
+ export { ChatMessages, type ChatMessagesProps } from './ChatMessages.tsx';
3
+ export { ChatInput, type ChatInputProps } from './ChatInput.tsx';
4
+ export { ToolMessage, type ToolMessageProps, type ToolGroup } from './ToolMessage.tsx';
5
+ export { TypingIndicator, type TypingIndicatorProps } from './TypingIndicator.tsx';
6
+ export { SessionSwitcher, type SessionSwitcherProps } from './SessionSwitcher.tsx';
7
+ export { ErrorBoundary, type ErrorBoundaryProps } from './ErrorBoundary.tsx';
8
+ export { AvailabilityGate, type AvailabilityGateProps } from './AvailabilityGate.tsx';
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACvE,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC1E,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjE,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,KAAK,SAAS,EAAE,MAAM,mBAAmB,CAAC;AACvF,OAAO,EAAE,eAAe,EAAE,KAAK,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AACnF,OAAO,EAAE,eAAe,EAAE,KAAK,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AACnF,OAAO,EAAE,aAAa,EAAE,KAAK,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAC7E,OAAO,EAAE,gBAAgB,EAAE,KAAK,qBAAqB,EAAE,MAAM,wBAAwB,CAAC"}
@@ -0,0 +1,8 @@
1
+ export { ChatMessage } from "./ChatMessage.js";
2
+ export { ChatMessages } from "./ChatMessages.js";
3
+ export { ChatInput } from "./ChatInput.js";
4
+ export { ToolMessage } from "./ToolMessage.js";
5
+ export { TypingIndicator } from "./TypingIndicator.js";
6
+ export { SessionSwitcher } from "./SessionSwitcher.js";
7
+ export { ErrorBoundary } from "./ErrorBoundary.js";
8
+ export { AvailabilityGate } from "./AvailabilityGate.js";
@@ -0,0 +1,2 @@
1
+ export { useChat, type UseChatOptions, type UseChatReturn } from './useChat.ts';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1 @@
1
+ export { useChat } from "./useChat.js";
@@ -0,0 +1,102 @@
1
+ import type { ChatMessage } from '../types/message.ts';
2
+ import type { ChatSession } from '../types/session.ts';
3
+ import type { ChatAvailability } from '../types/session.ts';
4
+ import type { FetchFn } from '../api.ts';
5
+ /**
6
+ * Configuration for the useChat hook.
7
+ */
8
+ export interface UseChatOptions {
9
+ /**
10
+ * Base path for the chat REST endpoints.
11
+ * e.g. '/datamachine/v1/chat'
12
+ */
13
+ basePath: string;
14
+ /**
15
+ * Fetch function for API calls. Must accept { path, method?, data? }
16
+ * and return parsed JSON. @wordpress/api-fetch works directly.
17
+ */
18
+ fetchFn: FetchFn;
19
+ /**
20
+ * Agent ID to scope the chat to.
21
+ */
22
+ agentId?: number;
23
+ /**
24
+ * Initial messages to hydrate state with (e.g. server-rendered).
25
+ */
26
+ initialMessages?: ChatMessage[];
27
+ /**
28
+ * Initial session ID (e.g. from server-rendered state).
29
+ */
30
+ initialSessionId?: string;
31
+ /**
32
+ * Maximum number of continuation turns before stopping.
33
+ * Defaults to 20.
34
+ */
35
+ maxContinueTurns?: number;
36
+ /**
37
+ * Called when a new message is added to the conversation.
38
+ */
39
+ onMessage?: (message: ChatMessage) => void;
40
+ /**
41
+ * Called when an error occurs.
42
+ */
43
+ onError?: (error: Error) => void;
44
+ }
45
+ /**
46
+ * Return value of the useChat hook.
47
+ */
48
+ export interface UseChatReturn {
49
+ /** All messages in the current conversation. */
50
+ messages: ChatMessage[];
51
+ /** Whether a message is being sent/processed. */
52
+ isLoading: boolean;
53
+ /** Current continuation turn count (0 when not processing). */
54
+ turnCount: number;
55
+ /** Current availability state. */
56
+ availability: ChatAvailability;
57
+ /** Active session ID. */
58
+ sessionId: string | null;
59
+ /** List of sessions. */
60
+ sessions: ChatSession[];
61
+ /** Whether sessions are loading. */
62
+ sessionsLoading: boolean;
63
+ /** Send a user message. */
64
+ sendMessage: (content: string) => void;
65
+ /** Switch to a different session. */
66
+ switchSession: (sessionId: string) => void;
67
+ /** Create a new session. */
68
+ newSession: () => void;
69
+ /** Delete a session. */
70
+ deleteSession: (sessionId: string) => void;
71
+ /** Clear the current session's messages locally. */
72
+ clearSession: () => void;
73
+ /** Refresh the session list. */
74
+ refreshSessions: () => void;
75
+ }
76
+ /**
77
+ * Core state orchestrator for the chat UI.
78
+ *
79
+ * Manages messages, sessions, continuation loops, and availability
80
+ * by calling the standard chat REST endpoints directly.
81
+ *
82
+ * @example
83
+ * ```tsx
84
+ * import apiFetch from '@wordpress/api-fetch';
85
+ *
86
+ * const chat = useChat({
87
+ * basePath: '/datamachine/v1/chat',
88
+ * fetchFn: apiFetch,
89
+ * agentId: 5,
90
+ * });
91
+ *
92
+ * return (
93
+ * <>
94
+ * <ChatMessages messages={chat.messages} />
95
+ * <TypingIndicator visible={chat.isLoading} />
96
+ * <ChatInput onSend={chat.sendMessage} disabled={chat.isLoading} />
97
+ * </>
98
+ * );
99
+ * ```
100
+ */
101
+ export declare function useChat({ basePath, fetchFn, agentId, initialMessages, initialSessionId, maxContinueTurns, onMessage, onError, }: UseChatOptions): UseChatReturn;
102
+ //# sourceMappingURL=useChat.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useChat.d.ts","sourceRoot":"","sources":["../../src/hooks/useChat.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,KAAK,EAAE,OAAO,EAAiB,MAAM,WAAW,CAAC;AASxD;;GAEG;AACH,MAAM,WAAW,cAAc;IAC9B;;;OAGG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,OAAO,EAAE,OAAO,CAAC;IACjB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;OAEG;IACH,eAAe,CAAC,EAAE,WAAW,EAAE,CAAC;IAChC;;OAEG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;OAEG;IACH,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,CAAC;IAC3C;;OAEG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CACjC;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC7B,gDAAgD;IAChD,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,iDAAiD;IACjD,SAAS,EAAE,OAAO,CAAC;IACnB,+DAA+D;IAC/D,SAAS,EAAE,MAAM,CAAC;IAClB,kCAAkC;IAClC,YAAY,EAAE,gBAAgB,CAAC;IAC/B,yBAAyB;IACzB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,wBAAwB;IACxB,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,oCAAoC;IACpC,eAAe,EAAE,OAAO,CAAC;IACzB,2BAA2B;IAC3B,WAAW,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,qCAAqC;IACrC,aAAa,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,4BAA4B;IAC5B,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,wBAAwB;IACxB,aAAa,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,oDAAoD;IACpD,YAAY,EAAE,MAAM,IAAI,CAAC;IACzB,gCAAgC;IAChC,eAAe,EAAE,MAAM,IAAI,CAAC;CAC5B;AAOD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,OAAO,CAAC,EACvB,QAAQ,EACR,OAAO,EACP,OAAO,EACP,eAAe,EACf,gBAAgB,EAChB,gBAAqB,EACrB,SAAS,EACT,OAAO,GACP,EAAE,cAAc,GAAG,aAAa,CAwLhC"}
@@ -0,0 +1,192 @@
1
+ import { useState, useCallback, useRef, useEffect } from 'react';
2
+ import { sendMessage as apiSendMessage, continueResponse as apiContinueResponse, listSessions as apiListSessions, loadSession as apiLoadSession, deleteSession as apiDeleteSession, } from "../api.js";
3
+ let messageIdCounter = 0;
4
+ function generateMessageId() {
5
+ return `msg_${Date.now()}_${++messageIdCounter}`;
6
+ }
7
+ /**
8
+ * Core state orchestrator for the chat UI.
9
+ *
10
+ * Manages messages, sessions, continuation loops, and availability
11
+ * by calling the standard chat REST endpoints directly.
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * import apiFetch from '@wordpress/api-fetch';
16
+ *
17
+ * const chat = useChat({
18
+ * basePath: '/datamachine/v1/chat',
19
+ * fetchFn: apiFetch,
20
+ * agentId: 5,
21
+ * });
22
+ *
23
+ * return (
24
+ * <>
25
+ * <ChatMessages messages={chat.messages} />
26
+ * <TypingIndicator visible={chat.isLoading} />
27
+ * <ChatInput onSend={chat.sendMessage} disabled={chat.isLoading} />
28
+ * </>
29
+ * );
30
+ * ```
31
+ */
32
+ export function useChat({ basePath, fetchFn, agentId, initialMessages, initialSessionId, maxContinueTurns = 20, onMessage, onError, }) {
33
+ const [messages, setMessages] = useState(initialMessages ?? []);
34
+ const [isLoading, setIsLoading] = useState(false);
35
+ const [turnCount, setTurnCount] = useState(0);
36
+ const [availability, setAvailability] = useState({ status: 'ready' });
37
+ const [sessionId, setSessionId] = useState(initialSessionId ?? null);
38
+ const [sessions, setSessions] = useState([]);
39
+ const [sessionsLoading, setSessionsLoading] = useState(false);
40
+ // Build API config from props
41
+ const configRef = useRef({ basePath, fetchFn, agentId });
42
+ configRef.current = { basePath, fetchFn, agentId };
43
+ const sessionIdRef = useRef(sessionId);
44
+ sessionIdRef.current = sessionId;
45
+ // Load sessions on mount
46
+ useEffect(() => {
47
+ const loadSessions = async () => {
48
+ setSessionsLoading(true);
49
+ try {
50
+ const list = await apiListSessions(configRef.current);
51
+ setSessions(list);
52
+ }
53
+ catch (err) {
54
+ // Sessions not available — degrade gracefully
55
+ onError?.(err instanceof Error ? err : new Error(String(err)));
56
+ }
57
+ finally {
58
+ setSessionsLoading(false);
59
+ }
60
+ };
61
+ loadSessions();
62
+ // eslint-disable-next-line react-hooks/exhaustive-deps
63
+ }, []);
64
+ const sendMessage = useCallback(async (content) => {
65
+ if (isLoading)
66
+ return;
67
+ // Optimistically add user message
68
+ const userMessage = {
69
+ id: generateMessageId(),
70
+ role: 'user',
71
+ content,
72
+ timestamp: new Date().toISOString(),
73
+ };
74
+ setMessages((prev) => [...prev, userMessage]);
75
+ onMessage?.(userMessage);
76
+ setIsLoading(true);
77
+ setTurnCount(0);
78
+ try {
79
+ const result = await apiSendMessage(configRef.current, content, sessionIdRef.current ?? undefined);
80
+ // Update session ID (may be newly created)
81
+ setSessionId(result.sessionId);
82
+ sessionIdRef.current = result.sessionId;
83
+ // Replace all messages with the full normalized conversation
84
+ setMessages(result.messages);
85
+ // Handle multi-turn continuation
86
+ if (!result.completed && !result.maxTurnsReached) {
87
+ let completed = false;
88
+ let turns = 0;
89
+ while (!completed && turns < maxContinueTurns) {
90
+ turns++;
91
+ setTurnCount(turns);
92
+ const continuation = await apiContinueResponse(configRef.current, result.sessionId);
93
+ setMessages((prev) => [...prev, ...continuation.messages]);
94
+ for (const msg of continuation.messages) {
95
+ onMessage?.(msg);
96
+ }
97
+ completed = continuation.completed || continuation.maxTurnsReached;
98
+ }
99
+ }
100
+ // Refresh sessions list after a message
101
+ apiListSessions(configRef.current)
102
+ .then(setSessions)
103
+ .catch(() => { });
104
+ }
105
+ catch (err) {
106
+ const error = err instanceof Error ? err : new Error(String(err));
107
+ onError?.(error);
108
+ // Check if it's an auth error
109
+ if (error.message.includes('403') || error.message.includes('rest_forbidden')) {
110
+ setAvailability({ status: 'login-required' });
111
+ }
112
+ // Add error as assistant message so it's visible in the UI
113
+ const errorMessage = {
114
+ id: generateMessageId(),
115
+ role: 'assistant',
116
+ content: `Sorry, something went wrong: ${error.message}`,
117
+ timestamp: new Date().toISOString(),
118
+ };
119
+ setMessages((prev) => [...prev, errorMessage]);
120
+ }
121
+ finally {
122
+ setIsLoading(false);
123
+ setTurnCount(0);
124
+ }
125
+ }, [isLoading, maxContinueTurns, onMessage, onError]);
126
+ const switchSession = useCallback(async (newSessionId) => {
127
+ setSessionId(newSessionId);
128
+ sessionIdRef.current = newSessionId;
129
+ setIsLoading(true);
130
+ try {
131
+ const loaded = await apiLoadSession(configRef.current, newSessionId);
132
+ setMessages(loaded);
133
+ }
134
+ catch (err) {
135
+ onError?.(err instanceof Error ? err : new Error(String(err)));
136
+ setMessages([]);
137
+ }
138
+ finally {
139
+ setIsLoading(false);
140
+ }
141
+ }, [onError]);
142
+ const newSession = useCallback(() => {
143
+ setSessionId(null);
144
+ sessionIdRef.current = null;
145
+ setMessages([]);
146
+ }, []);
147
+ const deleteSessionHandler = useCallback(async (targetSessionId) => {
148
+ try {
149
+ await apiDeleteSession(configRef.current, targetSessionId);
150
+ setSessions((prev) => prev.filter((s) => s.id !== targetSessionId));
151
+ if (sessionIdRef.current === targetSessionId) {
152
+ setSessionId(null);
153
+ sessionIdRef.current = null;
154
+ setMessages([]);
155
+ }
156
+ }
157
+ catch (err) {
158
+ onError?.(err instanceof Error ? err : new Error(String(err)));
159
+ }
160
+ }, [onError]);
161
+ const clearSession = useCallback(() => {
162
+ setMessages([]);
163
+ }, []);
164
+ const refreshSessions = useCallback(async () => {
165
+ setSessionsLoading(true);
166
+ try {
167
+ const list = await apiListSessions(configRef.current);
168
+ setSessions(list);
169
+ }
170
+ catch (err) {
171
+ onError?.(err instanceof Error ? err : new Error(String(err)));
172
+ }
173
+ finally {
174
+ setSessionsLoading(false);
175
+ }
176
+ }, [onError]);
177
+ return {
178
+ messages,
179
+ isLoading,
180
+ turnCount,
181
+ availability,
182
+ sessionId,
183
+ sessions,
184
+ sessionsLoading,
185
+ sendMessage,
186
+ switchSession,
187
+ newSession,
188
+ deleteSession: deleteSessionHandler,
189
+ clearSession,
190
+ refreshSessions,
191
+ };
192
+ }
@@ -0,0 +1,15 @@
1
+ export type { MessageRole, ToolCall, ToolResultMeta, ChatMessage, ContentFormat, ChatSession, ChatAvailability, ChatInitialState, RawMessage, RawSession, SessionMetadata, } from './types/index.ts';
2
+ export type { FetchFn, FetchOptions, ChatApiConfig, SendResult, ContinueResult } from './api.ts';
3
+ export { sendMessage, continueResponse, listSessions, loadSession, deleteSession, } from './api.ts';
4
+ export { normalizeMessage, normalizeConversation, normalizeSession } from './normalizer.ts';
5
+ export { ChatMessage as ChatMessageComponent, type ChatMessageProps, } from './components/ChatMessage.tsx';
6
+ export { ChatMessages, type ChatMessagesProps, } from './components/ChatMessages.tsx';
7
+ export { ChatInput, type ChatInputProps, } from './components/ChatInput.tsx';
8
+ export { ToolMessage, type ToolMessageProps, type ToolGroup, } from './components/ToolMessage.tsx';
9
+ export { TypingIndicator, type TypingIndicatorProps, } from './components/TypingIndicator.tsx';
10
+ export { SessionSwitcher, type SessionSwitcherProps, } from './components/SessionSwitcher.tsx';
11
+ export { ErrorBoundary, type ErrorBoundaryProps, } from './components/ErrorBoundary.tsx';
12
+ export { AvailabilityGate, type AvailabilityGateProps, } from './components/AvailabilityGate.tsx';
13
+ export { useChat, type UseChatOptions, type UseChatReturn, } from './hooks/useChat.ts';
14
+ export { Chat, type ChatProps } from './Chat.tsx';
15
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,YAAY,EACX,WAAW,EACX,QAAQ,EACR,cAAc,EACd,WAAW,EACX,aAAa,EACb,WAAW,EACX,gBAAgB,EAChB,gBAAgB,EAChB,UAAU,EACV,UAAU,EACV,eAAe,GACf,MAAM,kBAAkB,CAAC;AAG1B,YAAY,EAAE,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AACjG,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,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,WAAW,EACX,KAAK,gBAAgB,EACrB,KAAK,SAAS,GACd,MAAM,8BAA8B,CAAC;AAEtC,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,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,16 @@
1
+ export { sendMessage, continueResponse, listSessions, loadSession, deleteSession, } from "./api.js";
2
+ // Normalizer
3
+ export { normalizeMessage, normalizeConversation, normalizeSession } from "./normalizer.js";
4
+ // Components
5
+ export { ChatMessage as ChatMessageComponent, } from "./components/ChatMessage.js";
6
+ export { ChatMessages, } from "./components/ChatMessages.js";
7
+ export { ChatInput, } from "./components/ChatInput.js";
8
+ export { ToolMessage, } from "./components/ToolMessage.js";
9
+ export { TypingIndicator, } from "./components/TypingIndicator.js";
10
+ export { SessionSwitcher, } from "./components/SessionSwitcher.js";
11
+ export { ErrorBoundary, } from "./components/ErrorBoundary.js";
12
+ export { AvailabilityGate, } from "./components/AvailabilityGate.js";
13
+ // Hook
14
+ export { useChat, } from "./hooks/useChat.js";
15
+ // Composed
16
+ export { Chat } from "./Chat.js";