@agentforge-io/chat-react 0.1.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/dist/ChatPanel.d.ts +21 -0
- package/dist/ChatPanel.js +185 -0
- package/dist/hooks.d.ts +27 -0
- package/dist/hooks.js +53 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +36 -0
- package/dist/provider.d.ts +48 -0
- package/dist/provider.js +90 -0
- package/package.json +32 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type CSSProperties } from 'react';
|
|
2
|
+
export interface ChatPanelProps {
|
|
3
|
+
/** Override the header title. Falls back to theme.title, then agent.name. */
|
|
4
|
+
title?: string;
|
|
5
|
+
/** Override the input placeholder. */
|
|
6
|
+
placeholder?: string;
|
|
7
|
+
/** Tagline shown under the title in the header. Falls back to agent.description. */
|
|
8
|
+
subtitle?: string;
|
|
9
|
+
/** Extra style for the root container. The panel is `flex column` and
|
|
10
|
+
* fills its parent by default — wrap it in a sized element to control
|
|
11
|
+
* the height. */
|
|
12
|
+
style?: CSSProperties;
|
|
13
|
+
className?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Reference rendering of a chat panel. Pure React, no portal, no fixed
|
|
17
|
+
* positioning — drop it inside whatever container your app provides
|
|
18
|
+
* (modal, sidebar, page section). For the floating launcher pattern, see
|
|
19
|
+
* the script-tag widget at /widget.js.
|
|
20
|
+
*/
|
|
21
|
+
export declare function ChatPanel({ title, placeholder, subtitle, style, className, }: ChatPanelProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
'use client';
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.ChatPanel = ChatPanel;
|
|
5
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
6
|
+
const react_1 = require("react");
|
|
7
|
+
const hooks_1 = require("./hooks");
|
|
8
|
+
/**
|
|
9
|
+
* Reference rendering of a chat panel. Pure React, no portal, no fixed
|
|
10
|
+
* positioning — drop it inside whatever container your app provides
|
|
11
|
+
* (modal, sidebar, page section). For the floating launcher pattern, see
|
|
12
|
+
* the script-tag widget at /widget.js.
|
|
13
|
+
*/
|
|
14
|
+
function ChatPanel({ title, placeholder = 'Type a message…', subtitle, style, className, }) {
|
|
15
|
+
const { agent, theme } = (0, hooks_1.useAgent)();
|
|
16
|
+
const { messages, status, send, isBusy, error } = (0, hooks_1.useChat)();
|
|
17
|
+
const resolvedTitle = title ?? theme?.title ?? agent?.name ?? 'Chat';
|
|
18
|
+
const resolvedSubtitle = subtitle ?? agent?.description ?? (status === 'loading' ? 'Loading…' : '');
|
|
19
|
+
const primary = theme?.primaryColor ?? '#7c5cff';
|
|
20
|
+
const primarySoft = hexToRgba(primary, 0.35);
|
|
21
|
+
// Same CSS variable contract as the script-tag widget, so a custom <style>
|
|
22
|
+
// block at app level can re-skin both views with one rule.
|
|
23
|
+
const containerStyle = {
|
|
24
|
+
['--af-primary']: primary,
|
|
25
|
+
['--af-primary-soft']: primarySoft,
|
|
26
|
+
display: 'flex',
|
|
27
|
+
flexDirection: 'column',
|
|
28
|
+
height: '100%',
|
|
29
|
+
minHeight: 0,
|
|
30
|
+
background: '#fff',
|
|
31
|
+
color: '#0f172a',
|
|
32
|
+
borderRadius: 16,
|
|
33
|
+
overflow: 'hidden',
|
|
34
|
+
border: '1px solid #e2e8f0',
|
|
35
|
+
fontFamily: 'system-ui, -apple-system, "Segoe UI", Roboto, sans-serif',
|
|
36
|
+
...style,
|
|
37
|
+
};
|
|
38
|
+
const [draft, setDraft] = (0, react_1.useState)('');
|
|
39
|
+
const messagesRef = (0, react_1.useRef)(null);
|
|
40
|
+
// Stick to the bottom as new chunks arrive. Cheap and works well for the
|
|
41
|
+
// streaming-reply case where messages grow.
|
|
42
|
+
(0, react_1.useEffect)(() => {
|
|
43
|
+
const el = messagesRef.current;
|
|
44
|
+
if (!el)
|
|
45
|
+
return;
|
|
46
|
+
el.scrollTop = el.scrollHeight;
|
|
47
|
+
}, [messages]);
|
|
48
|
+
function onSubmit(e) {
|
|
49
|
+
e.preventDefault();
|
|
50
|
+
const text = draft.trim();
|
|
51
|
+
if (!text || isBusy)
|
|
52
|
+
return;
|
|
53
|
+
setDraft('');
|
|
54
|
+
void send(text);
|
|
55
|
+
}
|
|
56
|
+
function onKeyDown(e) {
|
|
57
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
58
|
+
e.preventDefault();
|
|
59
|
+
onSubmit(e);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: className, style: containerStyle, children: [(0, jsx_runtime_1.jsxs)("header", { style: {
|
|
63
|
+
padding: '14px 16px',
|
|
64
|
+
background: '#0f172a',
|
|
65
|
+
color: 'white',
|
|
66
|
+
display: 'flex',
|
|
67
|
+
alignItems: 'center',
|
|
68
|
+
gap: 10,
|
|
69
|
+
}, children: [theme?.avatarUrl && ((0, jsx_runtime_1.jsx)("img", { src: theme.avatarUrl, alt: "", style: {
|
|
70
|
+
width: 28,
|
|
71
|
+
height: 28,
|
|
72
|
+
borderRadius: '50%',
|
|
73
|
+
objectFit: 'cover',
|
|
74
|
+
background: 'rgba(255,255,255,0.1)',
|
|
75
|
+
} })), (0, jsx_runtime_1.jsxs)("div", { style: { flex: 1, minWidth: 0 }, children: [(0, jsx_runtime_1.jsx)("div", { style: { fontWeight: 600, fontSize: 14 }, children: resolvedTitle }), resolvedSubtitle && ((0, jsx_runtime_1.jsx)("div", { style: { fontSize: 11, opacity: 0.7, marginTop: 2 }, children: resolvedSubtitle }))] })] }), (0, jsx_runtime_1.jsx)("div", { ref: messagesRef, style: {
|
|
76
|
+
flex: 1,
|
|
77
|
+
minHeight: 0,
|
|
78
|
+
overflowY: 'auto',
|
|
79
|
+
padding: 16,
|
|
80
|
+
background: '#f8fafc',
|
|
81
|
+
display: 'flex',
|
|
82
|
+
flexDirection: 'column',
|
|
83
|
+
gap: 10,
|
|
84
|
+
}, children: messages.map((m) => ((0, jsx_runtime_1.jsx)(Bubble, { role: m.role, streaming: m.isStreaming, primary: primary, children: m.content }, m.id))) }), error && ((0, jsx_runtime_1.jsx)("div", { style: {
|
|
85
|
+
padding: '10px 16px',
|
|
86
|
+
fontSize: 12,
|
|
87
|
+
color: '#b91c1c',
|
|
88
|
+
background: '#fef2f2',
|
|
89
|
+
borderTop: '1px solid #fee2e2',
|
|
90
|
+
}, children: error })), (0, jsx_runtime_1.jsxs)("form", { onSubmit: onSubmit, style: {
|
|
91
|
+
padding: 12,
|
|
92
|
+
borderTop: '1px solid #e2e8f0',
|
|
93
|
+
background: 'white',
|
|
94
|
+
display: 'flex',
|
|
95
|
+
gap: 8,
|
|
96
|
+
}, children: [(0, jsx_runtime_1.jsx)("textarea", { value: draft, onChange: (e) => setDraft(e.target.value), onKeyDown: onKeyDown, placeholder: placeholder, rows: 1, style: {
|
|
97
|
+
flex: 1,
|
|
98
|
+
border: '1px solid #cbd5e1',
|
|
99
|
+
borderRadius: 10,
|
|
100
|
+
padding: '10px 12px',
|
|
101
|
+
fontSize: 14,
|
|
102
|
+
fontFamily: 'inherit',
|
|
103
|
+
outline: 'none',
|
|
104
|
+
resize: 'none',
|
|
105
|
+
minHeight: 40,
|
|
106
|
+
maxHeight: 120,
|
|
107
|
+
color: '#0f172a',
|
|
108
|
+
} }), (0, jsx_runtime_1.jsx)("button", { type: "submit", disabled: isBusy || !draft.trim(), style: {
|
|
109
|
+
background: primary,
|
|
110
|
+
color: 'white',
|
|
111
|
+
border: 'none',
|
|
112
|
+
borderRadius: 10,
|
|
113
|
+
padding: '0 14px',
|
|
114
|
+
cursor: isBusy || !draft.trim() ? 'not-allowed' : 'pointer',
|
|
115
|
+
fontWeight: 500,
|
|
116
|
+
fontSize: 14,
|
|
117
|
+
opacity: isBusy || !draft.trim() ? 0.5 : 1,
|
|
118
|
+
}, children: "Send" })] })] }));
|
|
119
|
+
}
|
|
120
|
+
function Bubble({ role, streaming, primary, children, }) {
|
|
121
|
+
if (role === 'system') {
|
|
122
|
+
return ((0, jsx_runtime_1.jsx)("div", { style: { alignSelf: 'center', fontSize: 12, color: '#64748b', padding: '4px 8px' }, children: children }));
|
|
123
|
+
}
|
|
124
|
+
const isUser = role === 'user';
|
|
125
|
+
// No content yet but still streaming → render the typing-dots indicator
|
|
126
|
+
// standalone, without the trailing caret that would have nothing to
|
|
127
|
+
// anchor to.
|
|
128
|
+
const isTyping = !isUser && streaming && (children === '' || children == null);
|
|
129
|
+
return ((0, jsx_runtime_1.jsxs)("div", { style: {
|
|
130
|
+
alignSelf: isUser ? 'flex-end' : 'flex-start',
|
|
131
|
+
maxWidth: '80%',
|
|
132
|
+
padding: isTyping ? '14px 14px' : '10px 12px',
|
|
133
|
+
minWidth: isTyping ? 56 : undefined,
|
|
134
|
+
borderRadius: 14,
|
|
135
|
+
fontSize: 14,
|
|
136
|
+
lineHeight: 1.5,
|
|
137
|
+
wordWrap: 'break-word',
|
|
138
|
+
whiteSpace: 'pre-wrap',
|
|
139
|
+
background: isUser ? primary : '#ffffff',
|
|
140
|
+
color: isUser ? 'white' : '#0f172a',
|
|
141
|
+
border: isUser ? 'none' : '1px solid #e2e8f0',
|
|
142
|
+
borderBottomRightRadius: isUser ? 4 : 14,
|
|
143
|
+
borderBottomLeftRadius: isUser ? 14 : 4,
|
|
144
|
+
animation: 'af-react-msg-in 220ms cubic-bezier(.2,.8,.2,1)',
|
|
145
|
+
}, children: [isTyping ? (0, jsx_runtime_1.jsx)(TypingDots, {}) : children, !isTyping && streaming && ((0, jsx_runtime_1.jsx)("span", { "aria-hidden": true, style: {
|
|
146
|
+
display: 'inline-block',
|
|
147
|
+
marginLeft: 2,
|
|
148
|
+
color: '#94a3b8',
|
|
149
|
+
animation: 'af-react-blink 1s infinite',
|
|
150
|
+
}, children: "\u258D" })), (0, jsx_runtime_1.jsx)("style", { children: `
|
|
151
|
+
@keyframes af-react-blink { 0%,80%,100% { opacity:.3 } 40% { opacity:1 } }
|
|
152
|
+
@keyframes af-react-bounce {
|
|
153
|
+
0%, 60%, 100% { transform: translateY(0); opacity: 0.4; }
|
|
154
|
+
30% { transform: translateY(-4px); opacity: 1; }
|
|
155
|
+
}
|
|
156
|
+
@keyframes af-react-msg-in {
|
|
157
|
+
from { opacity: 0; transform: translateY(6px); }
|
|
158
|
+
to { opacity: 1; transform: translateY(0); }
|
|
159
|
+
}
|
|
160
|
+
` })] }));
|
|
161
|
+
}
|
|
162
|
+
function TypingDots() {
|
|
163
|
+
const dot = {
|
|
164
|
+
width: 7,
|
|
165
|
+
height: 7,
|
|
166
|
+
borderRadius: '50%',
|
|
167
|
+
background: '#64748b',
|
|
168
|
+
display: 'inline-block',
|
|
169
|
+
animation: 'af-react-bounce 1.2s infinite ease-in-out',
|
|
170
|
+
};
|
|
171
|
+
return ((0, jsx_runtime_1.jsxs)("span", { "aria-label": "Assistant is typing", style: { display: 'inline-flex', gap: 4, alignItems: 'center' }, children: [(0, jsx_runtime_1.jsx)("span", { style: { ...dot } }), (0, jsx_runtime_1.jsx)("span", { style: { ...dot, animationDelay: '0.15s' } }), (0, jsx_runtime_1.jsx)("span", { style: { ...dot, animationDelay: '0.3s' } })] }));
|
|
172
|
+
}
|
|
173
|
+
function hexToRgba(hex, alpha) {
|
|
174
|
+
if (!hex || hex[0] !== '#')
|
|
175
|
+
return hex;
|
|
176
|
+
let h = hex.slice(1);
|
|
177
|
+
if (h.length === 3)
|
|
178
|
+
h = h.split('').map((c) => c + c).join('');
|
|
179
|
+
if (h.length !== 6)
|
|
180
|
+
return hex;
|
|
181
|
+
const r = parseInt(h.slice(0, 2), 16);
|
|
182
|
+
const g = parseInt(h.slice(2, 4), 16);
|
|
183
|
+
const b = parseInt(h.slice(4, 6), 16);
|
|
184
|
+
return `rgba(${r},${g},${b},${alpha})`;
|
|
185
|
+
}
|
package/dist/hooks.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ChatAgentSummary, ChatMessage, ChatSessionStatus, ChatTheme } from '@agentforge-io/chat-sdk';
|
|
2
|
+
/**
|
|
3
|
+
* Primary hook. Returns reactive chat state + actions. Re-renders on every
|
|
4
|
+
* SDK event, but the underlying state object is recreated each time so
|
|
5
|
+
* referential equality only changes when something actually changes —
|
|
6
|
+
* downstream `useMemo`s stay efficient.
|
|
7
|
+
*/
|
|
8
|
+
export declare function useChat(): {
|
|
9
|
+
status: ChatSessionStatus;
|
|
10
|
+
messages: ChatMessage[];
|
|
11
|
+
conversationId?: string;
|
|
12
|
+
error?: string;
|
|
13
|
+
browserSessionId: string;
|
|
14
|
+
send: (text: string) => Promise<string>;
|
|
15
|
+
start: () => Promise<void>;
|
|
16
|
+
destroy: () => void;
|
|
17
|
+
isBusy: boolean;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Read-only metadata about the resolved agent + theme. Returns `undefined`
|
|
21
|
+
* until the session finishes its initial `start()` call.
|
|
22
|
+
*/
|
|
23
|
+
export declare function useAgent(): {
|
|
24
|
+
agent?: ChatAgentSummary;
|
|
25
|
+
theme?: ChatTheme;
|
|
26
|
+
loading: boolean;
|
|
27
|
+
};
|
package/dist/hooks.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
'use client';
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.useChat = useChat;
|
|
5
|
+
exports.useAgent = useAgent;
|
|
6
|
+
const react_1 = require("react");
|
|
7
|
+
const provider_1 = require("./provider");
|
|
8
|
+
/**
|
|
9
|
+
* Primary hook. Returns reactive chat state + actions. Re-renders on every
|
|
10
|
+
* SDK event, but the underlying state object is recreated each time so
|
|
11
|
+
* referential equality only changes when something actually changes —
|
|
12
|
+
* downstream `useMemo`s stay efficient.
|
|
13
|
+
*/
|
|
14
|
+
function useChat() {
|
|
15
|
+
const { session } = (0, provider_1.useAgentChatContext)();
|
|
16
|
+
const state = (0, provider_1.useChatSessionState)();
|
|
17
|
+
const send = (0, react_1.useCallback)((text) => session.send(text), [session]);
|
|
18
|
+
const start = (0, react_1.useCallback)(() => session.start(), [session]);
|
|
19
|
+
const destroy = (0, react_1.useCallback)(() => session.destroy(), [session]);
|
|
20
|
+
return (0, react_1.useMemo)(() => ({
|
|
21
|
+
status: state.status,
|
|
22
|
+
messages: state.messages,
|
|
23
|
+
conversationId: state.conversationId,
|
|
24
|
+
error: state.lastError,
|
|
25
|
+
browserSessionId: session.browserSessionId,
|
|
26
|
+
send,
|
|
27
|
+
start,
|
|
28
|
+
destroy,
|
|
29
|
+
isBusy: state.status === 'loading' ||
|
|
30
|
+
state.status === 'sending' ||
|
|
31
|
+
state.status === 'streaming',
|
|
32
|
+
}), [state, session.browserSessionId, send, start, destroy]);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Read-only metadata about the resolved agent + theme. Returns `undefined`
|
|
36
|
+
* until the session finishes its initial `start()` call.
|
|
37
|
+
*/
|
|
38
|
+
function useAgent() {
|
|
39
|
+
const { themeOverride } = (0, provider_1.useAgentChatContext)();
|
|
40
|
+
const state = (0, provider_1.useChatSessionState)();
|
|
41
|
+
return (0, react_1.useMemo)(() => {
|
|
42
|
+
// Provider-level theme wins over the token's theme — same precedence
|
|
43
|
+
// as the data-* overrides in the script-tag widget.
|
|
44
|
+
const mergedTheme = state.theme || themeOverride
|
|
45
|
+
? { ...(state.theme ?? {}), ...(themeOverride ?? {}) }
|
|
46
|
+
: undefined;
|
|
47
|
+
return {
|
|
48
|
+
agent: state.agent,
|
|
49
|
+
theme: mergedTheme,
|
|
50
|
+
loading: state.status === 'loading' || (!state.agent && state.status !== 'error'),
|
|
51
|
+
};
|
|
52
|
+
}, [state.agent, state.theme, state.status, themeOverride]);
|
|
53
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @agentforge-io/chat-react — React + Next.js bindings for the headless chat SDK.
|
|
3
|
+
*
|
|
4
|
+
* Two ways to use this:
|
|
5
|
+
*
|
|
6
|
+
* // 1. Provider + hooks: build your own UI
|
|
7
|
+
* <AgentChatProvider token={...} apiBaseUrl={...}>
|
|
8
|
+
* <YourChatUi />
|
|
9
|
+
* </AgentChatProvider>
|
|
10
|
+
*
|
|
11
|
+
* function YourChatUi() {
|
|
12
|
+
* const { messages, send, isBusy } = useChat();
|
|
13
|
+
* const { agent, theme } = useAgent();
|
|
14
|
+
* ...
|
|
15
|
+
* }
|
|
16
|
+
*
|
|
17
|
+
* // 2. Drop-in: use the reference panel
|
|
18
|
+
* <AgentChatProvider ...>
|
|
19
|
+
* <ChatPanel />
|
|
20
|
+
* </AgentChatProvider>
|
|
21
|
+
*
|
|
22
|
+
* All components are marked `'use client'` so they work with the Next.js
|
|
23
|
+
* App Router out of the box.
|
|
24
|
+
*/
|
|
25
|
+
export { AgentChatProvider, useAgentChatContext, useChatSessionState } from './provider';
|
|
26
|
+
export type { AgentChatProviderProps, ChatUserContext, } from './provider';
|
|
27
|
+
export { useChat, useAgent } from './hooks';
|
|
28
|
+
export { ChatPanel } from './ChatPanel';
|
|
29
|
+
export type { ChatPanelProps } from './ChatPanel';
|
|
30
|
+
export type { ChatAgentSummary, ChatEvent, ChatMessage, ChatRole, ChatSessionStatus, ChatTheme, } from '@agentforge-io/chat-sdk';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @agentforge-io/chat-react — React + Next.js bindings for the headless chat SDK.
|
|
4
|
+
*
|
|
5
|
+
* Two ways to use this:
|
|
6
|
+
*
|
|
7
|
+
* // 1. Provider + hooks: build your own UI
|
|
8
|
+
* <AgentChatProvider token={...} apiBaseUrl={...}>
|
|
9
|
+
* <YourChatUi />
|
|
10
|
+
* </AgentChatProvider>
|
|
11
|
+
*
|
|
12
|
+
* function YourChatUi() {
|
|
13
|
+
* const { messages, send, isBusy } = useChat();
|
|
14
|
+
* const { agent, theme } = useAgent();
|
|
15
|
+
* ...
|
|
16
|
+
* }
|
|
17
|
+
*
|
|
18
|
+
* // 2. Drop-in: use the reference panel
|
|
19
|
+
* <AgentChatProvider ...>
|
|
20
|
+
* <ChatPanel />
|
|
21
|
+
* </AgentChatProvider>
|
|
22
|
+
*
|
|
23
|
+
* All components are marked `'use client'` so they work with the Next.js
|
|
24
|
+
* App Router out of the box.
|
|
25
|
+
*/
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.ChatPanel = exports.useAgent = exports.useChat = exports.useChatSessionState = exports.useAgentChatContext = exports.AgentChatProvider = void 0;
|
|
28
|
+
var provider_1 = require("./provider");
|
|
29
|
+
Object.defineProperty(exports, "AgentChatProvider", { enumerable: true, get: function () { return provider_1.AgentChatProvider; } });
|
|
30
|
+
Object.defineProperty(exports, "useAgentChatContext", { enumerable: true, get: function () { return provider_1.useAgentChatContext; } });
|
|
31
|
+
Object.defineProperty(exports, "useChatSessionState", { enumerable: true, get: function () { return provider_1.useChatSessionState; } });
|
|
32
|
+
var hooks_1 = require("./hooks");
|
|
33
|
+
Object.defineProperty(exports, "useChat", { enumerable: true, get: function () { return hooks_1.useChat; } });
|
|
34
|
+
Object.defineProperty(exports, "useAgent", { enumerable: true, get: function () { return hooks_1.useAgent; } });
|
|
35
|
+
var ChatPanel_1 = require("./ChatPanel");
|
|
36
|
+
Object.defineProperty(exports, "ChatPanel", { enumerable: true, get: function () { return ChatPanel_1.ChatPanel; } });
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { ChatSession, type ChatSessionState, type ChatTheme } from '@agentforge-io/chat-sdk';
|
|
3
|
+
/**
|
|
4
|
+
* UserContext is opaque metadata your app can attach to a chat. It travels in
|
|
5
|
+
* the conversation's `metadata.userContext` when one is created — useful for
|
|
6
|
+
* tying server-side analytics or chat transcripts back to the logged-in user
|
|
7
|
+
* in YOUR app. Nothing here is enforced by the chat token; treat it as data,
|
|
8
|
+
* not auth.
|
|
9
|
+
*/
|
|
10
|
+
export type ChatUserContext = Record<string, unknown>;
|
|
11
|
+
export interface AgentChatProviderProps {
|
|
12
|
+
/** Public chat token from your AgentForge admin. */
|
|
13
|
+
token: string;
|
|
14
|
+
/** Base URL of your AgentForge API (e.g. https://api.example.com). */
|
|
15
|
+
apiBaseUrl: string;
|
|
16
|
+
/** Optional persistent browser id. If omitted, the SDK generates one and
|
|
17
|
+
* the consumer can read it via `useChat().browserSessionId`. */
|
|
18
|
+
browserSessionId?: string;
|
|
19
|
+
/** Disable SSE streaming. Defaults to true (stream). */
|
|
20
|
+
stream?: boolean;
|
|
21
|
+
/** Theme override. Wins over whatever the token's theme says, mirroring
|
|
22
|
+
* the data-* override semantics of the script-tag widget. */
|
|
23
|
+
theme?: ChatTheme;
|
|
24
|
+
/** Free-form metadata about the end-user, attached to new conversations. */
|
|
25
|
+
userContext?: ChatUserContext;
|
|
26
|
+
/** Auto-start the session on mount. Defaults true; set false to start
|
|
27
|
+
* manually via `useChat().start()`. */
|
|
28
|
+
autoStart?: boolean;
|
|
29
|
+
children: ReactNode;
|
|
30
|
+
}
|
|
31
|
+
interface ContextValue {
|
|
32
|
+
session: ChatSession;
|
|
33
|
+
subscribe: (cb: () => void) => () => void;
|
|
34
|
+
getState: () => ChatSessionState;
|
|
35
|
+
/** Theme override passed via Provider, surfaced to consumers separately
|
|
36
|
+
* from the token's theme so the View layer can merge them as it wants. */
|
|
37
|
+
themeOverride?: ChatTheme;
|
|
38
|
+
userContext?: ChatUserContext;
|
|
39
|
+
}
|
|
40
|
+
export declare function AgentChatProvider({ token, apiBaseUrl, browserSessionId, stream, theme, userContext, autoStart, children, }: AgentChatProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
41
|
+
export declare function useAgentChatContext(): ContextValue;
|
|
42
|
+
/**
|
|
43
|
+
* Internal: subscribe a component to the session's state changes via React
|
|
44
|
+
* 18's `useSyncExternalStore`. Returns the latest state on every render and
|
|
45
|
+
* triggers re-renders only when the session emits.
|
|
46
|
+
*/
|
|
47
|
+
export declare function useChatSessionState(): ChatSessionState;
|
|
48
|
+
export {};
|
package/dist/provider.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
'use client';
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.AgentChatProvider = AgentChatProvider;
|
|
5
|
+
exports.useAgentChatContext = useAgentChatContext;
|
|
6
|
+
exports.useChatSessionState = useChatSessionState;
|
|
7
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
8
|
+
const react_1 = require("react");
|
|
9
|
+
const chat_sdk_1 = require("@agentforge-io/chat-sdk");
|
|
10
|
+
const AgentChatContext = (0, react_1.createContext)(null);
|
|
11
|
+
function AgentChatProvider({ token, apiBaseUrl, browserSessionId, stream, theme, userContext, autoStart = true, children, }) {
|
|
12
|
+
// Keep a stable session for the lifetime of these props. Re-create only
|
|
13
|
+
// when the inputs that materially identify the chat change.
|
|
14
|
+
const sessionRef = (0, react_1.useRef)(null);
|
|
15
|
+
const key = `${token}|${apiBaseUrl}|${browserSessionId ?? ''}|${stream ?? true}`;
|
|
16
|
+
if (!sessionRef.current || sessionRef.current.key !== key) {
|
|
17
|
+
// Tear down a previous session if any — switching tokens is rare but
|
|
18
|
+
// we don't want a dangling SDK instance holding listeners.
|
|
19
|
+
sessionRef.current?.session.destroy();
|
|
20
|
+
const opts = { token, apiBaseUrl, browserSessionId, stream };
|
|
21
|
+
sessionRef.current = { session: new chat_sdk_1.ChatSession(opts), key };
|
|
22
|
+
}
|
|
23
|
+
const session = sessionRef.current.session;
|
|
24
|
+
// Auto-start once per session instance.
|
|
25
|
+
const startedRef = (0, react_1.useRef)(false);
|
|
26
|
+
(0, react_1.useEffect)(() => {
|
|
27
|
+
if (!autoStart)
|
|
28
|
+
return;
|
|
29
|
+
if (startedRef.current)
|
|
30
|
+
return;
|
|
31
|
+
startedRef.current = true;
|
|
32
|
+
session.start();
|
|
33
|
+
}, [session, autoStart]);
|
|
34
|
+
// Clean up on unmount. Only when the component is actually leaving — the
|
|
35
|
+
// session ref above survives Strict Mode double-invocation in dev.
|
|
36
|
+
(0, react_1.useEffect)(() => {
|
|
37
|
+
return () => {
|
|
38
|
+
session.destroy();
|
|
39
|
+
sessionRef.current = null;
|
|
40
|
+
startedRef.current = false;
|
|
41
|
+
};
|
|
42
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
43
|
+
}, [session]);
|
|
44
|
+
// Cached snapshot for useSyncExternalStore. `session.getState()` returns a
|
|
45
|
+
// fresh object on every call (it's a fresh aggregate of the session's
|
|
46
|
+
// internal mutable state), so reading it directly trips React's
|
|
47
|
+
// "getSnapshot should be cached" detector and loops.
|
|
48
|
+
//
|
|
49
|
+
// We keep the last seen snapshot in a ref, invalidate it inside the
|
|
50
|
+
// subscribe callback, and only recompute on the next getState() call.
|
|
51
|
+
// Result: stable reference between events, fresh reference per event.
|
|
52
|
+
const snapshotRef = (0, react_1.useRef)(null);
|
|
53
|
+
const value = (0, react_1.useMemo)(() => {
|
|
54
|
+
// Reset cache when the session itself swaps (token change, etc).
|
|
55
|
+
snapshotRef.current = null;
|
|
56
|
+
return {
|
|
57
|
+
session,
|
|
58
|
+
themeOverride: theme,
|
|
59
|
+
userContext,
|
|
60
|
+
subscribe: (cb) => session.onEvent(() => {
|
|
61
|
+
// Invalidate cache so the next getState() re-reads — then notify.
|
|
62
|
+
snapshotRef.current = null;
|
|
63
|
+
cb();
|
|
64
|
+
}),
|
|
65
|
+
getState: () => {
|
|
66
|
+
if (snapshotRef.current === null) {
|
|
67
|
+
snapshotRef.current = session.getState();
|
|
68
|
+
}
|
|
69
|
+
return snapshotRef.current;
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}, [session, theme, userContext]);
|
|
73
|
+
return ((0, jsx_runtime_1.jsx)(AgentChatContext.Provider, { value: value, children: children }));
|
|
74
|
+
}
|
|
75
|
+
function useAgentChatContext() {
|
|
76
|
+
const ctx = (0, react_1.useContext)(AgentChatContext);
|
|
77
|
+
if (!ctx) {
|
|
78
|
+
throw new Error('useAgentChatContext must be used inside <AgentChatProvider>');
|
|
79
|
+
}
|
|
80
|
+
return ctx;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Internal: subscribe a component to the session's state changes via React
|
|
84
|
+
* 18's `useSyncExternalStore`. Returns the latest state on every render and
|
|
85
|
+
* triggers re-renders only when the session emits.
|
|
86
|
+
*/
|
|
87
|
+
function useChatSessionState() {
|
|
88
|
+
const { subscribe, getState } = useAgentChatContext();
|
|
89
|
+
return (0, react_1.useSyncExternalStore)(subscribe, getState, getState);
|
|
90
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agentforge-io/chat-react",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "React + Next.js adapter for @agentforge-io/chat-sdk. Provider, hooks, and a reference ChatPanel component.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc -p tsconfig.build.json",
|
|
13
|
+
"build:watch": "tsc -p tsconfig.build.json --watch",
|
|
14
|
+
"clean": "rm -rf dist *.tgz"
|
|
15
|
+
},
|
|
16
|
+
"peerDependencies": {
|
|
17
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
18
|
+
"react-dom": "^18.0.0 || ^19.0.0",
|
|
19
|
+
"@agentforge-io/chat-sdk": "^0.1.0"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@agentforge-io/chat-sdk": "^0.1.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/node": "^20.0.0",
|
|
26
|
+
"@types/react": "^18.0.0 || ^19.0.0",
|
|
27
|
+
"@types/react-dom": "^18.0.0 || ^19.0.0",
|
|
28
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
29
|
+
"react-dom": "^18.0.0 || ^19.0.0",
|
|
30
|
+
"typescript": "^5.0.0"
|
|
31
|
+
}
|
|
32
|
+
}
|