@object-ui/plugin-chatbot 3.3.0 → 3.3.2
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/CHANGELOG.md +19 -0
- package/README.md +20 -0
- package/dist/index.js +4120 -3703
- package/dist/index.umd.cjs +63 -33
- package/package.json +35 -11
- package/.turbo/turbo-build.log +0 -53
- package/src/ChatbotEnhanced.tsx +0 -426
- package/src/FloatingChatbot.tsx +0 -89
- package/src/FloatingChatbotPanel.tsx +0 -102
- package/src/FloatingChatbotProvider.tsx +0 -80
- package/src/FloatingChatbotTrigger.tsx +0 -55
- package/src/__tests__/ChatbotEnhanced.test.tsx +0 -199
- package/src/__tests__/ChatbotEnhancedStreaming.test.tsx +0 -159
- package/src/__tests__/FloatingChatbotProvider.test.tsx +0 -134
- package/src/__tests__/FloatingChatbotWidgets.test.tsx +0 -165
- package/src/__tests__/useObjectChat.test.tsx +0 -347
- package/src/index.tsx +0 -267
- package/src/renderer.tsx +0 -483
- package/src/useObjectChat.ts +0 -344
- package/src/utils.ts +0 -18
- package/tsconfig.json +0 -18
- package/vite.config.ts +0 -55
|
@@ -1,102 +0,0 @@
|
|
|
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 * as React from "react"
|
|
10
|
-
import { cn } from "@object-ui/components"
|
|
11
|
-
import { Button } from "@object-ui/components"
|
|
12
|
-
import { X, Maximize2, Minimize2 } from "lucide-react"
|
|
13
|
-
import { useFloatingChatbot } from "./FloatingChatbotProvider"
|
|
14
|
-
|
|
15
|
-
export interface FloatingChatbotPanelProps {
|
|
16
|
-
/** Panel title */
|
|
17
|
-
title?: string
|
|
18
|
-
/** Position of the panel (anchored to FAB corner) */
|
|
19
|
-
position?: "bottom-right" | "bottom-left"
|
|
20
|
-
/** Panel width in pixels (ignored in fullscreen) */
|
|
21
|
-
width?: number
|
|
22
|
-
/** Panel height in pixels (ignored in fullscreen) */
|
|
23
|
-
height?: number
|
|
24
|
-
/** Content to render inside the panel body */
|
|
25
|
-
children: React.ReactNode
|
|
26
|
-
/** Custom className for the panel container */
|
|
27
|
-
className?: string
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Floating panel overlay for the chatbot.
|
|
32
|
-
* Renders above all content, anchored to the configured position.
|
|
33
|
-
* Supports fullscreen toggle and close.
|
|
34
|
-
*/
|
|
35
|
-
export function FloatingChatbotPanel({
|
|
36
|
-
title = "Chat",
|
|
37
|
-
position = "bottom-right",
|
|
38
|
-
width = 400,
|
|
39
|
-
height = 520,
|
|
40
|
-
children,
|
|
41
|
-
className,
|
|
42
|
-
}: FloatingChatbotPanelProps) {
|
|
43
|
-
const { isOpen, isFullscreen, close, toggleFullscreen } = useFloatingChatbot()
|
|
44
|
-
|
|
45
|
-
if (!isOpen) return null
|
|
46
|
-
|
|
47
|
-
const panelStyle: React.CSSProperties = isFullscreen
|
|
48
|
-
? { inset: 0, width: "100vw", height: "100vh" }
|
|
49
|
-
: { width, height, maxHeight: "calc(100vh - 100px)" }
|
|
50
|
-
|
|
51
|
-
return (
|
|
52
|
-
<div
|
|
53
|
-
className={cn(
|
|
54
|
-
"fixed z-50 flex flex-col rounded-lg border bg-background shadow-xl overflow-hidden transition-all",
|
|
55
|
-
isFullscreen
|
|
56
|
-
? "inset-0 rounded-none"
|
|
57
|
-
: position === "bottom-right"
|
|
58
|
-
? "right-6 bottom-20"
|
|
59
|
-
: "left-6 bottom-20",
|
|
60
|
-
className
|
|
61
|
-
)}
|
|
62
|
-
style={panelStyle}
|
|
63
|
-
role="dialog"
|
|
64
|
-
aria-label={title}
|
|
65
|
-
data-testid="floating-chatbot-panel"
|
|
66
|
-
>
|
|
67
|
-
{/* Header */}
|
|
68
|
-
<div className="flex items-center justify-between px-4 py-2 border-b bg-muted/40">
|
|
69
|
-
<span className="text-sm font-medium truncate">{title}</span>
|
|
70
|
-
<div className="flex items-center gap-1">
|
|
71
|
-
<Button
|
|
72
|
-
variant="ghost"
|
|
73
|
-
size="icon"
|
|
74
|
-
className="h-7 w-7"
|
|
75
|
-
onClick={toggleFullscreen}
|
|
76
|
-
aria-label={isFullscreen ? "Exit fullscreen" : "Fullscreen"}
|
|
77
|
-
data-testid="floating-chatbot-fullscreen"
|
|
78
|
-
>
|
|
79
|
-
{isFullscreen ? (
|
|
80
|
-
<Minimize2 className="h-4 w-4" />
|
|
81
|
-
) : (
|
|
82
|
-
<Maximize2 className="h-4 w-4" />
|
|
83
|
-
)}
|
|
84
|
-
</Button>
|
|
85
|
-
<Button
|
|
86
|
-
variant="ghost"
|
|
87
|
-
size="icon"
|
|
88
|
-
className="h-7 w-7"
|
|
89
|
-
onClick={close}
|
|
90
|
-
aria-label="Close chat"
|
|
91
|
-
data-testid="floating-chatbot-close"
|
|
92
|
-
>
|
|
93
|
-
<X className="h-4 w-4" />
|
|
94
|
-
</Button>
|
|
95
|
-
</div>
|
|
96
|
-
</div>
|
|
97
|
-
|
|
98
|
-
{/* Body */}
|
|
99
|
-
<div className="flex-1 overflow-hidden">{children}</div>
|
|
100
|
-
</div>
|
|
101
|
-
)
|
|
102
|
-
}
|
|
@@ -1,80 +0,0 @@
|
|
|
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 * as React from "react"
|
|
10
|
-
|
|
11
|
-
export interface FloatingChatbotState {
|
|
12
|
-
/** Whether the floating panel is currently open */
|
|
13
|
-
isOpen: boolean
|
|
14
|
-
/** Whether the panel is in fullscreen mode */
|
|
15
|
-
isFullscreen: boolean
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface FloatingChatbotActions {
|
|
19
|
-
/** Open the floating panel */
|
|
20
|
-
open: () => void
|
|
21
|
-
/** Close the floating panel */
|
|
22
|
-
close: () => void
|
|
23
|
-
/** Toggle the floating panel open/closed */
|
|
24
|
-
toggle: () => void
|
|
25
|
-
/** Toggle fullscreen mode */
|
|
26
|
-
toggleFullscreen: () => void
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export type FloatingChatbotContextValue = FloatingChatbotState & FloatingChatbotActions
|
|
30
|
-
|
|
31
|
-
const FloatingChatbotContext = React.createContext<FloatingChatbotContextValue | null>(null)
|
|
32
|
-
|
|
33
|
-
export interface FloatingChatbotProviderProps {
|
|
34
|
-
/** Whether the panel is open by default */
|
|
35
|
-
defaultOpen?: boolean
|
|
36
|
-
children: React.ReactNode
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function FloatingChatbotProvider({
|
|
40
|
-
defaultOpen = false,
|
|
41
|
-
children,
|
|
42
|
-
}: FloatingChatbotProviderProps) {
|
|
43
|
-
const [isOpen, setIsOpen] = React.useState(defaultOpen)
|
|
44
|
-
const [isFullscreen, setIsFullscreen] = React.useState(false)
|
|
45
|
-
|
|
46
|
-
const value = React.useMemo<FloatingChatbotContextValue>(
|
|
47
|
-
() => ({
|
|
48
|
-
isOpen,
|
|
49
|
-
isFullscreen,
|
|
50
|
-
open: () => setIsOpen(true),
|
|
51
|
-
close: () => {
|
|
52
|
-
setIsOpen(false)
|
|
53
|
-
setIsFullscreen(false)
|
|
54
|
-
},
|
|
55
|
-
toggle: () => setIsOpen((prev) => !prev),
|
|
56
|
-
toggleFullscreen: () => setIsFullscreen((prev) => !prev),
|
|
57
|
-
}),
|
|
58
|
-
[isOpen, isFullscreen]
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
return (
|
|
62
|
-
<FloatingChatbotContext.Provider value={value}>
|
|
63
|
-
{children}
|
|
64
|
-
</FloatingChatbotContext.Provider>
|
|
65
|
-
)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Hook to access the floating chatbot state and actions.
|
|
70
|
-
* Must be used within a `FloatingChatbotProvider`.
|
|
71
|
-
*/
|
|
72
|
-
export function useFloatingChatbot(): FloatingChatbotContextValue {
|
|
73
|
-
const context = React.useContext(FloatingChatbotContext)
|
|
74
|
-
if (!context) {
|
|
75
|
-
throw new Error(
|
|
76
|
-
"useFloatingChatbot must be used within a <FloatingChatbotProvider>"
|
|
77
|
-
)
|
|
78
|
-
}
|
|
79
|
-
return context
|
|
80
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
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 * as React from "react"
|
|
10
|
-
import { cn } from "@object-ui/components"
|
|
11
|
-
import { Button } from "@object-ui/components"
|
|
12
|
-
import { MessageCircle, X } from "lucide-react"
|
|
13
|
-
import { useFloatingChatbot } from "./FloatingChatbotProvider"
|
|
14
|
-
|
|
15
|
-
export interface FloatingChatbotTriggerProps {
|
|
16
|
-
/** Position of the FAB */
|
|
17
|
-
position?: "bottom-right" | "bottom-left"
|
|
18
|
-
/** Size of the trigger button in pixels */
|
|
19
|
-
size?: number
|
|
20
|
-
/** Custom className */
|
|
21
|
-
className?: string
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Floating Action Button (FAB) trigger for the chatbot.
|
|
26
|
-
* Renders a circular button fixed to the viewport corner.
|
|
27
|
-
*/
|
|
28
|
-
export function FloatingChatbotTrigger({
|
|
29
|
-
position = "bottom-right",
|
|
30
|
-
size = 56,
|
|
31
|
-
className,
|
|
32
|
-
}: FloatingChatbotTriggerProps) {
|
|
33
|
-
const { isOpen, toggle } = useFloatingChatbot()
|
|
34
|
-
|
|
35
|
-
return (
|
|
36
|
-
<Button
|
|
37
|
-
onClick={toggle}
|
|
38
|
-
className={cn(
|
|
39
|
-
"fixed z-50 rounded-full shadow-lg transition-transform hover:scale-105",
|
|
40
|
-
position === "bottom-right" ? "right-6 bottom-6" : "left-6 bottom-6",
|
|
41
|
-
className
|
|
42
|
-
)}
|
|
43
|
-
style={{ width: size, height: size }}
|
|
44
|
-
size="icon"
|
|
45
|
-
aria-label={isOpen ? "Close chat" : "Open chat"}
|
|
46
|
-
data-testid="floating-chatbot-trigger"
|
|
47
|
-
>
|
|
48
|
-
{isOpen ? (
|
|
49
|
-
<X className="h-6 w-6" />
|
|
50
|
-
) : (
|
|
51
|
-
<MessageCircle className="h-6 w-6" />
|
|
52
|
-
)}
|
|
53
|
-
</Button>
|
|
54
|
-
)
|
|
55
|
-
}
|
|
@@ -1,199 +0,0 @@
|
|
|
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 } from 'vitest';
|
|
11
|
-
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
12
|
-
import { ChatbotEnhanced, type ChatMessage } from '../ChatbotEnhanced';
|
|
13
|
-
|
|
14
|
-
describe('ChatbotEnhanced', () => {
|
|
15
|
-
const mockMessages: ChatMessage[] = [
|
|
16
|
-
{
|
|
17
|
-
id: '1',
|
|
18
|
-
role: 'user',
|
|
19
|
-
content: 'Hello!',
|
|
20
|
-
timestamp: '10:00 AM',
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
id: '2',
|
|
24
|
-
role: 'assistant',
|
|
25
|
-
content: 'Hi there! How can I help you?',
|
|
26
|
-
timestamp: '10:01 AM',
|
|
27
|
-
},
|
|
28
|
-
];
|
|
29
|
-
|
|
30
|
-
it('should render without crashing', () => {
|
|
31
|
-
const { container } = render(<ChatbotEnhanced />);
|
|
32
|
-
expect(container).toBeTruthy();
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('should render messages', () => {
|
|
36
|
-
render(<ChatbotEnhanced messages={mockMessages} />);
|
|
37
|
-
|
|
38
|
-
expect(screen.getByText('Hello!')).toBeInTheDocument();
|
|
39
|
-
expect(screen.getByText('Hi there! How can I help you?')).toBeInTheDocument();
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('should render placeholder in input', () => {
|
|
43
|
-
const placeholder = 'Type your message...';
|
|
44
|
-
render(<ChatbotEnhanced placeholder={placeholder} />);
|
|
45
|
-
|
|
46
|
-
expect(screen.getByPlaceholderText(placeholder)).toBeInTheDocument();
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('should call onSendMessage when send button is clicked', async () => {
|
|
50
|
-
const onSendMessage = vi.fn();
|
|
51
|
-
render(<ChatbotEnhanced onSendMessage={onSendMessage} />);
|
|
52
|
-
|
|
53
|
-
const input = screen.getByPlaceholderText(/message/i);
|
|
54
|
-
const sendButton = screen.getByRole('button', { name: /send/i });
|
|
55
|
-
|
|
56
|
-
fireEvent.change(input, { target: { value: 'Test message' } });
|
|
57
|
-
fireEvent.click(sendButton);
|
|
58
|
-
|
|
59
|
-
await waitFor(() => {
|
|
60
|
-
expect(onSendMessage).toHaveBeenCalledWith('Test message', []);
|
|
61
|
-
});
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('should call onSendMessage when Enter is pressed', async () => {
|
|
65
|
-
const onSendMessage = vi.fn();
|
|
66
|
-
render(<ChatbotEnhanced onSendMessage={onSendMessage} />);
|
|
67
|
-
|
|
68
|
-
const input = screen.getByPlaceholderText(/message/i);
|
|
69
|
-
|
|
70
|
-
fireEvent.change(input, { target: { value: 'Test message' } });
|
|
71
|
-
fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' });
|
|
72
|
-
|
|
73
|
-
await waitFor(() => {
|
|
74
|
-
expect(onSendMessage).toHaveBeenCalledWith('Test message', []);
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('should not send empty messages', async () => {
|
|
79
|
-
const onSendMessage = vi.fn();
|
|
80
|
-
render(<ChatbotEnhanced onSendMessage={onSendMessage} />);
|
|
81
|
-
|
|
82
|
-
const sendButton = screen.getByRole('button', { name: /send/i });
|
|
83
|
-
|
|
84
|
-
fireEvent.click(sendButton);
|
|
85
|
-
|
|
86
|
-
await waitFor(() => {
|
|
87
|
-
expect(onSendMessage).not.toHaveBeenCalled();
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('should call onClear when clear button is clicked', () => {
|
|
92
|
-
const onClear = vi.fn();
|
|
93
|
-
render(<ChatbotEnhanced messages={mockMessages} onClear={onClear} />);
|
|
94
|
-
|
|
95
|
-
const clearButton = screen.getByRole('button', { name: /clear/i });
|
|
96
|
-
fireEvent.click(clearButton);
|
|
97
|
-
|
|
98
|
-
expect(onClear).toHaveBeenCalled();
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('should render markdown content when enabled', () => {
|
|
102
|
-
const markdownMessage: ChatMessage[] = [
|
|
103
|
-
{
|
|
104
|
-
id: '1',
|
|
105
|
-
role: 'assistant',
|
|
106
|
-
content: '**Bold text** and *italic text*',
|
|
107
|
-
},
|
|
108
|
-
];
|
|
109
|
-
|
|
110
|
-
render(<ChatbotEnhanced messages={markdownMessage} enableMarkdown={true} />);
|
|
111
|
-
|
|
112
|
-
// Check that markdown is rendered (will create strong and em tags)
|
|
113
|
-
const { container } = render(<ChatbotEnhanced messages={markdownMessage} enableMarkdown={true} />);
|
|
114
|
-
expect(container.querySelector('strong')).toBeTruthy();
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('should render plain text when markdown is disabled', () => {
|
|
118
|
-
const textMessage: ChatMessage[] = [
|
|
119
|
-
{
|
|
120
|
-
id: '1',
|
|
121
|
-
role: 'assistant',
|
|
122
|
-
content: '**Bold text** and *italic text*',
|
|
123
|
-
},
|
|
124
|
-
];
|
|
125
|
-
|
|
126
|
-
render(<ChatbotEnhanced messages={textMessage} enableMarkdown={false} />);
|
|
127
|
-
|
|
128
|
-
// Should render as plain text
|
|
129
|
-
expect(screen.getByText('**Bold text** and *italic text*')).toBeInTheDocument();
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it('should show timestamps when enabled', () => {
|
|
133
|
-
render(<ChatbotEnhanced messages={mockMessages} showTimestamp={true} />);
|
|
134
|
-
|
|
135
|
-
expect(screen.getByText('10:00 AM')).toBeInTheDocument();
|
|
136
|
-
expect(screen.getByText('10:01 AM')).toBeInTheDocument();
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('should disable input when disabled prop is true', () => {
|
|
140
|
-
render(<ChatbotEnhanced disabled={true} />);
|
|
141
|
-
|
|
142
|
-
const input = screen.getByPlaceholderText(/message/i);
|
|
143
|
-
expect(input).toBeDisabled();
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it('should render file upload button when enabled', () => {
|
|
147
|
-
render(<ChatbotEnhanced enableFileUpload={true} />);
|
|
148
|
-
|
|
149
|
-
const fileButton = screen.getByRole('button', { name: /attach/i });
|
|
150
|
-
expect(fileButton).toBeInTheDocument();
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it('should handle streaming messages', () => {
|
|
154
|
-
const streamingMessage: ChatMessage[] = [
|
|
155
|
-
{
|
|
156
|
-
id: '1',
|
|
157
|
-
role: 'assistant',
|
|
158
|
-
content: 'Streaming content...',
|
|
159
|
-
streaming: true,
|
|
160
|
-
},
|
|
161
|
-
];
|
|
162
|
-
|
|
163
|
-
const { container } = render(<ChatbotEnhanced messages={streamingMessage} />);
|
|
164
|
-
|
|
165
|
-
expect(screen.getByText('Streaming content...')).toBeInTheDocument();
|
|
166
|
-
// Streaming messages typically have a visual indicator (cursor/animation)
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
it('should render user and assistant avatars', () => {
|
|
170
|
-
render(
|
|
171
|
-
<ChatbotEnhanced
|
|
172
|
-
messages={mockMessages}
|
|
173
|
-
userAvatarFallback="U"
|
|
174
|
-
assistantAvatarFallback="A"
|
|
175
|
-
/>
|
|
176
|
-
);
|
|
177
|
-
|
|
178
|
-
// Avatars should be rendered
|
|
179
|
-
expect(screen.getByText('U')).toBeInTheDocument();
|
|
180
|
-
expect(screen.getByText('A')).toBeInTheDocument();
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it('should clear input after sending message', async () => {
|
|
184
|
-
const onSendMessage = vi.fn();
|
|
185
|
-
render(<ChatbotEnhanced onSendMessage={onSendMessage} />);
|
|
186
|
-
|
|
187
|
-
const input = screen.getByPlaceholderText(/message/i) as HTMLInputElement;
|
|
188
|
-
|
|
189
|
-
fireEvent.change(input, { target: { value: 'Test message' } });
|
|
190
|
-
expect(input.value).toBe('Test message');
|
|
191
|
-
|
|
192
|
-
const sendButton = screen.getByRole('button', { name: /send/i });
|
|
193
|
-
fireEvent.click(sendButton);
|
|
194
|
-
|
|
195
|
-
await waitFor(() => {
|
|
196
|
-
expect(input.value).toBe('');
|
|
197
|
-
});
|
|
198
|
-
});
|
|
199
|
-
});
|
|
@@ -1,159 +0,0 @@
|
|
|
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 } from 'vitest';
|
|
11
|
-
import { render, screen, fireEvent } from '@testing-library/react';
|
|
12
|
-
import { ChatbotEnhanced, type ChatMessage } from '../ChatbotEnhanced';
|
|
13
|
-
|
|
14
|
-
describe('ChatbotEnhanced - Streaming & AI Features', () => {
|
|
15
|
-
const mockMessages: ChatMessage[] = [
|
|
16
|
-
{ id: '1', role: 'user', content: 'Hello!' },
|
|
17
|
-
{ id: '2', role: 'assistant', content: 'Hi there!' },
|
|
18
|
-
];
|
|
19
|
-
|
|
20
|
-
it('should show stop button when isLoading and onStop is provided', () => {
|
|
21
|
-
const onStop = vi.fn();
|
|
22
|
-
render(
|
|
23
|
-
<ChatbotEnhanced
|
|
24
|
-
messages={mockMessages}
|
|
25
|
-
isLoading={true}
|
|
26
|
-
onStop={onStop}
|
|
27
|
-
/>
|
|
28
|
-
);
|
|
29
|
-
|
|
30
|
-
const stopButton = screen.getByRole('button', { name: /stop/i });
|
|
31
|
-
expect(stopButton).toBeInTheDocument();
|
|
32
|
-
|
|
33
|
-
fireEvent.click(stopButton);
|
|
34
|
-
expect(onStop).toHaveBeenCalledTimes(1);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('should not show stop button when not loading', () => {
|
|
38
|
-
const onStop = vi.fn();
|
|
39
|
-
render(
|
|
40
|
-
<ChatbotEnhanced
|
|
41
|
-
messages={mockMessages}
|
|
42
|
-
isLoading={false}
|
|
43
|
-
onStop={onStop}
|
|
44
|
-
/>
|
|
45
|
-
);
|
|
46
|
-
|
|
47
|
-
expect(screen.queryByRole('button', { name: /stop/i })).not.toBeInTheDocument();
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('should not show stop button when onStop is not provided', () => {
|
|
51
|
-
render(
|
|
52
|
-
<ChatbotEnhanced
|
|
53
|
-
messages={mockMessages}
|
|
54
|
-
isLoading={true}
|
|
55
|
-
/>
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
expect(screen.queryByRole('button', { name: /stop/i })).not.toBeInTheDocument();
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('should show error message when error is provided', () => {
|
|
62
|
-
const error = new Error('Connection failed');
|
|
63
|
-
render(
|
|
64
|
-
<ChatbotEnhanced
|
|
65
|
-
messages={mockMessages}
|
|
66
|
-
error={error}
|
|
67
|
-
/>
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
expect(screen.getByText('Connection failed')).toBeInTheDocument();
|
|
71
|
-
expect(screen.getByRole('alert')).toBeInTheDocument();
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('should show retry button in error state when onReload is provided', () => {
|
|
75
|
-
const onReload = vi.fn();
|
|
76
|
-
const error = new Error('Network error');
|
|
77
|
-
render(
|
|
78
|
-
<ChatbotEnhanced
|
|
79
|
-
messages={mockMessages}
|
|
80
|
-
error={error}
|
|
81
|
-
onReload={onReload}
|
|
82
|
-
/>
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
const retryButton = screen.getByRole('button', { name: /retry/i });
|
|
86
|
-
expect(retryButton).toBeInTheDocument();
|
|
87
|
-
|
|
88
|
-
fireEvent.click(retryButton);
|
|
89
|
-
expect(onReload).toHaveBeenCalledTimes(1);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('should not show retry button when onReload is not provided', () => {
|
|
93
|
-
const error = new Error('Error');
|
|
94
|
-
render(
|
|
95
|
-
<ChatbotEnhanced
|
|
96
|
-
messages={mockMessages}
|
|
97
|
-
error={error}
|
|
98
|
-
/>
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
expect(screen.queryByRole('button', { name: /retry/i })).not.toBeInTheDocument();
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it('should disable input when isLoading is true', () => {
|
|
105
|
-
render(
|
|
106
|
-
<ChatbotEnhanced
|
|
107
|
-
messages={mockMessages}
|
|
108
|
-
isLoading={true}
|
|
109
|
-
/>
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
const input = screen.getByPlaceholderText(/message/i);
|
|
113
|
-
expect(input).toBeDisabled();
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('should disable send button when isLoading is true', () => {
|
|
117
|
-
render(
|
|
118
|
-
<ChatbotEnhanced
|
|
119
|
-
messages={mockMessages}
|
|
120
|
-
isLoading={true}
|
|
121
|
-
/>
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
const sendButton = screen.getByRole('button', { name: /send/i });
|
|
125
|
-
expect(sendButton).toBeDisabled();
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it('should show streaming indicator on streaming messages', () => {
|
|
129
|
-
const streamingMessages: ChatMessage[] = [
|
|
130
|
-
{
|
|
131
|
-
id: '1',
|
|
132
|
-
role: 'assistant',
|
|
133
|
-
content: 'Generating...',
|
|
134
|
-
streaming: true,
|
|
135
|
-
},
|
|
136
|
-
];
|
|
137
|
-
|
|
138
|
-
const { container } = render(
|
|
139
|
-
<ChatbotEnhanced messages={streamingMessages} />
|
|
140
|
-
);
|
|
141
|
-
|
|
142
|
-
expect(screen.getByText('Generating...')).toBeInTheDocument();
|
|
143
|
-
// Streaming indicator should be present (bouncing dots)
|
|
144
|
-
const bouncingDots = container.querySelectorAll('.animate-bounce');
|
|
145
|
-
expect(bouncingDots.length).toBe(3);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it('should show default error message when error has no message', () => {
|
|
149
|
-
const error = new Error('');
|
|
150
|
-
render(
|
|
151
|
-
<ChatbotEnhanced
|
|
152
|
-
messages={mockMessages}
|
|
153
|
-
error={error}
|
|
154
|
-
/>
|
|
155
|
-
);
|
|
156
|
-
|
|
157
|
-
expect(screen.getByText('An error occurred')).toBeInTheDocument();
|
|
158
|
-
});
|
|
159
|
-
});
|