11/**
22 * ConsoleFloatingChatbot
33 *
4- * Extracted from ConsoleLayout so it can be code-split via React.lazy.
4+ * Wires the global FAB chatbot to the framework's `@objectstack/service-ai`
5+ * backend (the `/api/v1/ai/agents/:agentName/chat` Vercel Data Stream
6+ * endpoint) and exposes an in-header agent picker.
7+ *
58 * The chatbot pulls in `react-markdown` + `micromark` (~150 KB) which is
69 * unused on every page until the AI assistant is enabled, so deferring it
710 * keeps those bytes off the initial paint.
811 * @module
912 */
1013import React from 'react' ;
11- import { FloatingChatbot , useObjectChat , type ChatMessage } from '@object-ui/plugin-chatbot' ;
14+ import {
15+ FloatingChatbot ,
16+ useObjectChat ,
17+ useAgents ,
18+ type ChatMessage ,
19+ type AgentDescriptor ,
20+ } from '@object-ui/plugin-chatbot' ;
21+ import {
22+ Select ,
23+ SelectContent ,
24+ SelectItem ,
25+ SelectTrigger ,
26+ SelectValue ,
27+ } from '@object-ui/components' ;
1228
1329interface ConsoleObject {
1430 name : string ;
@@ -18,11 +34,65 @@ interface ConsoleObject {
1834export interface ConsoleFloatingChatbotProps {
1935 appLabel : string ;
2036 objects : ConsoleObject [ ] ;
37+ /**
38+ * Base URL of the AI service. Defaults to `${VITE_SERVER_URL}/api/v1/ai`
39+ * (or the relative `/api/v1/ai` when no server URL is configured).
40+ */
41+ apiBase ?: string ;
42+ /** Default agent name to select on first render. */
43+ defaultAgent ?: string ;
44+ }
45+
46+ const DEFAULT_AI_PATH = '/api/v1/ai' ;
47+
48+ function resolveApiBase ( explicit ?: string ) : string {
49+ if ( explicit ) return explicit . replace ( / \/ $ / , '' ) ;
50+ const env = ( import . meta as any ) . env ?? { } ;
51+ const fromEnv = env . VITE_AI_BASE_URL as string | undefined ;
52+ if ( fromEnv ) return fromEnv . replace ( / \/ $ / , '' ) ;
53+ const serverUrl = ( env . VITE_SERVER_URL as string | undefined ) ?? '' ;
54+ return `${ serverUrl . replace ( / \/ $ / , '' ) } ${ DEFAULT_AI_PATH } ` ;
55+ }
56+
57+ /**
58+ * Inner component that owns the chat hook. Re-mounted (via `key`) whenever
59+ * the active agent changes so `useObjectChat`'s first-render mode lock
60+ * (api vs local) picks up the new endpoint.
61+ */
62+ interface ChatbotInnerProps {
63+ appLabel : string ;
64+ objects : ConsoleObject [ ] ;
65+ agents : AgentDescriptor [ ] ;
66+ agentsLoading : boolean ;
67+ agentsError : Error | undefined ;
68+ activeAgent : string | undefined ;
69+ onAgentChange : ( name : string ) => void ;
70+ chatApi : string | undefined ;
2171}
2272
23- export default function ConsoleFloatingChatbot ( { appLabel, objects } : ConsoleFloatingChatbotProps ) {
73+ function ChatbotInner ( {
74+ appLabel,
75+ objects,
76+ agents,
77+ agentsLoading,
78+ agentsError,
79+ activeAgent,
80+ onAgentChange,
81+ chatApi,
82+ } : ChatbotInnerProps ) {
2483 const objectNames = objects . map ( ( o ) => o . label || o . name ) . join ( ', ' ) ;
2584
85+ const activeAgentLabel = React . useMemo < string > ( ( ) => {
86+ const found = agents . find ( ( a ) => a . name === activeAgent ) ;
87+ return found ?. label ?? activeAgent ?? appLabel ;
88+ } , [ agents , activeAgent , appLabel ] ) ;
89+
90+ const welcomeContent = activeAgent
91+ ? `Hello! I'm **${ activeAgentLabel } **, your ${ appLabel } assistant. How can I help you today?`
92+ : `Hello! I'm your **${ appLabel } ** assistant. ${
93+ agentsError ? '(Backend unreachable — running in offline demo mode.)' : ''
94+ } `;
95+
2696 const {
2797 messages,
2898 isLoading,
@@ -32,32 +102,78 @@ export default function ConsoleFloatingChatbot({ appLabel, objects }: ConsoleFlo
32102 reload,
33103 clear,
34104 } = useObjectChat ( {
105+ api : chatApi ,
106+ conversationId : activeAgent ? `${ appLabel } :${ activeAgent } ` : undefined ,
107+ body : {
108+ context : {
109+ activeApp : appLabel ,
110+ objects : objects . map ( ( o ) => ( { name : o . name , label : o . label } ) ) ,
111+ agentName : activeAgent ,
112+ } ,
113+ } ,
35114 initialMessages : [
36115 {
37116 id : 'welcome' ,
38117 role : 'assistant' as const ,
39- content : `Hello! I'm your ** ${ appLabel } ** assistant. How can I help you today?` ,
118+ content : welcomeContent ,
40119 } ,
41120 ] ,
42- autoResponse : true ,
121+ // Local-mode fallback: only used when `chatApi` is undefined (no agent
122+ // resolved yet, or no backend available). Keeps the UI usable.
123+ autoResponse : ! chatApi ,
43124 autoResponseText : objectNames
44125 ? `I can help you work with ${ objectNames } . What would you like to do?`
45- : ' Thanks for your message! I\ 'm here to help you navigate and manage your data.' ,
46- autoResponseDelay : 800 ,
126+ : " Thanks for your message! I'm here to help you navigate and manage your data." ,
127+ autoResponseDelay : 600 ,
47128 } ) ;
48129
130+ const headerExtra =
131+ agents . length > 0 ? (
132+ < Select
133+ value = { activeAgent }
134+ onValueChange = { onAgentChange }
135+ disabled = { agentsLoading }
136+ >
137+ < SelectTrigger
138+ className = "h-7 w-[180px] text-xs"
139+ data-testid = "floating-chatbot-agent-picker"
140+ >
141+ < SelectValue placeholder = "Choose agent..." />
142+ </ SelectTrigger >
143+ < SelectContent align = "end" >
144+ { agents . map ( ( agent : AgentDescriptor ) => (
145+ < SelectItem key = { agent . name } value = { agent . name } className = "text-xs" >
146+ < span className = "font-medium" > { agent . label } </ span >
147+ { agent . description ? (
148+ < span className = "block text-muted-foreground text-[10px] truncate max-w-[220px]" >
149+ { agent . description }
150+ </ span >
151+ ) : null }
152+ </ SelectItem >
153+ ) ) }
154+ </ SelectContent >
155+ </ Select >
156+ ) : null ;
157+
49158 return (
50159 < FloatingChatbot
51160 floatingConfig = { {
52161 position : 'bottom-right' ,
53162 defaultOpen : false ,
54- panelWidth : 400 ,
55- panelHeight : 520 ,
163+ panelWidth : 420 ,
164+ panelHeight : 560 ,
56165 title : `${ appLabel } Assistant` ,
57166 triggerSize : 56 ,
58167 } }
168+ headerExtra = { headerExtra }
59169 messages = { messages as ChatMessage [ ] }
60- placeholder = "Ask anything..."
170+ placeholder = {
171+ activeAgent
172+ ? `Ask ${ activeAgentLabel } ...`
173+ : agentsLoading
174+ ? 'Loading agents...'
175+ : 'Ask anything...'
176+ }
61177 onSendMessage = { ( content : string ) => sendMessage ( content ) }
62178 onClear = { clear }
63179 onStop = { isLoading ? stop : undefined }
@@ -68,3 +184,47 @@ export default function ConsoleFloatingChatbot({ appLabel, objects }: ConsoleFlo
68184 />
69185 ) ;
70186}
187+
188+ export default function ConsoleFloatingChatbot ( {
189+ appLabel,
190+ objects,
191+ apiBase : apiBaseProp ,
192+ defaultAgent : defaultAgentProp ,
193+ } : ConsoleFloatingChatbotProps ) {
194+ const apiBase = React . useMemo ( ( ) => resolveApiBase ( apiBaseProp ) , [ apiBaseProp ] ) ;
195+ const env = ( import . meta as any ) . env ?? { } ;
196+ const envDefaultAgent = env . VITE_AI_DEFAULT_AGENT as string | undefined ;
197+
198+ const { agents, isLoading : agentsLoading , error : agentsError } = useAgents ( { apiBase } ) ;
199+
200+ const [ activeAgent , setActiveAgent ] = React . useState < string | undefined > ( undefined ) ;
201+ React . useEffect ( ( ) => {
202+ if ( ! activeAgent && agents . length > 0 ) {
203+ const preferred = defaultAgentProp ?? envDefaultAgent ;
204+ const match = preferred && agents . find ( ( a ) => a . name === preferred ) ;
205+ setActiveAgent ( ( match ?? agents [ 0 ] ) . name ) ;
206+ }
207+ } , [ agents , activeAgent , defaultAgentProp , envDefaultAgent ] ) ;
208+
209+ const chatApi = activeAgent
210+ ? `${ apiBase } /agents/${ encodeURIComponent ( activeAgent ) } /chat`
211+ : undefined ;
212+
213+ // `key` forces a clean remount whenever the active agent (and therefore the
214+ // chat API URL) changes — required because `useObjectChat` locks its mode
215+ // (api vs local) on first render.
216+ return (
217+ < ChatbotInner
218+ key = { chatApi ?? 'local' }
219+ appLabel = { appLabel }
220+ objects = { objects }
221+ agents = { agents }
222+ agentsLoading = { agentsLoading }
223+ agentsError = { agentsError }
224+ activeAgent = { activeAgent }
225+ onAgentChange = { setActiveAgent }
226+ chatApi = { chatApi }
227+ />
228+ ) ;
229+ }
230+
0 commit comments