@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/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@object-ui/plugin-chatbot",
3
- "version": "3.3.0",
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.160",
28
- "ai": "^6.0.158",
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.0",
34
- "@object-ui/core": "3.3.0",
35
- "@object-ui/react": "3.3.0",
36
- "@object-ui/types": "3.3.0"
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.2",
48
- "vite": "^8.0.8",
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",
@@ -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
- vite v8.0.8 building client environment for production...
6
- 
7
- rendering chunks...
8
- 
9
- [vite:dts] Start generate declaration files...
10
- src/useObjectChat.ts:178:5 - error TS2353: Object literal may only specify known properties, and 'api' does not exist in type 'UseChatOptions<UIMessage<unknown, UIDataTypes, UITools>>'.
11
-
12
- 178 api: isApiMode ? api! : '/api/noop',
13
-    ~~~
14
- src/useObjectChat.ts:216:7 - error TS2339: Property 'isLoading' does not exist on type 'UseChatHelpers<UIMessage<unknown, UIDataTypes, UITools>>'.
15
-
16
- 216 isLoading,
17
-    ~~~~~~~~~
18
- src/useObjectChat.ts:218:7 - error TS2339: Property 'input' does not exist on type 'UseChatHelpers<UIMessage<unknown, UIDataTypes, UITools>>'.
19
-
20
- 218 input,
21
-    ~~~~~
22
- src/useObjectChat.ts:219:7 - error TS2339: Property 'setInput' does not exist on type 'UseChatHelpers<UIMessage<unknown, UIDataTypes, UITools>>'.
23
-
24
- 219 setInput,
25
-    ~~~~~~~~
26
- src/useObjectChat.ts:220:7 - error TS2339: Property 'handleInputChange' does not exist on type 'UseChatHelpers<UIMessage<unknown, UIDataTypes, UITools>>'.
27
-
28
- 220 handleInputChange,
29
-    ~~~~~~~~~~~~~~~~~
30
- src/useObjectChat.ts:221:7 - error TS2339: Property 'append' does not exist on type 'UseChatHelpers<UIMessage<unknown, UIDataTypes, UITools>>'.
31
-
32
- 221 append,
33
-    ~~~~~~
34
- src/useObjectChat.ts:223:7 - error TS2339: Property 'reload' does not exist on type 'UseChatHelpers<UIMessage<unknown, UIDataTypes, UITools>>'.
35
-
36
- 223 reload,
37
-    ~~~~~~
38
-
39
- [vite:dts] Declaration files built in 30288ms.
40
- 
41
- computing gzip size...
42
- dist/index.js 1,222.85 kB │ gzip: 352.84 kB
43
-
44
- [PLUGIN_TIMINGS] Warning: Your build spent significant time in plugin `vite:dts`. See https://rolldown.rs/options/checks#plugintimings for more details.
45
- 
46
- 
47
- rendering chunks...
48
- computing gzip size...
49
- dist/index.umd.cjs 1,006.91 kB │ gzip: 327.98 kB
50
-
51
- [MISSING_GLOBAL_NAME] Warning: No name was provided for external module "react/jsx-runtime" in "output.globals" – guessing "react_jsx_runtime".
52
- 
53
- ✓ built in 37.23s
@@ -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 }
@@ -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
- }