@devicai/ui
v0.10.4
Published
React component library for Devic AI assistants
Maintainers
Readme
@devicai/ui
React component library for integrating Devic AI assistants into your application.
Features
- ChatDrawer - A ready-to-use chat drawer component
- AICommandBar - A spotlight-style command bar for quick AI interactions
- AIGenerationButton - A button for triggering AI generation with modal, tooltip, or direct modes
- useDevicChat - Hook for building custom chat UIs
- Model Interface Protocol - Support for client-side tool execution
- Message Feedback - Built-in thumbs up/down feedback with comments
- CSS Variables - Easy theming with CSS custom properties
- TypeScript - Full type definitions included
- React 17+ - Compatible with React 17 and above
- Minimal Dependencies - Only React as a peer dependency
Installation
npm install @devicai/ui
# or
yarn add @devicai/ui
# or
pnpm add @devicai/uiQuick Start
Using ChatDrawer (Simplest)
import { DevicProvider, ChatDrawer } from '@devicai/ui';
import '@devicai/ui/dist/esm/styles.css';
function App() {
return (
<DevicProvider apiKey="your-api-key">
<ChatDrawer
assistantId="my-assistant"
options={{
position: 'right',
welcomeMessage: 'Hello! How can I help you?',
suggestedMessages: ['Help me with...', 'Tell me about...'],
}}
/>
</DevicProvider>
);
}Using the Hook (Custom UI)
import { DevicProvider, useDevicChat } from '@devicai/ui';
function CustomChat() {
const { messages, isLoading, sendMessage } = useDevicChat({
assistantId: 'my-assistant',
});
return (
<div>
{messages.map((msg) => (
<div key={msg.uid}>
<strong>{msg.role}:</strong> {msg.content.message}
</div>
))}
{isLoading && <div>Thinking...</div>}
<button onClick={() => sendMessage('Hello!')}>Send</button>
</div>
);
}
function App() {
return (
<DevicProvider apiKey="your-api-key">
<CustomChat />
</DevicProvider>
);
}Components
DevicProvider
Context provider for global configuration.
<DevicProvider
apiKey="devic-xxx" // Required
baseUrl="https://api.devic.ai"
tenantId="tenant-123" // Optional global tenant
tenantMetadata={{ ... }} // Optional global metadata
>
<App />
</DevicProvider>ChatDrawer
A complete chat drawer component.
<ChatDrawer
assistantId="my-assistant"
chatUid="optional-existing-chat"
options={{
position: 'right', // 'left' | 'right'
width: 400,
defaultOpen: false,
color: '#1890ff', // Primary color
welcomeMessage: 'Hello!',
suggestedMessages: ['Help me...'],
enableFileUploads: true,
allowedFileTypes: { images: true, documents: true },
inputPlaceholder: 'Type a message...',
title: 'Chat Assistant',
showToolTimeline: true,
}}
enabledTools={['tool1', 'tool2']}
modelInterfaceTools={[
{
toolName: 'get_user_location',
schema: {
type: 'function',
function: {
name: 'get_user_location',
description: 'Get user current location',
parameters: { type: 'object', properties: {} }
}
},
callback: async () => {
const pos = await getCurrentPosition();
return { lat: pos.coords.latitude, lng: pos.coords.longitude };
}
}
]}
tenantId="specific-tenant" // Override provider
tenantMetadata={{ userId: '123' }}
apiKey="override-key" // Override provider
// Callbacks
onMessageSent={(message) => {}}
onMessageReceived={(message) => {}}
onToolCall={(toolName, params) => {}}
onError={(error) => {}}
onChatCreated={(chatUid) => {}}
onOpen={() => {}}
onClose={() => {}}
// Controlled mode
isOpen={true}
/>AICommandBar
A floating command bar (similar to Spotlight/Command Palette) for quick AI interactions.
import { AICommandBar } from '@devicai/ui';
<AICommandBar
assistantId="my-assistant"
options={{
shortcut: 'cmd+k', // Keyboard shortcut to open
placeholder: 'Ask AI...',
position: 'fixed', // 'inline' | 'fixed'
fixedPlacement: { bottom: 20, right: 20 },
showResultCard: true, // Show response in a card
showShortcutHint: true, // Show shortcut badge
// Commands (slash commands)
commands: [
{
keyword: 'summarize',
description: 'Summarize content',
message: 'Please summarize this page.',
icon: <SummarizeIcon />,
},
],
// History
enableHistory: true,
maxHistoryItems: 50,
// Theming
backgroundColor: '#ffffff',
textColor: '#1f2937',
borderColor: '#e5e7eb',
borderRadius: 12,
}}
// Callbacks
onResponse={({ message, toolCalls, chatUid }) => {}}
onSubmit={(message) => {}}
onToolCall={(toolName, params) => {}}
onError={(error) => {}}
onOpen={() => {}}
onClose={() => {}}
// Integration with ChatDrawer
onExecute="openDrawer" // 'callback' | 'openDrawer'
chatDrawerRef={drawerRef} // Required when onExecute="openDrawer"
// Controlled mode
isVisible={true}
onVisibilityChange={(visible) => {}}
/>AICommandBar with ChatDrawer Integration
import { useRef } from 'react';
import { AICommandBar, ChatDrawer, ChatDrawerHandle } from '@devicai/ui';
function App() {
const drawerRef = useRef<ChatDrawerHandle>(null);
return (
<>
<AICommandBar
assistantId="my-assistant"
onExecute="openDrawer"
chatDrawerRef={drawerRef}
options={{
shortcut: 'cmd+k',
showResultCard: false,
}}
/>
<ChatDrawer ref={drawerRef} assistantId="my-assistant" />
</>
);
}AICommandBar Handle (ref methods)
const commandBarRef = useRef<AICommandBarHandle>(null);
// Methods available via ref
commandBarRef.current?.open();
commandBarRef.current?.close();
commandBarRef.current?.toggle();
commandBarRef.current?.focus();
commandBarRef.current?.submit('Hello!');
commandBarRef.current?.reset();AIGenerationButton
A button component for triggering AI generation with three interaction modes: direct, modal, or tooltip.
import { AIGenerationButton } from '@devicai/ui';
// Modal mode (default) - opens a modal for user input
<AIGenerationButton
assistantId="my-assistant"
options={{
mode: 'modal',
modalTitle: 'Generate with AI',
modalDescription: 'Describe what you want to generate.',
placeholder: 'E.g., Create a product description...',
confirmText: 'Generate',
cancelText: 'Cancel',
}}
onResponse={({ message, toolCalls }) => {
console.log('Generated:', message.content.message);
}}
/>
// Direct mode - sends predefined prompt immediately
<AIGenerationButton
assistantId="my-assistant"
options={{
mode: 'direct',
prompt: 'Generate a summary of this content',
label: 'Summarize',
loadingLabel: 'Summarizing...',
}}
onResponse={({ message }) => setSummary(message.content.message)}
/>
// Tooltip mode - shows inline input
<AIGenerationButton
assistantId="my-assistant"
options={{
mode: 'tooltip',
tooltipPlacement: 'bottom', // 'top' | 'bottom' | 'left' | 'right'
tooltipWidth: 350,
}}
onResponse={handleGeneration}
/>AIGenerationButton Options
<AIGenerationButton
assistantId="my-assistant"
options={{
// Mode
mode: 'modal', // 'direct' | 'modal' | 'tooltip'
prompt: 'Predefined prompt', // Required for direct mode
// Labels
label: 'Generate with AI',
loadingLabel: 'Generating...',
placeholder: 'Describe what you want...',
modalTitle: 'Generate with AI',
modalDescription: 'Optional description',
confirmText: 'Generate',
cancelText: 'Cancel',
// Button styling
variant: 'primary', // 'primary' | 'secondary' | 'outline' | 'ghost'
size: 'medium', // 'small' | 'medium' | 'large'
icon: <CustomIcon />, // Custom icon
hideIcon: false,
hideLabel: false, // Icon-only button
// Tooltip options
tooltipPlacement: 'top',
tooltipWidth: 300,
// Tool call display
toolRenderers: {
search_docs: (input, output) => (
<div>Found {output.count} results</div>
),
},
toolIcons: {
search_docs: <SearchIcon />,
},
processingMessage: 'Processing...',
// Theming
color: '#3b82f6',
backgroundColor: '#ffffff',
textColor: '#1f2937',
borderColor: '#e5e7eb',
borderRadius: 8,
zIndex: 10000,
}}
// Callbacks
onResponse={({ message, toolCalls, chatUid }) => {}}
onBeforeSend={(prompt) => modifiedPrompt} // Modify prompt before sending
onError={(error) => {}}
onStart={() => {}}
onOpen={() => {}}
onClose={() => {}}
// Other props
modelInterfaceTools={[...]}
tenantId="tenant-123"
tenantMetadata={{ userId: '456' }}
disabled={false}
/>AIGenerationButton Handle (ref methods)
const buttonRef = useRef<AIGenerationButtonHandle>(null);
// Trigger generation programmatically
const result = await buttonRef.current?.generate('Custom prompt');
// Open/close modal or tooltip
buttonRef.current?.open();
buttonRef.current?.close();
buttonRef.current?.reset();
// Check processing state
if (buttonRef.current?.isProcessing) { ... }Hooks
useDevicChat
Main hook for chat functionality.
const {
messages, // ChatMessage[]
chatUid, // string | null
isLoading, // boolean
status, // 'idle' | 'processing' | 'completed' | 'error'
error, // Error | null
sendMessage, // (message: string, options?: { files?: ChatFile[] }) => Promise<void>
clearChat, // () => void
loadChat, // (chatUid: string) => Promise<void>
} = useDevicChat({
assistantId: 'my-assistant',
chatUid: 'optional-existing-chat',
apiKey: 'override-key',
baseUrl: 'https://api.devic.ai',
tenantId: 'tenant-123',
tenantMetadata: { userId: '456' },
enabledTools: ['tool1', 'tool2'],
modelInterfaceTools: [...],
pollingInterval: 1000,
onMessageSent: (message) => {},
onMessageReceived: (message) => {},
onToolCall: (toolName, params) => {},
onError: (error) => {},
onChatCreated: (chatUid) => {},
});useAICommandBar
Hook for building custom command bar UIs.
import { useAICommandBar } from '@devicai/ui';
const {
isVisible, // boolean
open, // () => void
close, // () => void
toggle, // () => void
inputValue, // string
setInputValue, // (value: string) => void
inputRef, // RefObject<HTMLInputElement>
focus, // () => void
isProcessing, // boolean
currentToolSummary, // string | null
toolCalls, // ToolCallSummary[]
result, // CommandBarResult | null
error, // Error | null
history, // string[]
showingHistory, // boolean
showingCommands, // boolean
filteredCommands, // AICommandBarCommand[]
submit, // (message?: string) => Promise<void>
reset, // () => void
handleKeyDown, // (e: KeyboardEvent) => void
} = useAICommandBar({
assistantId: 'my-assistant',
options: { shortcut: 'cmd+k' },
onResponse: (result) => {},
onError: (error) => {},
});useAIGenerationButton
Hook for building custom generation button UIs.
import { useAIGenerationButton } from '@devicai/ui';
const {
isOpen, // boolean - modal/tooltip open state
isProcessing, // boolean
inputValue, // string
setInputValue, // (value: string) => void
error, // Error | null
result, // GenerationResult | null
toolCalls, // ToolCallSummary[]
currentToolSummary, // string | null
inputRef, // RefObject<HTMLTextAreaElement>
open, // () => void
close, // () => void
generate, // (prompt?: string) => Promise<GenerationResult | null>
reset, // () => void
handleKeyDown, // (e: KeyboardEvent) => void
} = useAIGenerationButton({
assistantId: 'my-assistant',
options: { mode: 'modal' },
onResponse: (result) => {},
onBeforeSend: (prompt) => prompt,
onError: (error) => {},
onStart: () => {},
});useModelInterface
Hook for implementing the Model Interface Protocol.
const {
toolSchemas, // Tool schemas to send to API
isClientTool, // (name: string) => boolean
handleToolCalls, // (toolCalls: ToolCall[]) => Promise<ToolCallResponse[]>
extractPendingToolCalls, // (messages: ChatMessage[]) => ToolCall[]
} = useModelInterface({
tools: [
{
toolName: 'get_user_location',
schema: {
type: 'function',
function: {
name: 'get_user_location',
description: 'Get user location',
parameters: { type: 'object', properties: {} }
}
},
callback: async () => ({ lat: 40.7, lng: -74.0 })
}
],
onToolExecute: (toolName, params) => {},
onToolComplete: (toolName, result) => {},
onToolError: (toolName, error) => {},
});usePolling
Hook for polling real-time chat history.
const {
data, // RealtimeChatHistory | null
isPolling, // boolean
error, // Error | null
start, // () => void
stop, // () => void
refetch, // () => Promise<void>
} = usePolling(
chatUid,
async () => client.getRealtimeHistory(assistantId, chatUid),
{
interval: 1000,
enabled: true,
stopStatuses: ['completed', 'error'],
onUpdate: (data) => {},
onStop: (data) => {},
onError: (error) => {},
}
);API Client
Use the API client directly for advanced use cases.
import { DevicApiClient } from '@devicai/ui';
const client = new DevicApiClient({
apiKey: 'your-api-key',
baseUrl: 'https://api.devic.ai',
});
// Get assistants
const assistants = await client.getAssistants();
// Send message (async mode)
const { chatUid } = await client.sendMessageAsync('my-assistant', {
message: 'Hello!',
tenantId: 'tenant-123',
});
// Poll for results
const result = await client.getRealtimeHistory('my-assistant', chatUid);
// Send tool responses
await client.sendToolResponses('my-assistant', chatUid, [
{ tool_call_id: 'call_123', content: { result: 'data' }, role: 'tool' }
]);Theming
Customize appearance with CSS variables:
.devic-chat-drawer {
--devic-primary: #1890ff;
--devic-primary-hover: #40a9ff;
--devic-primary-light: #e6f7ff;
--devic-bg: #ffffff;
--devic-bg-secondary: #f5f5f5;
--devic-text: #333333;
--devic-text-secondary: #666666;
--devic-text-muted: #999999;
--devic-border: #e8e8e8;
--devic-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
--devic-radius: 8px;
--devic-radius-sm: 4px;
--devic-radius-lg: 16px;
}Or use the color option in ChatDrawer:
<ChatDrawer
options={{ color: '#ff4081' }}
/>Model Interface Protocol
The Model Interface Protocol allows you to define client-side tools that the assistant can call during a conversation.
const locationTool: ModelInterfaceTool = {
toolName: 'get_user_location',
schema: {
type: 'function',
function: {
name: 'get_user_location',
description: 'Get the user current geographic location',
parameters: {
type: 'object',
properties: {},
}
}
},
callback: async () => {
return new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(
(pos) => resolve({
latitude: pos.coords.latitude,
longitude: pos.coords.longitude,
}),
(err) => reject(err)
);
});
}
};
<ChatDrawer
assistantId="my-assistant"
modelInterfaceTools={[locationTool]}
/>TypeScript
All types are exported:
import type {
// Chat types
ChatMessage,
ChatFile,
ChatDrawerOptions,
ChatDrawerHandle,
// AICommandBar types
AICommandBarOptions,
AICommandBarHandle,
AICommandBarCommand,
CommandBarResult,
ToolCallSummary,
// AIGenerationButton types
AIGenerationButtonOptions,
AIGenerationButtonHandle,
AIGenerationButtonMode,
GenerationResult,
// Tool types
ModelInterfaceTool,
ModelInterfaceToolSchema,
ToolCall,
ToolCallResponse,
// API types
RealtimeChatHistory,
// Hook types
UseDevicChatOptions,
} from '@devicai/ui';License
MIT
