@object-ui/plugin-chatbot 3.1.4 → 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.
Files changed (42) hide show
  1. package/.turbo/turbo-build.log +40 -9
  2. package/CHANGELOG.md +27 -0
  3. package/README.md +86 -4
  4. package/dist/index.d.ts +1 -1
  5. package/dist/index.js +11875 -2897
  6. package/dist/index.umd.cjs +71 -28
  7. package/dist/{src → packages/plugin-chatbot/src}/ChatbotEnhanced.d.ts +8 -0
  8. package/dist/packages/plugin-chatbot/src/ChatbotEnhanced.d.ts.map +1 -0
  9. package/dist/packages/plugin-chatbot/src/FloatingChatbot.d.ts +15 -0
  10. package/dist/packages/plugin-chatbot/src/FloatingChatbot.d.ts.map +1 -0
  11. package/dist/packages/plugin-chatbot/src/FloatingChatbotPanel.d.ts +29 -0
  12. package/dist/packages/plugin-chatbot/src/FloatingChatbotPanel.d.ts.map +1 -0
  13. package/dist/packages/plugin-chatbot/src/FloatingChatbotProvider.d.ts +37 -0
  14. package/dist/packages/plugin-chatbot/src/FloatingChatbotProvider.d.ts.map +1 -0
  15. package/dist/packages/plugin-chatbot/src/FloatingChatbotTrigger.d.ts +21 -0
  16. package/dist/packages/plugin-chatbot/src/FloatingChatbotTrigger.d.ts.map +1 -0
  17. package/dist/{src → packages/plugin-chatbot/src}/index.d.ts +10 -0
  18. package/dist/packages/plugin-chatbot/src/index.d.ts.map +1 -0
  19. package/dist/packages/plugin-chatbot/src/renderer.d.ts.map +1 -0
  20. package/dist/packages/plugin-chatbot/src/useObjectChat.d.ts +112 -0
  21. package/dist/packages/plugin-chatbot/src/useObjectChat.d.ts.map +1 -0
  22. package/dist/packages/plugin-chatbot/src/utils.d.ts.map +1 -0
  23. package/package.json +10 -8
  24. package/src/ChatbotEnhanced.tsx +55 -4
  25. package/src/FloatingChatbot.tsx +89 -0
  26. package/src/FloatingChatbotPanel.tsx +102 -0
  27. package/src/FloatingChatbotProvider.tsx +80 -0
  28. package/src/FloatingChatbotTrigger.tsx +55 -0
  29. package/src/__tests__/ChatbotEnhancedStreaming.test.tsx +159 -0
  30. package/src/__tests__/FloatingChatbotProvider.test.tsx +134 -0
  31. package/src/__tests__/FloatingChatbotWidgets.test.tsx +165 -0
  32. package/src/__tests__/useObjectChat.test.tsx +347 -0
  33. package/src/index.tsx +19 -0
  34. package/src/renderer.tsx +261 -92
  35. package/src/useObjectChat.ts +344 -0
  36. package/vite.config.ts +2 -1
  37. package/dist/src/ChatbotEnhanced.d.ts.map +0 -1
  38. package/dist/src/index.d.ts.map +0 -1
  39. package/dist/src/renderer.d.ts.map +0 -1
  40. package/dist/src/utils.d.ts.map +0 -1
  41. /package/dist/{src → packages/plugin-chatbot/src}/renderer.d.ts +0 -0
  42. /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';