Skip to content

Commit b4fd455

Browse files
feat(plugin-chatbot): add floating chatbot FAB widget with provider, trigger, panel, and portal rendering
Agent-Logs-Url: https://github.com/objectstack-ai/objectui/sessions/923bf2a3-3b00-4623-bcf5-7b90c8feaa6b Co-authored-by: xuyushun441-sys <255036401+xuyushun441-sys@users.noreply.github.com>
1 parent 332a0a8 commit b4fd455

File tree

12 files changed

+832
-1
lines changed

12 files changed

+832
-1
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- **Floating Chatbot FAB Widget** (`@object-ui/plugin-chatbot`): Airtable-style floating action button (FAB) that opens a chatbot panel overlay. New components: `FloatingChatbot`, `FloatingChatbotPanel`, `FloatingChatbotTrigger`, `FloatingChatbotProvider`. Features include configurable position (bottom-right/left), fullscreen toggle, close, portal rendering to avoid z-index conflicts, and `useFloatingChatbot` hook for programmatic control. Registered as `chatbot-floating` in ComponentRegistry. 21 new unit tests.
13+
14+
- **New ChatbotSchema floating fields** (`@object-ui/types`): Extended `ChatbotSchema` with `displayMode` (`'inline' | 'floating'`) and `floatingConfig` (`FloatingChatbotConfig`) for schema-driven floating chatbot configuration. New `FloatingChatbotConfig` interface with `position`, `defaultOpen`, `panelWidth`, `panelHeight`, `title`, `triggerIcon`, and `triggerSize` options.
15+
1216
- **AI SDUI Chatbot integration** (`@object-ui/plugin-chatbot`): Refactored chatbot plugin to support full AI streaming via `service-ai` backend and `vercel/ai` SDK (`@ai-sdk/react`). New `useObjectChat` composable hook wraps `@ai-sdk/react`'s `useChat` for SSE streaming, tool-calling, and production-grade chat. Auto-detects API mode (when `api` schema field is set) vs legacy local auto-response mode. ChatbotEnhanced component now supports stop, reload, error display, and streaming state indicators. 44 unit tests (19 new hook tests, 10 new streaming tests).
1317

1418
- **New ChatbotSchema fields** (`@object-ui/types`): Extended `ChatbotSchema` with `api`, `conversationId`, `systemPrompt`, `model`, `streamingEnabled`, `headers`, `requestBody`, `maxToolRoundtrips`, and `onError` fields for service-ai integration. Extended `ChatMessage` with `streaming`, `toolInvocations` fields and added `ChatToolInvocation` interface for tool-calling flows.
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
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+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
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+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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+
}

0 commit comments

Comments
 (0)