@object-ui/plugin-chatbot 3.3.0 → 3.3.1
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 +10 -0
- package/README.md +20 -0
- package/dist/index.js +4121 -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
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/plugin-chatbot",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Chatbot interface plugin for Object UI",
|
|
7
|
-
"homepage": "https://www.objectui.org",
|
|
7
|
+
"homepage": "https://www.objectui.org/docs/plugins/plugin-chatbot",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
|
-
"url": "https://github.com/objectstack-ai/objectui.git",
|
|
10
|
+
"url": "git+https://github.com/objectstack-ai/objectui.git",
|
|
11
11
|
"directory": "packages/plugin-chatbot"
|
|
12
12
|
},
|
|
13
13
|
"bugs": {
|
|
@@ -24,16 +24,16 @@
|
|
|
24
24
|
}
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@ai-sdk/react": "^3.0.
|
|
28
|
-
"ai": "^6.0.
|
|
27
|
+
"@ai-sdk/react": "^3.0.176",
|
|
28
|
+
"ai": "^6.0.174",
|
|
29
29
|
"lucide-react": "^1.8.0",
|
|
30
30
|
"react-markdown": "^10.1.0",
|
|
31
31
|
"react-syntax-highlighter": "^16.1.1",
|
|
32
32
|
"remark-gfm": "^4.0.1",
|
|
33
|
-
"@object-ui/components": "3.3.
|
|
34
|
-
"@object-ui/core": "3.3.
|
|
35
|
-
"@object-ui/react": "3.3.
|
|
36
|
-
"@object-ui/types": "3.3.
|
|
33
|
+
"@object-ui/components": "3.3.1",
|
|
34
|
+
"@object-ui/core": "3.3.1",
|
|
35
|
+
"@object-ui/react": "3.3.1",
|
|
36
|
+
"@object-ui/types": "3.3.1"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"react": "^18.0.0 || ^19.0.0",
|
|
@@ -44,10 +44,34 @@
|
|
|
44
44
|
"@types/react-dom": "19.2.3",
|
|
45
45
|
"@types/react-syntax-highlighter": "^15.5.13",
|
|
46
46
|
"@vitejs/plugin-react": "^6.0.1",
|
|
47
|
-
"typescript": "^6.0.
|
|
48
|
-
"vite": "^8.0.
|
|
47
|
+
"typescript": "^6.0.3",
|
|
48
|
+
"vite": "^8.0.9",
|
|
49
49
|
"vite-plugin-dts": "^4.5.4"
|
|
50
50
|
},
|
|
51
|
+
"keywords": [
|
|
52
|
+
"objectui",
|
|
53
|
+
"sdui",
|
|
54
|
+
"schema-driven-ui",
|
|
55
|
+
"react",
|
|
56
|
+
"tailwind",
|
|
57
|
+
"shadcn",
|
|
58
|
+
"objectstack",
|
|
59
|
+
"plugin",
|
|
60
|
+
"chatbot",
|
|
61
|
+
"ai",
|
|
62
|
+
"chat",
|
|
63
|
+
"conversation"
|
|
64
|
+
],
|
|
65
|
+
"author": "ObjectStack Team <team@objectstack.ai>",
|
|
66
|
+
"publishConfig": {
|
|
67
|
+
"access": "public"
|
|
68
|
+
},
|
|
69
|
+
"files": [
|
|
70
|
+
"dist",
|
|
71
|
+
"README.md",
|
|
72
|
+
"CHANGELOG.md",
|
|
73
|
+
"LICENSE"
|
|
74
|
+
],
|
|
51
75
|
"scripts": {
|
|
52
76
|
"build": "vite build",
|
|
53
77
|
"test": "vitest run",
|
package/.turbo/turbo-build.log
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
> @object-ui/plugin-chatbot@3.3.0 build /home/runner/work/objectui/objectui/packages/plugin-chatbot
|
|
3
|
-
> vite build
|
|
4
|
-
|
|
5
|
-
[36mvite v8.0.8 [32mbuilding client environment for production...[36m[39m
|
|
6
|
-
[2K
|
|
7
|
-
rendering chunks...
|
|
8
|
-
[32m
|
|
9
|
-
[36m[vite:dts][32m Start generate declaration files...[39m
|
|
10
|
-
[96msrc/useObjectChat.ts[0m:[93m178[0m:[93m5[0m - [91merror[0m[90m TS2353: [0mObject literal may only specify known properties, and 'api' does not exist in type 'UseChatOptions<UIMessage<unknown, UIDataTypes, UITools>>'.
|
|
11
|
-
|
|
12
|
-
[7m178[0m api: isApiMode ? api! : '/api/noop',
|
|
13
|
-
[7m [0m [91m ~~~[0m
|
|
14
|
-
[96msrc/useObjectChat.ts[0m:[93m216[0m:[93m7[0m - [91merror[0m[90m TS2339: [0mProperty 'isLoading' does not exist on type 'UseChatHelpers<UIMessage<unknown, UIDataTypes, UITools>>'.
|
|
15
|
-
|
|
16
|
-
[7m216[0m isLoading,
|
|
17
|
-
[7m [0m [91m ~~~~~~~~~[0m
|
|
18
|
-
[96msrc/useObjectChat.ts[0m:[93m218[0m:[93m7[0m - [91merror[0m[90m TS2339: [0mProperty 'input' does not exist on type 'UseChatHelpers<UIMessage<unknown, UIDataTypes, UITools>>'.
|
|
19
|
-
|
|
20
|
-
[7m218[0m input,
|
|
21
|
-
[7m [0m [91m ~~~~~[0m
|
|
22
|
-
[96msrc/useObjectChat.ts[0m:[93m219[0m:[93m7[0m - [91merror[0m[90m TS2339: [0mProperty 'setInput' does not exist on type 'UseChatHelpers<UIMessage<unknown, UIDataTypes, UITools>>'.
|
|
23
|
-
|
|
24
|
-
[7m219[0m setInput,
|
|
25
|
-
[7m [0m [91m ~~~~~~~~[0m
|
|
26
|
-
[96msrc/useObjectChat.ts[0m:[93m220[0m:[93m7[0m - [91merror[0m[90m TS2339: [0mProperty 'handleInputChange' does not exist on type 'UseChatHelpers<UIMessage<unknown, UIDataTypes, UITools>>'.
|
|
27
|
-
|
|
28
|
-
[7m220[0m handleInputChange,
|
|
29
|
-
[7m [0m [91m ~~~~~~~~~~~~~~~~~[0m
|
|
30
|
-
[96msrc/useObjectChat.ts[0m:[93m221[0m:[93m7[0m - [91merror[0m[90m TS2339: [0mProperty 'append' does not exist on type 'UseChatHelpers<UIMessage<unknown, UIDataTypes, UITools>>'.
|
|
31
|
-
|
|
32
|
-
[7m221[0m append,
|
|
33
|
-
[7m [0m [91m ~~~~~~[0m
|
|
34
|
-
[96msrc/useObjectChat.ts[0m:[93m223[0m:[93m7[0m - [91merror[0m[90m TS2339: [0mProperty 'reload' does not exist on type 'UseChatHelpers<UIMessage<unknown, UIDataTypes, UITools>>'.
|
|
35
|
-
|
|
36
|
-
[7m223[0m reload,
|
|
37
|
-
[7m [0m [91m ~~~~~~[0m
|
|
38
|
-
|
|
39
|
-
[32m[36m[vite:dts][32m Declaration files built in 30288ms.
|
|
40
|
-
[39m
|
|
41
|
-
computing gzip size...
|
|
42
|
-
dist/index.js 1,222.85 kB │ gzip: 352.84 kB
|
|
43
|
-
|
|
44
|
-
[33m[33m[PLUGIN_TIMINGS] Warning:[0m Your build spent significant time in plugin `vite:dts`. See https://rolldown.rs/options/checks#plugintimings for more details.
|
|
45
|
-
[39m
|
|
46
|
-
[2K
|
|
47
|
-
rendering chunks...
|
|
48
|
-
computing gzip size...
|
|
49
|
-
dist/index.umd.cjs 1,006.91 kB │ gzip: 327.98 kB
|
|
50
|
-
|
|
51
|
-
[33m[33m[MISSING_GLOBAL_NAME] Warning:[0m No name was provided for external module "react/jsx-runtime" in "output.globals" – guessing "react_jsx_runtime".
|
|
52
|
-
[39m
|
|
53
|
-
[32m✓ built in 37.23s[39m
|
package/src/ChatbotEnhanced.tsx
DELETED
|
@@ -1,426 +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, Input, ScrollArea, Avatar, AvatarFallback, AvatarImage } from "@object-ui/components"
|
|
12
|
-
import { Send, Trash2, Paperclip, X, Square, RefreshCw, AlertCircle } from "lucide-react"
|
|
13
|
-
import ReactMarkdown from "react-markdown"
|
|
14
|
-
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
|
|
15
|
-
import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism"
|
|
16
|
-
import remarkGfm from "remark-gfm"
|
|
17
|
-
|
|
18
|
-
export interface ChatMessage {
|
|
19
|
-
id: string
|
|
20
|
-
role: "user" | "assistant" | "system"
|
|
21
|
-
content: string
|
|
22
|
-
timestamp?: string
|
|
23
|
-
avatar?: string
|
|
24
|
-
avatarFallback?: string
|
|
25
|
-
streaming?: boolean
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface ChatbotEnhancedProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
29
|
-
messages?: ChatMessage[]
|
|
30
|
-
placeholder?: string
|
|
31
|
-
onSendMessage?: (message: string, files?: File[]) => void
|
|
32
|
-
onClear?: () => void
|
|
33
|
-
/** Stop the current streaming response (API mode only) */
|
|
34
|
-
onStop?: () => void
|
|
35
|
-
/** Reload / retry the last assistant message (API mode only) */
|
|
36
|
-
onReload?: () => void
|
|
37
|
-
disabled?: boolean
|
|
38
|
-
/** Whether the assistant is currently generating a response */
|
|
39
|
-
isLoading?: boolean
|
|
40
|
-
/** Current streaming/API error */
|
|
41
|
-
error?: Error
|
|
42
|
-
showTimestamp?: boolean
|
|
43
|
-
userAvatarUrl?: string
|
|
44
|
-
userAvatarFallback?: string
|
|
45
|
-
assistantAvatarUrl?: string
|
|
46
|
-
assistantAvatarFallback?: string
|
|
47
|
-
maxHeight?: string
|
|
48
|
-
enableMarkdown?: boolean
|
|
49
|
-
enableFileUpload?: boolean
|
|
50
|
-
acceptedFileTypes?: string
|
|
51
|
-
maxFileSize?: number
|
|
52
|
-
onStreamingUpdate?: (messageId: string, content: string) => void
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function MessageContent({ content, enableMarkdown }: { content: string; enableMarkdown?: boolean }) {
|
|
56
|
-
if (!enableMarkdown) {
|
|
57
|
-
return <div className="whitespace-pre-wrap">{content}</div>
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return (
|
|
61
|
-
<div className="prose prose-sm dark:prose-invert max-w-none">
|
|
62
|
-
<ReactMarkdown
|
|
63
|
-
remarkPlugins={[remarkGfm]}
|
|
64
|
-
components={{
|
|
65
|
-
code({ node, inline, className, children, ...props }: any) {
|
|
66
|
-
const match = /language-(\w+)/.exec(className || '')
|
|
67
|
-
return !inline && match ? (
|
|
68
|
-
<SyntaxHighlighter
|
|
69
|
-
style={oneDark}
|
|
70
|
-
language={match[1]}
|
|
71
|
-
PreTag="div"
|
|
72
|
-
className="rounded-md my-2"
|
|
73
|
-
{...props}
|
|
74
|
-
>
|
|
75
|
-
{String(children).replace(/\n$/, '')}
|
|
76
|
-
</SyntaxHighlighter>
|
|
77
|
-
) : (
|
|
78
|
-
<code className={cn("bg-muted px-1 py-0.5 rounded text-sm", className)} {...props}>
|
|
79
|
-
{children}
|
|
80
|
-
</code>
|
|
81
|
-
)
|
|
82
|
-
},
|
|
83
|
-
p: ({ children }) => <p className="mb-2 last:mb-0">{children}</p>,
|
|
84
|
-
ul: ({ children }) => <ul className="list-disc pl-4 mb-2">{children}</ul>,
|
|
85
|
-
ol: ({ children }) => <ol className="list-decimal pl-4 mb-2">{children}</ol>,
|
|
86
|
-
li: ({ children }) => <li className="mb-1">{children}</li>,
|
|
87
|
-
a: ({ href, children }) => (
|
|
88
|
-
<a href={href} className="text-primary hover:underline" target="_blank" rel="noopener noreferrer">
|
|
89
|
-
{children}
|
|
90
|
-
</a>
|
|
91
|
-
),
|
|
92
|
-
blockquote: ({ children }) => (
|
|
93
|
-
<blockquote className="border-l-4 border-primary pl-4 italic my-2 text-muted-foreground">
|
|
94
|
-
{children}
|
|
95
|
-
</blockquote>
|
|
96
|
-
),
|
|
97
|
-
table: ({ children }) => (
|
|
98
|
-
<div className="overflow-x-auto my-2">
|
|
99
|
-
<table className="min-w-full divide-y divide-border">{children}</table>
|
|
100
|
-
</div>
|
|
101
|
-
),
|
|
102
|
-
th: ({ children }) => (
|
|
103
|
-
<th className="px-3 py-2 text-left text-xs font-medium uppercase tracking-wider bg-muted">
|
|
104
|
-
{children}
|
|
105
|
-
</th>
|
|
106
|
-
),
|
|
107
|
-
td: ({ children }) => (
|
|
108
|
-
<td className="px-3 py-2 text-sm border-t border-border">{children}</td>
|
|
109
|
-
),
|
|
110
|
-
}}
|
|
111
|
-
>
|
|
112
|
-
{content}
|
|
113
|
-
</ReactMarkdown>
|
|
114
|
-
</div>
|
|
115
|
-
)
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const ChatbotEnhanced = React.forwardRef<HTMLDivElement, ChatbotEnhancedProps>(
|
|
119
|
-
(
|
|
120
|
-
{
|
|
121
|
-
className,
|
|
122
|
-
messages = [],
|
|
123
|
-
placeholder = "Type your message...",
|
|
124
|
-
onSendMessage,
|
|
125
|
-
onClear,
|
|
126
|
-
onStop,
|
|
127
|
-
onReload,
|
|
128
|
-
disabled = false,
|
|
129
|
-
isLoading = false,
|
|
130
|
-
error,
|
|
131
|
-
showTimestamp = false,
|
|
132
|
-
userAvatarUrl,
|
|
133
|
-
userAvatarFallback = "You",
|
|
134
|
-
assistantAvatarUrl,
|
|
135
|
-
assistantAvatarFallback = "AI",
|
|
136
|
-
maxHeight = "500px",
|
|
137
|
-
enableMarkdown = true,
|
|
138
|
-
enableFileUpload = false,
|
|
139
|
-
acceptedFileTypes = "image/*,.pdf,.doc,.docx,.txt",
|
|
140
|
-
maxFileSize = 10 * 1024 * 1024, // 10MB
|
|
141
|
-
...props
|
|
142
|
-
},
|
|
143
|
-
ref
|
|
144
|
-
) => {
|
|
145
|
-
const [inputValue, setInputValue] = React.useState("")
|
|
146
|
-
const [selectedFiles, setSelectedFiles] = React.useState<File[]>([])
|
|
147
|
-
const scrollRef = React.useRef<HTMLDivElement>(null)
|
|
148
|
-
const inputRef = React.useRef<HTMLInputElement>(null)
|
|
149
|
-
const fileInputRef = React.useRef<HTMLInputElement>(null)
|
|
150
|
-
|
|
151
|
-
// Auto-scroll to bottom when new messages arrive
|
|
152
|
-
React.useEffect(() => {
|
|
153
|
-
if (scrollRef.current) {
|
|
154
|
-
const scrollElement = scrollRef.current.querySelector('[data-radix-scroll-area-viewport]')
|
|
155
|
-
if (scrollElement) {
|
|
156
|
-
scrollElement.scrollTop = scrollElement.scrollHeight
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}, [messages])
|
|
160
|
-
|
|
161
|
-
const handleSend = () => {
|
|
162
|
-
if ((inputValue.trim() || selectedFiles.length > 0) && onSendMessage) {
|
|
163
|
-
onSendMessage(inputValue.trim(), selectedFiles)
|
|
164
|
-
setInputValue("")
|
|
165
|
-
setSelectedFiles([])
|
|
166
|
-
inputRef.current?.focus()
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const isInputDisabled = disabled || isLoading
|
|
171
|
-
const isSendDisabled = isInputDisabled || (!inputValue.trim() && selectedFiles.length === 0)
|
|
172
|
-
|
|
173
|
-
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
174
|
-
if (e.key === "Enter" && !e.shiftKey) {
|
|
175
|
-
e.preventDefault()
|
|
176
|
-
handleSend()
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
181
|
-
const files = Array.from(e.target.files || [])
|
|
182
|
-
|
|
183
|
-
// Validate file sizes and MIME types
|
|
184
|
-
const validFiles = files.filter(file => {
|
|
185
|
-
// Size validation
|
|
186
|
-
if (file.size > maxFileSize) {
|
|
187
|
-
console.warn(`File ${file.name} exceeds ${maxFileSize / 1024 / 1024}MB limit`)
|
|
188
|
-
return false
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Basic MIME type validation
|
|
192
|
-
const acceptedTypes = acceptedFileTypes.split(',').map(t => t.trim())
|
|
193
|
-
const matchesType = acceptedTypes.some(type => {
|
|
194
|
-
if (type.startsWith('.')) {
|
|
195
|
-
return file.name.toLowerCase().endsWith(type.toLowerCase())
|
|
196
|
-
}
|
|
197
|
-
if (type.endsWith('/*')) {
|
|
198
|
-
const category = type.split('/')[0]
|
|
199
|
-
return file.type.startsWith(category + '/')
|
|
200
|
-
}
|
|
201
|
-
return file.type === type
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
if (!matchesType) {
|
|
205
|
-
console.warn(`File ${file.name} type ${file.type} not accepted`)
|
|
206
|
-
return false
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
return true
|
|
210
|
-
})
|
|
211
|
-
|
|
212
|
-
if (validFiles.length < files.length) {
|
|
213
|
-
console.warn(`${files.length - validFiles.length} file(s) were rejected`)
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
setSelectedFiles(prev => [...prev, ...validFiles])
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const handleRemoveFile = (index: number) => {
|
|
220
|
-
setSelectedFiles(prev => prev.filter((_, i) => i !== index))
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const handleClear = () => {
|
|
224
|
-
if (onClear) {
|
|
225
|
-
onClear()
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
return (
|
|
230
|
-
<div
|
|
231
|
-
ref={ref}
|
|
232
|
-
className={cn(
|
|
233
|
-
"flex flex-col border rounded-lg bg-background overflow-hidden",
|
|
234
|
-
className
|
|
235
|
-
)}
|
|
236
|
-
style={{ maxHeight }}
|
|
237
|
-
{...props}
|
|
238
|
-
>
|
|
239
|
-
{/* Header with clear button */}
|
|
240
|
-
{onClear && messages.length > 0 && (
|
|
241
|
-
<div className="flex items-center justify-between px-4 py-2 border-b bg-muted/30">
|
|
242
|
-
<span className="text-sm text-muted-foreground">
|
|
243
|
-
{messages.length} message{messages.length !== 1 ? 's' : ''}
|
|
244
|
-
</span>
|
|
245
|
-
<Button
|
|
246
|
-
variant="ghost"
|
|
247
|
-
size="sm"
|
|
248
|
-
onClick={handleClear}
|
|
249
|
-
className="h-8 text-xs"
|
|
250
|
-
>
|
|
251
|
-
<Trash2 className="h-3 w-3 mr-1" />
|
|
252
|
-
Clear
|
|
253
|
-
</Button>
|
|
254
|
-
</div>
|
|
255
|
-
)}
|
|
256
|
-
|
|
257
|
-
{/* Messages area */}
|
|
258
|
-
<ScrollArea ref={scrollRef} className="flex-1 p-4">
|
|
259
|
-
<div className="space-y-4">
|
|
260
|
-
{messages.length === 0 ? (
|
|
261
|
-
<div className="flex items-center justify-center h-32 text-muted-foreground text-sm">
|
|
262
|
-
Start a conversation...
|
|
263
|
-
</div>
|
|
264
|
-
) : (
|
|
265
|
-
messages.map((message) => (
|
|
266
|
-
<div
|
|
267
|
-
key={message.id}
|
|
268
|
-
className={cn(
|
|
269
|
-
"flex gap-3",
|
|
270
|
-
message.role === "user" ? "justify-end" : "justify-start"
|
|
271
|
-
)}
|
|
272
|
-
>
|
|
273
|
-
{message.role !== "user" && (
|
|
274
|
-
<Avatar className="h-8 w-8 flex-shrink-0">
|
|
275
|
-
<AvatarImage src={message.avatar || assistantAvatarUrl} />
|
|
276
|
-
<AvatarFallback>{message.avatarFallback || assistantAvatarFallback}</AvatarFallback>
|
|
277
|
-
</Avatar>
|
|
278
|
-
)}
|
|
279
|
-
|
|
280
|
-
<div
|
|
281
|
-
className={cn(
|
|
282
|
-
"rounded-lg px-4 py-2 max-w-[80%]",
|
|
283
|
-
message.role === "user"
|
|
284
|
-
? "bg-primary text-primary-foreground"
|
|
285
|
-
: "bg-muted"
|
|
286
|
-
)}
|
|
287
|
-
>
|
|
288
|
-
<MessageContent content={message.content} enableMarkdown={enableMarkdown} />
|
|
289
|
-
{showTimestamp && message.timestamp && (
|
|
290
|
-
<div className="text-xs opacity-70 mt-1">
|
|
291
|
-
{message.timestamp}
|
|
292
|
-
</div>
|
|
293
|
-
)}
|
|
294
|
-
{message.streaming && (
|
|
295
|
-
<div className="flex gap-1 mt-2">
|
|
296
|
-
<div className="w-2 h-2 bg-current rounded-full animate-bounce" style={{ animationDelay: '0ms' }} />
|
|
297
|
-
<div className="w-2 h-2 bg-current rounded-full animate-bounce" style={{ animationDelay: '150ms' }} />
|
|
298
|
-
<div className="w-2 h-2 bg-current rounded-full animate-bounce" style={{ animationDelay: '300ms' }} />
|
|
299
|
-
</div>
|
|
300
|
-
)}
|
|
301
|
-
</div>
|
|
302
|
-
|
|
303
|
-
{message.role === "user" && (
|
|
304
|
-
<Avatar className="h-8 w-8 flex-shrink-0">
|
|
305
|
-
<AvatarImage src={message.avatar || userAvatarUrl} />
|
|
306
|
-
<AvatarFallback>{message.avatarFallback || userAvatarFallback}</AvatarFallback>
|
|
307
|
-
</Avatar>
|
|
308
|
-
)}
|
|
309
|
-
</div>
|
|
310
|
-
))
|
|
311
|
-
)}
|
|
312
|
-
</div>
|
|
313
|
-
</ScrollArea>
|
|
314
|
-
|
|
315
|
-
{/* Selected files preview */}
|
|
316
|
-
{selectedFiles.length > 0 && (
|
|
317
|
-
<div className="px-4 py-2 border-t bg-muted/30">
|
|
318
|
-
<div className="flex flex-wrap gap-2">
|
|
319
|
-
{selectedFiles.map((file, index) => (
|
|
320
|
-
<div
|
|
321
|
-
key={index}
|
|
322
|
-
className="flex items-center gap-2 bg-background border rounded px-2 py-1 text-xs"
|
|
323
|
-
>
|
|
324
|
-
<Paperclip className="h-3 w-3" />
|
|
325
|
-
<span className="max-w-[150px] truncate">{file.name}</span>
|
|
326
|
-
<Button
|
|
327
|
-
variant="ghost"
|
|
328
|
-
size="sm"
|
|
329
|
-
className="h-4 w-4 p-0"
|
|
330
|
-
onClick={() => handleRemoveFile(index)}
|
|
331
|
-
>
|
|
332
|
-
<X className="h-3 w-3" />
|
|
333
|
-
</Button>
|
|
334
|
-
</div>
|
|
335
|
-
))}
|
|
336
|
-
</div>
|
|
337
|
-
</div>
|
|
338
|
-
)}
|
|
339
|
-
|
|
340
|
-
{/* Error display */}
|
|
341
|
-
{error && (
|
|
342
|
-
<div className="flex items-center gap-2 px-4 py-2 border-t bg-destructive/10 text-destructive text-sm" role="alert">
|
|
343
|
-
<AlertCircle className="h-4 w-4 flex-shrink-0" />
|
|
344
|
-
<span className="flex-1 truncate">{error.message || 'An error occurred'}</span>
|
|
345
|
-
{onReload && (
|
|
346
|
-
<Button
|
|
347
|
-
variant="ghost"
|
|
348
|
-
size="sm"
|
|
349
|
-
onClick={onReload}
|
|
350
|
-
className="h-7 text-xs"
|
|
351
|
-
aria-label="Retry"
|
|
352
|
-
>
|
|
353
|
-
<RefreshCw className="h-3 w-3 mr-1" />
|
|
354
|
-
Retry
|
|
355
|
-
</Button>
|
|
356
|
-
)}
|
|
357
|
-
</div>
|
|
358
|
-
)}
|
|
359
|
-
|
|
360
|
-
{/* Streaming controls */}
|
|
361
|
-
{isLoading && onStop && (
|
|
362
|
-
<div className="flex items-center justify-center px-4 py-2 border-t">
|
|
363
|
-
<Button
|
|
364
|
-
variant="outline"
|
|
365
|
-
size="sm"
|
|
366
|
-
onClick={onStop}
|
|
367
|
-
className="h-8 text-xs"
|
|
368
|
-
aria-label="Stop generating"
|
|
369
|
-
>
|
|
370
|
-
<Square className="h-3 w-3 mr-1" />
|
|
371
|
-
Stop generating
|
|
372
|
-
</Button>
|
|
373
|
-
</div>
|
|
374
|
-
)}
|
|
375
|
-
|
|
376
|
-
{/* Input area */}
|
|
377
|
-
<div className="flex items-center gap-2 p-4 border-t">
|
|
378
|
-
{enableFileUpload && (
|
|
379
|
-
<>
|
|
380
|
-
<input
|
|
381
|
-
ref={fileInputRef}
|
|
382
|
-
type="file"
|
|
383
|
-
multiple
|
|
384
|
-
accept={acceptedFileTypes}
|
|
385
|
-
onChange={handleFileSelect}
|
|
386
|
-
className="hidden"
|
|
387
|
-
/>
|
|
388
|
-
<Button
|
|
389
|
-
variant="ghost"
|
|
390
|
-
size="icon"
|
|
391
|
-
onClick={() => fileInputRef.current?.click()}
|
|
392
|
-
disabled={isInputDisabled}
|
|
393
|
-
aria-label="Attach file"
|
|
394
|
-
>
|
|
395
|
-
<Paperclip className="h-4 w-4" />
|
|
396
|
-
</Button>
|
|
397
|
-
</>
|
|
398
|
-
)}
|
|
399
|
-
|
|
400
|
-
<Input
|
|
401
|
-
ref={inputRef}
|
|
402
|
-
value={inputValue}
|
|
403
|
-
onChange={(e) => setInputValue(e.target.value)}
|
|
404
|
-
onKeyDown={handleKeyDown}
|
|
405
|
-
placeholder={placeholder}
|
|
406
|
-
disabled={isInputDisabled}
|
|
407
|
-
className="flex-1"
|
|
408
|
-
/>
|
|
409
|
-
|
|
410
|
-
<Button
|
|
411
|
-
onClick={handleSend}
|
|
412
|
-
disabled={isSendDisabled}
|
|
413
|
-
size="icon"
|
|
414
|
-
aria-label="Send message"
|
|
415
|
-
>
|
|
416
|
-
<Send className="h-4 w-4" />
|
|
417
|
-
</Button>
|
|
418
|
-
</div>
|
|
419
|
-
</div>
|
|
420
|
-
)
|
|
421
|
-
}
|
|
422
|
-
)
|
|
423
|
-
|
|
424
|
-
ChatbotEnhanced.displayName = "ChatbotEnhanced"
|
|
425
|
-
|
|
426
|
-
export { ChatbotEnhanced }
|
package/src/FloatingChatbot.tsx
DELETED
|
@@ -1,89 +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 * as ReactDOM from "react-dom"
|
|
11
|
-
import type { FloatingChatbotConfig } from "@object-ui/types"
|
|
12
|
-
import { FloatingChatbotProvider } from "./FloatingChatbotProvider"
|
|
13
|
-
import { FloatingChatbotTrigger } from "./FloatingChatbotTrigger"
|
|
14
|
-
import { FloatingChatbotPanel } from "./FloatingChatbotPanel"
|
|
15
|
-
import { ChatbotEnhanced, type ChatbotEnhancedProps } from "./ChatbotEnhanced"
|
|
16
|
-
|
|
17
|
-
export interface FloatingChatbotProps extends ChatbotEnhancedProps {
|
|
18
|
-
/** Floating configuration */
|
|
19
|
-
floatingConfig?: FloatingChatbotConfig
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Floating Chatbot — Airtable-style FAB widget.
|
|
24
|
-
*
|
|
25
|
-
* Wraps `ChatbotEnhanced` in a floating panel that can be toggled
|
|
26
|
-
* via a fixed FAB trigger button. Uses React portal to avoid
|
|
27
|
-
* DOM/z-index conflicts.
|
|
28
|
-
*/
|
|
29
|
-
export function FloatingChatbot({
|
|
30
|
-
floatingConfig,
|
|
31
|
-
...chatbotProps
|
|
32
|
-
}: FloatingChatbotProps) {
|
|
33
|
-
const {
|
|
34
|
-
position = "bottom-right",
|
|
35
|
-
defaultOpen = false,
|
|
36
|
-
panelWidth = 400,
|
|
37
|
-
panelHeight = 520,
|
|
38
|
-
title = "Chat",
|
|
39
|
-
triggerSize = 56,
|
|
40
|
-
} = floatingConfig ?? {}
|
|
41
|
-
|
|
42
|
-
const [portalContainer, setPortalContainer] = React.useState<HTMLElement | null>(null)
|
|
43
|
-
|
|
44
|
-
React.useEffect(() => {
|
|
45
|
-
// Create a portal root so the floating UI sits outside the normal DOM tree
|
|
46
|
-
let container = document.getElementById("floating-chatbot-portal")
|
|
47
|
-
if (!container) {
|
|
48
|
-
container = document.createElement("div")
|
|
49
|
-
container.id = "floating-chatbot-portal"
|
|
50
|
-
document.body.appendChild(container)
|
|
51
|
-
}
|
|
52
|
-
setPortalContainer(container)
|
|
53
|
-
|
|
54
|
-
return () => {
|
|
55
|
-
// Only remove if we created it and it's still in the DOM
|
|
56
|
-
if (container && container.parentNode && !container.hasChildNodes()) {
|
|
57
|
-
container.parentNode.removeChild(container)
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}, [])
|
|
61
|
-
|
|
62
|
-
const content = (
|
|
63
|
-
<FloatingChatbotProvider defaultOpen={defaultOpen}>
|
|
64
|
-
<FloatingChatbotTrigger
|
|
65
|
-
position={position}
|
|
66
|
-
size={triggerSize}
|
|
67
|
-
/>
|
|
68
|
-
<FloatingChatbotPanel
|
|
69
|
-
title={title}
|
|
70
|
-
position={position}
|
|
71
|
-
width={panelWidth}
|
|
72
|
-
height={panelHeight}
|
|
73
|
-
>
|
|
74
|
-
<ChatbotEnhanced
|
|
75
|
-
{...chatbotProps}
|
|
76
|
-
maxHeight="100%"
|
|
77
|
-
className="h-full border-0 rounded-none"
|
|
78
|
-
/>
|
|
79
|
-
</FloatingChatbotPanel>
|
|
80
|
-
</FloatingChatbotProvider>
|
|
81
|
-
)
|
|
82
|
-
|
|
83
|
-
// Use portal rendering when in browser, fallback to inline for SSR / tests
|
|
84
|
-
if (portalContainer) {
|
|
85
|
-
return ReactDOM.createPortal(content, portalContainer)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return content
|
|
89
|
-
}
|