@object-ui/plugin-chatbot 3.1.5 → 3.3.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/.turbo/turbo-build.log +39 -8
- package/CHANGELOG.md +18 -0
- package/README.md +86 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +11875 -2897
- package/dist/index.umd.cjs +71 -28
- package/dist/{src → packages/plugin-chatbot/src}/ChatbotEnhanced.d.ts +8 -0
- package/dist/packages/plugin-chatbot/src/ChatbotEnhanced.d.ts.map +1 -0
- package/dist/packages/plugin-chatbot/src/FloatingChatbot.d.ts +15 -0
- package/dist/packages/plugin-chatbot/src/FloatingChatbot.d.ts.map +1 -0
- package/dist/packages/plugin-chatbot/src/FloatingChatbotPanel.d.ts +29 -0
- package/dist/packages/plugin-chatbot/src/FloatingChatbotPanel.d.ts.map +1 -0
- package/dist/packages/plugin-chatbot/src/FloatingChatbotProvider.d.ts +37 -0
- package/dist/packages/plugin-chatbot/src/FloatingChatbotProvider.d.ts.map +1 -0
- package/dist/packages/plugin-chatbot/src/FloatingChatbotTrigger.d.ts +21 -0
- package/dist/packages/plugin-chatbot/src/FloatingChatbotTrigger.d.ts.map +1 -0
- package/dist/{src → packages/plugin-chatbot/src}/index.d.ts +10 -0
- package/dist/packages/plugin-chatbot/src/index.d.ts.map +1 -0
- package/dist/packages/plugin-chatbot/src/renderer.d.ts.map +1 -0
- package/dist/packages/plugin-chatbot/src/useObjectChat.d.ts +112 -0
- package/dist/packages/plugin-chatbot/src/useObjectChat.d.ts.map +1 -0
- package/dist/packages/plugin-chatbot/src/utils.d.ts.map +1 -0
- package/package.json +10 -8
- package/src/ChatbotEnhanced.tsx +55 -4
- package/src/FloatingChatbot.tsx +89 -0
- package/src/FloatingChatbotPanel.tsx +102 -0
- package/src/FloatingChatbotProvider.tsx +80 -0
- package/src/FloatingChatbotTrigger.tsx +55 -0
- package/src/__tests__/ChatbotEnhancedStreaming.test.tsx +159 -0
- package/src/__tests__/FloatingChatbotProvider.test.tsx +134 -0
- package/src/__tests__/FloatingChatbotWidgets.test.tsx +165 -0
- package/src/__tests__/useObjectChat.test.tsx +347 -0
- package/src/index.tsx +19 -0
- package/src/renderer.tsx +261 -92
- package/src/useObjectChat.ts +344 -0
- package/vite.config.ts +2 -1
- package/dist/src/ChatbotEnhanced.d.ts.map +0 -1
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/renderer.d.ts.map +0 -1
- package/dist/src/utils.d.ts.map +0 -1
- /package/dist/{src → packages/plugin-chatbot/src}/renderer.d.ts +0 -0
- /package/dist/{src → packages/plugin-chatbot/src}/utils.d.ts +0 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import '@testing-library/jest-dom';
|
|
10
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
11
|
+
import { renderHook, act } from '@testing-library/react';
|
|
12
|
+
|
|
13
|
+
// Mock @ai-sdk/react so the static import in useObjectChat doesn't break tests
|
|
14
|
+
vi.mock('@ai-sdk/react', () => ({
|
|
15
|
+
useChat: vi.fn(() => ({
|
|
16
|
+
messages: [],
|
|
17
|
+
isLoading: false,
|
|
18
|
+
error: undefined,
|
|
19
|
+
input: '',
|
|
20
|
+
setInput: vi.fn(),
|
|
21
|
+
handleInputChange: vi.fn(),
|
|
22
|
+
append: vi.fn(),
|
|
23
|
+
stop: vi.fn(),
|
|
24
|
+
reload: vi.fn(),
|
|
25
|
+
setMessages: vi.fn(),
|
|
26
|
+
})),
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
import { useObjectChat } from '../useObjectChat';
|
|
30
|
+
|
|
31
|
+
describe('useObjectChat', () => {
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
vi.useFakeTimers();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
vi.useRealTimers();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('local/legacy mode (no api)', () => {
|
|
41
|
+
it('should initialize with empty messages when no initialMessages provided', () => {
|
|
42
|
+
const { result } = renderHook(() => useObjectChat());
|
|
43
|
+
|
|
44
|
+
expect(result.current.messages).toEqual([]);
|
|
45
|
+
expect(result.current.isLoading).toBe(false);
|
|
46
|
+
expect(result.current.error).toBeUndefined();
|
|
47
|
+
expect(result.current.isApiMode).toBe(false);
|
|
48
|
+
expect(result.current.input).toBe('');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should initialize with provided messages', () => {
|
|
52
|
+
const initialMessages = [
|
|
53
|
+
{ id: '1', role: 'assistant' as const, content: 'Hello!' },
|
|
54
|
+
{ id: '2', role: 'user' as const, content: 'Hi!' },
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
const { result } = renderHook(() =>
|
|
58
|
+
useObjectChat({ initialMessages })
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
expect(result.current.messages).toHaveLength(2);
|
|
62
|
+
expect(result.current.messages[0].content).toBe('Hello!');
|
|
63
|
+
expect(result.current.messages[1].content).toBe('Hi!');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should add a user message when sendMessage is called', () => {
|
|
67
|
+
const { result } = renderHook(() => useObjectChat());
|
|
68
|
+
|
|
69
|
+
act(() => {
|
|
70
|
+
result.current.sendMessage('Hello world');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(result.current.messages).toHaveLength(1);
|
|
74
|
+
expect(result.current.messages[0].role).toBe('user');
|
|
75
|
+
expect(result.current.messages[0].content).toBe('Hello world');
|
|
76
|
+
expect(result.current.messages[0].id).toBeTruthy();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should not send empty messages', () => {
|
|
80
|
+
const { result } = renderHook(() => useObjectChat());
|
|
81
|
+
|
|
82
|
+
act(() => {
|
|
83
|
+
result.current.sendMessage('');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(result.current.messages).toHaveLength(0);
|
|
87
|
+
|
|
88
|
+
act(() => {
|
|
89
|
+
result.current.sendMessage(' ');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(result.current.messages).toHaveLength(0);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should trim whitespace from messages', () => {
|
|
96
|
+
const { result } = renderHook(() => useObjectChat());
|
|
97
|
+
|
|
98
|
+
act(() => {
|
|
99
|
+
result.current.sendMessage(' Hello ');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
expect(result.current.messages[0].content).toBe('Hello');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should call onSend callback when message is sent', () => {
|
|
106
|
+
const onSend = vi.fn();
|
|
107
|
+
const { result } = renderHook(() => useObjectChat({ onSend }));
|
|
108
|
+
|
|
109
|
+
act(() => {
|
|
110
|
+
result.current.sendMessage('Test message');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
expect(onSend).toHaveBeenCalledWith(
|
|
114
|
+
'Test message',
|
|
115
|
+
expect.arrayContaining([
|
|
116
|
+
expect.objectContaining({ content: 'Test message', role: 'user' }),
|
|
117
|
+
])
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should add auto-response after delay when autoResponse is enabled', () => {
|
|
122
|
+
const { result } = renderHook(() =>
|
|
123
|
+
useObjectChat({
|
|
124
|
+
autoResponse: true,
|
|
125
|
+
autoResponseText: 'Auto reply!',
|
|
126
|
+
autoResponseDelay: 500,
|
|
127
|
+
})
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
act(() => {
|
|
131
|
+
result.current.sendMessage('Hello');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
expect(result.current.messages).toHaveLength(1);
|
|
135
|
+
expect(result.current.isLoading).toBe(true);
|
|
136
|
+
|
|
137
|
+
act(() => {
|
|
138
|
+
vi.advanceTimersByTime(500);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
expect(result.current.messages).toHaveLength(2);
|
|
142
|
+
expect(result.current.messages[1].role).toBe('assistant');
|
|
143
|
+
expect(result.current.messages[1].content).toBe('Auto reply!');
|
|
144
|
+
expect(result.current.isLoading).toBe(false);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should use default auto-response text when none provided', () => {
|
|
148
|
+
const { result } = renderHook(() =>
|
|
149
|
+
useObjectChat({
|
|
150
|
+
autoResponse: true,
|
|
151
|
+
autoResponseDelay: 500,
|
|
152
|
+
})
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
act(() => {
|
|
156
|
+
result.current.sendMessage('Hello');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
act(() => {
|
|
160
|
+
vi.advanceTimersByTime(500);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
expect(result.current.messages[1].content).toBe('Thank you for your message!');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should add timestamps when showTimestamp is enabled', () => {
|
|
167
|
+
const { result } = renderHook(() =>
|
|
168
|
+
useObjectChat({ showTimestamp: true })
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
act(() => {
|
|
172
|
+
result.current.sendMessage('Hello');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
expect(result.current.messages[0].timestamp).toBeTruthy();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('should not add timestamps when showTimestamp is disabled', () => {
|
|
179
|
+
const { result } = renderHook(() =>
|
|
180
|
+
useObjectChat({ showTimestamp: false })
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
act(() => {
|
|
184
|
+
result.current.sendMessage('Hello');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
expect(result.current.messages[0].timestamp).toBeUndefined();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('should clear all messages when clear is called', () => {
|
|
191
|
+
const { result } = renderHook(() =>
|
|
192
|
+
useObjectChat({
|
|
193
|
+
initialMessages: [
|
|
194
|
+
{ id: '1', role: 'assistant', content: 'Hello!' },
|
|
195
|
+
],
|
|
196
|
+
})
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
expect(result.current.messages).toHaveLength(1);
|
|
200
|
+
|
|
201
|
+
act(() => {
|
|
202
|
+
result.current.clear();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
expect(result.current.messages).toHaveLength(0);
|
|
206
|
+
expect(result.current.isLoading).toBe(false);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('should stop auto-response timer when stop is called', () => {
|
|
210
|
+
const { result } = renderHook(() =>
|
|
211
|
+
useObjectChat({
|
|
212
|
+
autoResponse: true,
|
|
213
|
+
autoResponseDelay: 1000,
|
|
214
|
+
})
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
act(() => {
|
|
218
|
+
result.current.sendMessage('Hello');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
expect(result.current.isLoading).toBe(true);
|
|
222
|
+
|
|
223
|
+
act(() => {
|
|
224
|
+
result.current.stop();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
expect(result.current.isLoading).toBe(false);
|
|
228
|
+
|
|
229
|
+
// Advance time - no auto-response should appear
|
|
230
|
+
act(() => {
|
|
231
|
+
vi.advanceTimersByTime(2000);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
expect(result.current.messages).toHaveLength(1); // Only user message
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('reload should be a no-op in local mode', () => {
|
|
238
|
+
const { result } = renderHook(() =>
|
|
239
|
+
useObjectChat({
|
|
240
|
+
initialMessages: [
|
|
241
|
+
{ id: '1', role: 'user', content: 'Hello' },
|
|
242
|
+
],
|
|
243
|
+
})
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
act(() => {
|
|
247
|
+
result.current.reload();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
expect(result.current.messages).toHaveLength(1);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('should handle input state management', () => {
|
|
254
|
+
const { result } = renderHook(() => useObjectChat());
|
|
255
|
+
|
|
256
|
+
expect(result.current.input).toBe('');
|
|
257
|
+
|
|
258
|
+
act(() => {
|
|
259
|
+
result.current.setInput('Hello');
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
expect(result.current.input).toBe('Hello');
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('should handle input change events', () => {
|
|
266
|
+
const { result } = renderHook(() => useObjectChat());
|
|
267
|
+
|
|
268
|
+
act(() => {
|
|
269
|
+
result.current.handleInputChange({
|
|
270
|
+
target: { value: 'Hello world' },
|
|
271
|
+
} as React.ChangeEvent<HTMLInputElement>);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
expect(result.current.input).toBe('Hello world');
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('should clear input after sending message', () => {
|
|
278
|
+
const { result } = renderHook(() => useObjectChat());
|
|
279
|
+
|
|
280
|
+
act(() => {
|
|
281
|
+
result.current.setInput('Hello');
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
expect(result.current.input).toBe('Hello');
|
|
285
|
+
|
|
286
|
+
act(() => {
|
|
287
|
+
result.current.sendMessage('Hello');
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
expect(result.current.input).toBe('');
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('should normalize initial messages with missing fields', () => {
|
|
294
|
+
const { result } = renderHook(() =>
|
|
295
|
+
useObjectChat({
|
|
296
|
+
initialMessages: [
|
|
297
|
+
{ id: '', role: 'user' as const, content: '' },
|
|
298
|
+
],
|
|
299
|
+
})
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
expect(result.current.messages[0].id).toBe('msg-0');
|
|
303
|
+
expect(result.current.messages[0].content).toBe('');
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('should report isApiMode as false in local mode', () => {
|
|
307
|
+
const { result } = renderHook(() => useObjectChat());
|
|
308
|
+
expect(result.current.isApiMode).toBe(false);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('should report isApiMode as false when api is empty string', () => {
|
|
312
|
+
const { result } = renderHook(() => useObjectChat({ api: '' }));
|
|
313
|
+
expect(result.current.isApiMode).toBe(false);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('should cancel pending auto-response timer when clear is called', () => {
|
|
317
|
+
const { result } = renderHook(() =>
|
|
318
|
+
useObjectChat({
|
|
319
|
+
autoResponse: true,
|
|
320
|
+
autoResponseText: 'Auto reply!',
|
|
321
|
+
autoResponseDelay: 1000,
|
|
322
|
+
})
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
act(() => {
|
|
326
|
+
result.current.sendMessage('Hello');
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
expect(result.current.messages).toHaveLength(1);
|
|
330
|
+
expect(result.current.isLoading).toBe(true);
|
|
331
|
+
|
|
332
|
+
act(() => {
|
|
333
|
+
result.current.clear();
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
expect(result.current.messages).toHaveLength(0);
|
|
337
|
+
expect(result.current.isLoading).toBe(false);
|
|
338
|
+
|
|
339
|
+
// Advance time - no auto-response should appear after clear
|
|
340
|
+
act(() => {
|
|
341
|
+
vi.advanceTimersByTime(2000);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
expect(result.current.messages).toHaveLength(0);
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
});
|
package/src/index.tsx
CHANGED
|
@@ -244,5 +244,24 @@ TypingIndicator.displayName = "TypingIndicator"
|
|
|
244
244
|
|
|
245
245
|
export { Chatbot, TypingIndicator }
|
|
246
246
|
|
|
247
|
+
// Export the composable chat hook for custom integrations
|
|
248
|
+
export { useObjectChat } from './useObjectChat';
|
|
249
|
+
export type { UseObjectChatOptions, UseObjectChatReturn } from './useObjectChat';
|
|
250
|
+
|
|
251
|
+
// Export floating chatbot components
|
|
252
|
+
export { FloatingChatbot } from './FloatingChatbot';
|
|
253
|
+
export type { FloatingChatbotProps } from './FloatingChatbot';
|
|
254
|
+
export { FloatingChatbotProvider, useFloatingChatbot } from './FloatingChatbotProvider';
|
|
255
|
+
export type {
|
|
256
|
+
FloatingChatbotProviderProps,
|
|
257
|
+
FloatingChatbotState,
|
|
258
|
+
FloatingChatbotActions,
|
|
259
|
+
FloatingChatbotContextValue,
|
|
260
|
+
} from './FloatingChatbotProvider';
|
|
261
|
+
export { FloatingChatbotTrigger } from './FloatingChatbotTrigger';
|
|
262
|
+
export type { FloatingChatbotTriggerProps } from './FloatingChatbotTrigger';
|
|
263
|
+
export { FloatingChatbotPanel } from './FloatingChatbotPanel';
|
|
264
|
+
export type { FloatingChatbotPanelProps } from './FloatingChatbotPanel';
|
|
265
|
+
|
|
247
266
|
// Export renderer to register the component with ObjectUI
|
|
248
267
|
export * from './renderer';
|