@binarcode/restify-chatbot
v1.0.2
Published
Professional AI Chatbot Vue 3 component for Laravel Restify backends. SSE streaming, file uploads, @mentions, context-aware suggestions, and full TypeScript support.
Readme
@binarcode/restify-chatbot
A production-ready AI chatbot component for Vue 3 with real-time SSE streaming, file attachments, @mentions, and seamless Laravel Restify integration.
📖 Laravel Restify | 📦 npm | 🏢 BinarCode
✨ Features
- 🌊 Real-time SSE Streaming - Smooth character-by-character response streaming
- 📎 File Attachments - Upload and process documents, images, and more
- 🎤 Audio Input - Voice recording with visual feedback and wave animation
- 👥 @Mentions System - Reference entities from your application (employees, jobs, projects, etc.)
- 💡 Context-Aware Suggestions - Smart prompts based on current page/route
- 💬 Chat History - Persistent conversation memory with configurable limits
- 📝 Markdown Rendering - Beautiful formatting with syntax highlighting
- 📊 Quota Management - Track and display API usage limits
- 🎨 Fully Customizable - Override any style with CSS classes
- 🌙 Dark Mode Support - Automatic dark/light theme detection
- 📱 Responsive Design - Works on desktop, tablet, and mobile
- ⌨️ Keyboard Shortcuts - Quick access with configurable shortcuts
- 🔳 Fullscreen Mode - Expandable chat interface
- 🎯 TypeScript First - Full type definitions included
- 🗃️ Pinia Integration - State management built-in
- 🔧 Slot-Based Customization - Override any component section
- 🌐 i18n Ready - Full internationalization support
- 🆘 Support Mode - Route conversations to human support
- 🔄 Retry Logic - Automatic retry with configurable backoff
📦 Installation
npm install @binarcode/restify-chatbotPeer Dependencies
npm install vue@^3.3.0 pinia@^2.1.0🚀 Quick Start
1. Create Plugin Configuration
Create a plugin file to configure the chatbot with your API endpoints and preferences:
// plugins/restifyAi.ts
import type { App } from 'vue'
import { RestifyAiPlugin } from '@binarcode/restify-chatbot'
import '@binarcode/restify-chatbot/styles'
export function installRestifyAi(app: App) {
app.use(RestifyAiPlugin, {
// API Configuration
endpoints: {
ask: '/ai/ask', // SSE streaming endpoint
quota: '/ai/quota', // Optional: quota endpoint
uploadFile: '/ai/upload', // Optional: file upload endpoint
},
baseUrl: import.meta.env.VITE_API_URL,
// Authentication
getAuthToken: () => localStorage.getItem('token'),
// AI Model Configuration
model: 'gpt-4o-mini',
temperature: 0.7,
// Optional: Custom labels for i18n
labels: () => ({
title: 'How can I help you today?',
aiName: 'AI Assistant',
placeholder: 'Ask me anything...',
// ... more labels
}),
})
}Note: You can also import styles directly in
main.tsif you prefer. The important thing is to import@binarcode/restify-chatbot/stylesonce in your application.
2. Register in Your Application
// main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import { installRestifyAi } from './plugins/restifyAi'
const app = createApp(App)
app.use(createPinia())
installRestifyAi(app)
app.mount('#app')3. Add the Component
<template>
<AiChatDrawer v-model="aiStore.showChat" top-offset="50px" />
</template>
<script setup lang="ts">
import { AiChatDrawer, useRestifyAiStore } from '@binarcode/restify-chatbot'
const aiStore = useRestifyAiStore()
</script>4. Enable Keyboard Shortcut (Optional)
// In your layout or App.vue
import { useAiDrawerShortcut } from '@binarcode/restify-chatbot'
useAiDrawerShortcut() // Enables Cmd/Ctrl+G to toggle⚙️ Configuration
Configuration Options Example
import type { App } from 'vue'
import { RestifyAiPlugin } from '@binarcode/restify-chatbot'
import type { MentionProvider, SuggestionProvider, AISuggestion } from '@binarcode/restify-chatbot'
export function setupRestifyAi(app: App) {
app.use(RestifyAiPlugin, {
// ═══════════════════════════════════════════════════════════════
// REQUIRED
// ═══════════════════════════════════════════════════════════════
endpoints: {
ask: '/ask', // SSE streaming endpoint
uploadFile: '/ai/upload', // File upload endpoint
quota: '/ai/quota', // Quota fetch endpoint
},
getAuthToken: () => localStorage.getItem('token'),
// ═══════════════════════════════════════════════════════════════
// API CONFIGURATION
// ═══════════════════════════════════════════════════════════════
baseUrl: 'https://api.example.com',
// Custom headers for every request
getCustomHeaders: () => ({
'X-Tenant-ID': getTenantId(),
'Accept-Language': getCurrentLocale(),
}),
// Transform request payload before sending
buildRequest: (payload) => ({
...payload,
customField: 'value',
}),
// Parse SSE stream content (default handles OpenAI format)
parseStreamContent: (data) => {
const parsed = JSON.parse(data)
return parsed.choices?.[0]?.delta?.content || null
},
// ═══════════════════════════════════════════════════════════════
// RETRY CONFIGURATION
// ═══════════════════════════════════════════════════════════════
retry: {
maxRetries: 3,
retryDelay: 1000,
shouldRetry: (error, attempt) => attempt < 3,
},
// ═══════════════════════════════════════════════════════════════
// INTERNATIONALIZATION
// ═══════════════════════════════════════════════════════════════
// Permission check function
can: (permission) => userCan(permission),
// ═══════════════════════════════════════════════════════════════
// LABELS (all customizable)
// ═══════════════════════════════════════════════════════════════
// Option 1: Static labels (simple)
labels: {
title: 'AI Assistant',
aiName: 'AI Assistant',
you: 'You',
newChat: 'New chat',
placeholder: 'Ask me anything...',
inputPlaceholder: 'Ask me anything...',
supportPlaceholder: 'Describe your issue...',
loadingText: 'Gathering data...',
analyzingText: 'Analyzing...',
craftingText: 'Crafting response...',
quotaRemaining: 'questions remaining',
noQuota: 'No AI credit available',
contactSupport: 'Contact Support',
close: 'Close',
minimize: 'Minimize',
fullscreen: 'Fullscreen',
exitFullscreen: 'Exit fullscreen',
copyToClipboard: 'Copy to clipboard',
copied: 'Content copied to clipboard',
showMore: 'Show more',
showLess: 'Show less',
retry: 'Retry',
attachFiles: 'Attach files',
emptyStateTitle: 'How can I help you today?',
emptyStateDescription: 'Ask questions or get help with tasks',
keyboardShortcutHint: 'Press ⌘G to toggle',
sendMessage: 'Send message',
attachFile: 'Attach file',
closeConfirmTitle: 'Close chat window?',
closeConfirmMessage: 'You will lose the conversation.',
confirmClose: 'Yes, close it',
cancel: 'Cancel',
toggleSupportMode: 'Contact Support',
exitSupportMode: 'Exit Support Mode',
historyLimitReachedTitle: 'Conversation Limit Reached',
historyLimitReachedMessage: 'Maximum messages reached.',
startNewChat: 'New Chat',
},
// Option 2: Reactive labels with i18n (recommended for multi-language)
// Pass a function that returns labels - gets called fresh on each render
labels: () => {
const { t } = i18n
return {
title: t('How can I help you today?'),
aiName: t('AI Assistant'),
newChat: t('New chat'),
// ... all other labels
}
},
// ═══════════════════════════════════════════════════════════════
// MENTION PROVIDERS
// ═══════════════════════════════════════════════════════════════
mentionProviders: createMentionProviders(),
// ═══════════════════════════════════════════════════════════════
// SUGGESTION PROVIDERS (reactive with i18n)
// ═══════════════════════════════════════════════════════════════
// For reactive suggestions, wrap in computed and use t() inside functions
suggestionProviders: computed(() => createSuggestionProviders()),
defaultSuggestions: computed(() => getDefaultSuggestions()),
// ═══════════════════════════════════════════════════════════════
// THEME
// ═══════════════════════════════════════════════════════════════
theme: {
primaryColor: '#3b82f6',
primaryLightColor: '#60a5fa',
userBubbleColor: '#3b82f6',
userTextColor: '#ffffff',
borderColor: '#e5e7eb',
drawerWidth: '600px',
drawerFullscreenWidth: '90vw',
},
// ═══════════════════════════════════════════════════════════════
// LIMITS & AI MODEL
// ═══════════════════════════════════════════════════════════════
chatHistoryLimit: 20, // Maximum user messages per conversation
model: 'gpt-4', // AI model to use (passed to backend)
temperature: 0.7, // AI temperature (0-1)
maxTokens: 2048, // Maximum tokens per response
maxAttachments: 5,
maxFileSize: 10 * 1024 * 1024, // 10MB
acceptedFileTypes: 'image/*,.pdf,.txt,.doc,.docx,.xls,.xlsx,.csv',
// ═══════════════════════════════════════════════════════════════
// STORAGE
// ═══════════════════════════════════════════════════════════════
chatHistoryKey: 'app_chat_history',
drawerStateKey: 'app_chat_drawer_open',
// ═══════════════════════════════════════════════════════════════
// FEATURES
// ═══════════════════════════════════════════════════════════════
keyboardShortcut: 'mod+g', // 'mod' = Cmd on Mac, Ctrl on Windows
enableSupportMode: true,
useQuota: true, // Enable quota management
useHeadersRateLimiter: true, // Use rate limit headers instead of quota endpoint
useConversationId: true, // Enable conversation ID tracking
useConversationHistory: true, // Enable conversation history sidebar
maxConversationHistory: 10, // Max conversations to store (default: 10)
canToggle: () => true,
// ═══════════════════════════════════════════════════════════════
// AVATARS
// ═══════════════════════════════════════════════════════════════
assistantAvatar: CustomAiAvatarComponent,
userAvatar: () => authStore.profile?.avatar || null,
// ═══════════════════════════════════════════════════════════════
// CALLBACKS
// ═══════════════════════════════════════════════════════════════
onError: (error) => console.error('AI Error:', error),
onQuotaFetched: (quota) => console.log('Quota:', quota),
onMessageSent: (message) => analytics.track('ai_message_sent'),
onResponseReceived: (message) => console.log('Response:', message),
onDrawerToggle: (isOpen) => console.log('Drawer:', isOpen),
onNewChat: () => console.log('New chat started'),
onSetupComplete: () => console.log('Setup completed'),
// Stream lifecycle hooks
onStreamStart: () => console.log('Stream started'),
onStreamEnd: (fullMessage) => console.log('Stream ended'),
onStreamChunk: (chunk) => console.log('Chunk:', chunk),
beforeSend: (payload) => payload,
afterResponse: (message) => {},
// File upload hooks
onFileUploadStart: (file) => {},
onFileUploadProgress: (file, progress) => {},
onFileUploadComplete: (file) => {},
onFileUploadError: (file, error) => {},
})
}📋 AiChatDrawer Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| modelValue | boolean | required | Controls drawer visibility (v-model) |
| width | string | "600px" | Drawer width |
| fullscreenWidth | string | "90vw" | Width when in fullscreen mode |
| topOffset | string | "0" | Top offset for fixed headers |
| position | "left" \| "right" | "right" | Drawer position |
| showBackdrop | boolean | false | Show backdrop overlay |
| closeOnBackdropClick | boolean | true | Close when clicking backdrop |
| closeOnEscape | boolean | true | Close on Escape key |
| showQuota | boolean | true | Show quota display (API usage remaining) |
| showMessageCount | boolean | true | Show message count badge (X/20 format) |
| showFullscreenToggle | boolean | true | Show fullscreen button |
| showMinimizeButton | boolean | true | Show minimize button |
| showCloseButton | boolean | true | Show close button |
| showNewChatButton | boolean | true | Show new chat button |
| confirmClose | boolean | true | Confirm before clearing history |
| enableAudioInput | boolean | false | Enable voice recording button |
| isRecording | boolean | false | Audio recording state (controlled) |
| inputValue | string | '' | Input text value (v-model:inputValue) |
| autoFetchQuota | boolean | true | Auto-fetch quota when opened |
| historyLimit | HistoryLimitConfig | - | History limit configuration |
| loadingText | LoadingTextConfig | - | Loading text configuration |
| ui | AiChatDrawerUI | {} | Custom CSS classes |
| texts | AiChatDrawerTexts | {} | Custom text labels |
Component Usage Example
<template>
<AiChatDrawer
v-model="aiStore.showChat"
top-offset="50px"
:show-backdrop="false"
:confirm-close="true"
:show-quota="true"
@contact-support="handleContactSupport"
>
<template #context-link>
<p class="text-center text-xs text-gray-500">
For accurate results, provide
<router-link to="/settings/ai-context" class="text-primary-600 hover:underline">
company context
</router-link>
</p>
</template>
</AiChatDrawer>
</template>
<script setup lang="ts">
import { AiChatDrawer, useRestifyAiStore, useAiDrawerShortcut } from '@binarcode/restify-chatbot'
const aiStore = useRestifyAiStore()
useAiDrawerShortcut()
function handleContactSupport() {
// Handle support request
}
</script>Audio Input Example
Enable voice recording with the enableAudioInput prop and handle the recording state:
<template>
<AiChatDrawer
v-model="aiStore.showChat"
v-model:input-value="audioInput"
:enable-audio-input="true"
:is-recording="isRecording"
@toggle-audio-recording="toggleRecording"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { AiChatDrawer, useRestifyAiStore } from '@binarcode/restify-chatbot'
const aiStore = useRestifyAiStore()
const isRecording = ref(false)
const audioInput = ref('')
let mediaRecorder: MediaRecorder | null = null
async function toggleRecording() {
if (isRecording.value) {
// Stop recording
mediaRecorder?.stop()
isRecording.value = false
} else {
// Start recording
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
mediaRecorder = new MediaRecorder(stream)
const chunks: Blob[] = []
mediaRecorder.ondataavailable = (e) => chunks.push(e.data)
mediaRecorder.onstop = async () => {
const audioBlob = new Blob(chunks, { type: 'audio/webm' })
// Process the audio (e.g., send to transcription API)
const transcription = await transcribeAudio(audioBlob)
audioInput.value = transcription
stream.getTracks().forEach(track => track.stop())
}
mediaRecorder.start()
isRecording.value = true
} catch (error) {
console.error('Failed to start recording:', error)
}
}
}
async function transcribeAudio(blob: Blob): Promise<string> {
// Implement your transcription logic here
// e.g., send to OpenAI Whisper, Google Speech-to-Text, etc.
return 'Transcribed text...'
}
</script>Features:
- 🎤 Microphone button appears when
enableAudioInputis enabled - 📊 Animated wave indicator shows when recording is active
- 🔴 Visual feedback with red styling during recording
- 🎨 Customizable via
audioButton,audioButtonRecording, andaudioRecordingIndicatorUI classes - 🌐 Text labels customizable via
startRecording,stopRecording, andrecordingtexts
📡 Events
| Event | Payload | Description |
|-------|---------|-------------|
| update:modelValue | boolean | Drawer state changed |
| update:inputValue | string | Input text value changed |
| close | - | Drawer was closed |
| contact-support | - | Support mode activated |
| new-chat | - | New chat started |
| toggle-audio-recording | - | Audio recording toggled |
🎰 Slots
| Slot | Props | Description |
|------|-------|-------------|
| header | HeaderSlotProps | Custom header content |
| quota | { quota: ChatQuota } | Custom quota display |
| setup | - | Custom setup guide |
| empty-state | EmptyStateSlotProps | Custom empty state |
| message | MessageSlotProps | Custom message bubble |
| input | InputSlotProps | Custom input area |
| context-link | - | Custom context link below input |
Slot Props Types
interface HeaderSlotProps {
quota: ChatQuota
isFullscreen: boolean
hasHistory: boolean
onNewChat: () => void
onClose: () => void
onMinimize: () => void
onToggleFullscreen: () => void
}
interface EmptyStateSlotProps {
suggestions: AISuggestion[]
onSuggestionClick: (suggestion: AISuggestion) => void
}
interface MessageSlotProps {
message: ChatMessage
isUser: boolean
isLoading: boolean
isStreaming: boolean
}
interface InputSlotProps {
modelValue: string
sending: boolean
disabled: boolean
onSubmit: (payload: SubmitPayload) => void
onCancel: () => void
}🏪 Store API
import { useRestifyAiStore } from '@binarcode/restify-chatbot'
const store = useRestifyAiStore()
// ═══════════════════════════════════════════════════════════════
// STATE
// ═══════════════════════════════════════════════════════════════
store.chatHistoryLimit // number - Maximum messages allowed
store.chatHistory // ChatMessage[] - All messages
store.uploadedFiles // Record<string, ChatAttachment> - Uploaded files by ID
store.showChat // boolean - Drawer visibility
store.sending // boolean - Message being sent
store.loading // boolean - Loading state
store.isFullscreen // boolean - Fullscreen mode
store.quota // { limit, used, remaining }
store.error // { message, failedQuestion, failedAttachments, timestamp, quotaExceeded }
store.supportRequestMode // boolean - Support mode active
store.pageContext // PageContext | null - Current page context
store.setupState // SetupState - Setup wizard state
store.conversationId // string | null - Current conversation ID from backend
store.conversationName // string | null - Current conversation name
store.conversationHistory // ConversationHistoryItem[] - List of saved conversations
store.showHistorySidebar // boolean - History sidebar visibility
// ═══════════════════════════════════════════════════════════════
// GETTERS
// ═══════════════════════════════════════════════════════════════
store.hasMessages // boolean - Has any messages
store.isInSetupMode // boolean - In setup mode
store.canChat // boolean - Can send messages
// ═══════════════════════════════════════════════════════════════
// ACTIONS
// ═══════════════════════════════════════════════════════════════
store.toggleDrawer() // Toggle drawer visibility
store.openDrawer() // Open drawer
store.closeDrawer() // Close drawer
store.askQuestion(question, attachments?, mentions?, isSupportRequest?)
store.cancelRequest() // Cancel current request
store.retry() // Retry failed message
store.clearChatHistory() // Clear all messages
store.clearError() // Clear error state
store.toggleSupportMode() // Toggle support mode
store.fetchQuota() // Fetch quota from server
store.uploadFile(file) // Upload file
store.setPageContext(context) // Set page context
store.scrollToBottom(delay?) // Scroll chat to bottom
// Conversation History Actions
store.toggleHistorySidebar() // Toggle history sidebar
store.loadConversation(id) // Load a saved conversation
store.renameConversation(id, title) // Rename a conversation
store.deleteConversation(id) // Delete a saved conversation
store.saveCurrentConversation() // Save current conversation
// Setup Mode Actions
store.startSetupMode() // Start setup wizard
store.setSetupStep(step) // Set current setup step
store.testConnection() // Test backend connection
store.completeSetup() // Complete setup
store.skipSetup() // Skip setup wizard🪝 Composables
useAiDrawerShortcut
Toggle drawer with keyboard shortcut:
import { useAiDrawerShortcut } from '@binarcode/restify-chatbot'
// Uses store's showChat state directly
useAiDrawerShortcut()usePageAiContext
Set page context for AI suggestions:
import { usePageAiContext } from '@binarcode/restify-chatbot'
// Simple usage
usePageAiContext('invoices')
// With dynamic metadata
usePageAiContext('employee-detail', {
employeeId: computed(() => route.params.id),
employeeName: computed(() => employee.value?.name),
})useAiContext
Programmatic context control:
import { useAiContext } from '@binarcode/restify-chatbot'
const { setContext, updateContext, clearContext, context } = useAiContext()
setContext({
pageType: 'dashboard',
entityType: 'report',
entityId: '123',
metadata: { period: 'Q4' }
})useAiSuggestions
Get suggestions for current context:
import { useAiSuggestions } from '@binarcode/restify-chatbot'
const { suggestions, hasContextualSuggestions, resolvePrompt } = useAiSuggestions()useLoadingText
Manage dynamic loading text messages:
import { useLoadingText } from '@binarcode/restify-chatbot'
const {
loadingMessage,
startLoadingText,
resetLoadingText
} = useLoadingText(
() => isSending.value,
() => ({
messages: ['Thinking...', 'Analyzing...', 'Crafting response...'],
intervals: [0, 2000, 5000]
})
)useHistoryLimit
Handle chat history limits with warnings:
import { useHistoryLimit } from '@binarcode/restify-chatbot'
const historyLimit = useHistoryLimit({
getHistoryLength: () => chatHistory.length,
getStoreLimit: () => store.chatHistoryLimit,
getConfig: () => historyLimitConfig,
getTexts: () => texts,
onStartNewChat: () => store.clearChatHistory(),
onNewChatEmit: () => emit('new-chat'),
})🏷️ Mention Providers
Enable @mentions to reference entities from your application:
import type { MentionProvider, MentionItem } from '@binarcode/restify-chatbot'
function createMentionProviders(): MentionProvider[] {
return [
{
type: 'employee',
label: 'Team Members',
iconClass: 'text-primary',
priority: 10,
// Search function - can be sync or async
search: (query: string): MentionItem[] => {
const employeeStore = useEmployeeStore()
const employees = employeeStore.allEmployees || []
if (!query) {
return employees.slice(0, 5).map(emp => ({
id: emp.id,
type: 'employee',
attributes: emp.attributes,
}))
}
const lowerQuery = query.toLowerCase()
return employees
.filter((emp) => {
const firstName = emp.attributes?.first_name?.toLowerCase() || ''
const lastName = emp.attributes?.last_name?.toLowerCase() || ''
return firstName.includes(lowerQuery) || lastName.includes(lowerQuery)
})
.slice(0, 5)
.map(emp => ({
id: emp.id,
type: 'employee',
attributes: emp.attributes,
}))
},
// Display formatting
getDisplayName: (item: MentionItem): string => {
const firstName = item.attributes?.first_name || ''
const lastName = item.attributes?.last_name || ''
return `${firstName} ${lastName}`.trim()
},
getSubtitle: (item: MentionItem): string | null => {
return item.attributes?.position || null
},
buildMentionText: (item: MentionItem): string => {
const firstName = item.attributes?.first_name || ''
const lastName = item.attributes?.last_name || ''
return `@${firstName} ${lastName}`.trim()
},
},
{
type: 'job',
label: 'Jobs',
iconClass: 'text-blue-500',
routes: ['/hiring/jobs'], // Only show on these routes
priority: 5,
search: async (query: string): Promise<MentionItem[]> => {
const response = await api.get('/jobs', { params: { search: query }})
return response.data.map(job => ({
id: job.id,
type: 'job',
attributes: job,
}))
},
getDisplayName: (item) => item.attributes?.title || 'Untitled',
getSubtitle: (item) => item.attributes?.location || null,
},
]
}MentionProvider Interface
interface MentionProvider {
type: string // Unique type identifier
label: string // Display label for group
icon?: Component // Icon component
iconClass?: string // Icon CSS classes
routes?: string[] // Limit to specific routes
priority?: number // Sort order (higher = first)
search: (query: string) => Promise<MentionItem[]> | MentionItem[]
getDisplayName?: (item: MentionItem) => string
getSubtitle?: (item: MentionItem) => string | null
buildMentionText?: (item: MentionItem) => string
}
interface MentionItem {
id: string
type: string
name?: string
label?: string
title?: string
attributes?: Record<string, any> | null
relationships?: Record<string, any> | null
}💡 Suggestion Providers
Context-aware suggestions based on current page:
import type { SuggestionProvider, AISuggestion, PageContext } from '@binarcode/restify-chatbot'
import { UserGroupIcon, ChartBarIcon, CalendarDaysIcon } from '@heroicons/vue/24/outline'
function createSuggestionProviders(): SuggestionProvider[] {
return [
{
id: 'employees',
routes: ['/employees'],
priority: 10,
getSuggestions: (context: PageContext): AISuggestion[] => {
const isDetailView = context.entityId
const employeeName = context.metadata?.employeeName
if (isDetailView && employeeName) {
// Suggestions for employee detail page
return [
{
id: 'performance-review',
title: 'Prepare Evaluation',
description: 'Performance review points',
icon: ChartBarIcon,
gradientClass: 'bg-gradient-to-br from-violet-500/10 to-purple-500/10',
prompt: `Help me prepare a performance review for ${employeeName}.`,
permission: 'manageEmployees',
},
]
}
// Suggestions for employee list
return [
{
id: 'find-available',
title: 'Who is Available?',
description: 'See who is not on leave',
icon: CalendarDaysIcon,
gradientClass: 'bg-gradient-to-br from-teal-500/10 to-emerald-500/10',
prompt: 'Who is available today and not on leave?',
permission: 'manageEmployees',
},
{
id: 'team-analytics',
title: 'Team Analysis',
description: 'Team structure insights',
icon: ChartBarIcon,
gradientClass: 'bg-gradient-to-br from-amber-500/10 to-orange-500/10',
prompt: 'Give me an overview of our team structure.',
permission: 'manageEmployees',
},
]
},
},
{
id: 'hiring',
routes: ['/hiring/jobs', '/hiring/candidates'],
priority: 10,
getSuggestions: (): AISuggestion[] => [
{
id: 'open-positions',
title: 'Open Positions',
description: 'List all open job positions',
icon: UserGroupIcon,
prompt: 'Show me all currently open job positions.',
},
],
},
]
}Default Suggestions
function getDefaultSuggestions(): AISuggestion[] {
return [
{
id: 'help',
title: 'How can you help?',
description: 'Learn what I can do',
prompt: 'What can you help me with?',
},
{
id: 'contact-support',
title: 'Contact Support',
description: 'Get help from a human',
prompt: 'I need to contact support',
isSupportRequest: true,
},
]
}SuggestionProvider Interface
interface SuggestionProvider {
id: string // Unique identifier
routes?: string[] // Route patterns to match
matcher?: (path: string, context: PageContext | null) => boolean
getSuggestions: (context: PageContext) => AISuggestion[]
extractContext?: (path: string) => Record<string, any>
priority?: number // Higher = first
}
interface AISuggestion {
id: string
title: string
description?: string
icon?: Component
className?: string
gradientClass?: string
prompt: string | ((context: PageContext) => string)
permission?: string
category?: string
isSupportRequest?: boolean
}🎨 UI Customization
Override CSS classes for any component. The ui prop on AiChatDrawer allows you to customize all components in one place - it extends all child component UI interfaces, so you can customize the drawer, input, messages, empty state, and mentions all from a single object.
<AiChatDrawer
v-model="isOpen"
:ui="{
// Drawer customization
backdrop: 'bg-black/50 backdrop-blur-sm',
drawer: 'shadow-2xl',
panel: 'bg-gray-50 dark:bg-gray-900',
header: 'border-b-2 border-primary-500',
body: 'custom-scrollbar',
footer: 'border-t border-gray-200',
// ChatInput customization (automatically passed down)
textarea: 'rounded-xl',
sendButton: 'bg-blue-500 hover:bg-blue-600',
// ChatMessage customization (automatically passed down)
userBubble: 'bg-blue-500 text-white',
assistantBubble: 'bg-gray-100 dark:bg-gray-800',
// AiEmptyState customization (automatically passed down)
suggestionCard: 'border-2 border-primary-500',
}"
/>AiChatDrawerUI
The main UI interface that combines all component UI interfaces. Pass this to the ui prop to customize all components:
interface AiChatDrawerUI extends ChatInputUI, ChatMessageUI, AiEmptyStateUI, MentionListUI {
// Drawer-specific
backdrop?: string // Backdrop overlay
drawer?: string // Main drawer container
panel?: string // Inner panel
header?: string // Header container
headerTitle?: string // Header title
headerActions?: string // Header actions
headerActionButton?: string // Header buttons
body?: string // Messages container
footer?: string // Footer container
// Dialogs
closeConfirmModal?: string // Confirm modal
closeConfirmButton?: string // Confirm button
cancelButton?: string // Cancel button
historyLimitModal?: string // History limit modal
historyLimitButton?: string // History limit button
// Header elements
quotaDisplay?: string // Quota display
messageCountBadge?: string // Message count badge
newChatButton?: string // New chat button
// Error display
errorContainer?: string // Error container
errorMessage?: string // Error message
retryButton?: string // Retry button
contactSupportButton?: string // Contact support button
}ChatInputUI
interface ChatInputUI {
root?: string // Root container
form?: string // Form wrapper
inputContainer?: string // Input container
inputWrapper?: string // Input border wrapper
textarea?: string // Textarea element
attachButton?: string // Attach button
sendButton?: string // Send button
sendButtonActive?: string // Send active state
sendButtonDisabled?: string // Send disabled state
stopButton?: string // Stop button
supportToggle?: string // Support toggle
supportBadge?: string // Support badge
attachmentsContainer?: string // Attachments container
attachmentItem?: string // Attachment item
attachmentThumbnail?: string // Attachment thumbnail
attachmentRemove?: string // Remove button
suggestionsDropdown?: string // Suggestions dropdown
suggestionItem?: string // Suggestion item
suggestionItemSelected?: string // Selected suggestion
contextLink?: string // Context link
// Audio input
audioButton?: string // Audio button
audioButtonRecording?: string // Audio button when recording
audioRecordingIndicator?: string // Recording indicator
}ChatMessageUI
interface ChatMessageUI {
root?: string // Root container
userMessage?: string // User message container
userBubble?: string // User bubble
userAvatar?: string // User avatar
assistantMessage?: string // Assistant container
assistantBubble?: string // Assistant bubble
loadingIndicator?: string // Loading indicator
loadingDots?: string // Loading dots
content?: string // Content wrapper
attachmentsContainer?: string // Attachments
attachmentItem?: string // Attachment item
actionsContainer?: string // Actions container
showMoreButton?: string // Show more button
}AiEmptyStateUI
interface AiEmptyStateUI {
root?: string // Root container
content?: string // Content container
header?: string // Header container
badge?: string // AI badge
title?: string // Title
description?: string // Description
grid?: string // Suggestions grid
suggestionCard?: string // Suggestion card
suggestionIconContainer?: string // Icon container
suggestionIcon?: string // Icon
suggestionTitle?: string // Suggestion title
suggestionDescription?: string // Suggestion description
}MentionListUI
interface MentionListUI {
root?: string // Root container
container?: string // List container
groupHeader?: string // Group header
item?: string // Item
itemSelected?: string // Selected item
itemIcon?: string // Item icon
itemContent?: string // Item content
itemName?: string // Item name
itemSubtitle?: string // Item subtitle
}AiAvatarUI / UserAvatarUI
interface AiAvatarUI {
container?: string // Container
icon?: string // Icon
}
interface UserAvatarUI {
container?: string // Container
icon?: string // Icon
}ChatMessageActionsUI
interface ChatMessageActionsUI {
container?: string // Container
button?: string // Action button
copyButton?: string // Copy button
successState?: string // Success state
}📝 TypeScript Types
All types are exported:
import type {
// Core Config
RestifyAiConfig,
RestifyAiEndpoints,
RestifyAiLabels,
RestifyAiTheme,
// Chat Types
ChatMessage,
ChatQuota,
ChatError,
ChatAttachment,
ChatRole,
SubmitPayload,
Mention,
ConversationHistoryItem,
// Context
PageContext,
// Providers
MentionProvider,
MentionItem,
MentionContext,
MentionParseResult,
SuggestionProvider,
AISuggestion,
// History
HistoryLimitConfig,
LoadingTextConfig,
// Setup
SetupStep,
SetupState,
// Store
AiStoreState,
// UI Customization
AiChatDrawerUI,
ChatInputUI,
ChatMessageUI,
AiEmptyStateUI,
MentionListUI,
AiAvatarUI,
UserAvatarUI,
ChatMessageActionsUI,
// Text Customization
AiChatDrawerTexts,
ChatInputTexts,
ChatMessageTexts,
AiEmptyStateTexts,
MentionListTexts,
ChatMessageActionsTexts,
// Slot Props
HeaderSlotProps,
MessageSlotProps,
InputSlotProps,
EmptyStateSlotProps,
// API Hooks
AiRequestPayload,
AiStreamChunk,
BeforeSendHook,
AfterResponseHook,
OnStreamStartHook,
OnStreamEndHook,
OnStreamChunkHook,
StreamParserFunction,
RequestInterceptor,
ResponseInterceptor,
RetryConfig,
// Functions
PermissionCheckFunction,
} from '@binarcode/restify-chatbot'
// Constants
import { ChatRoles } from '@binarcode/restify-chatbot'🔌 Backend Integration
This package is designed for Laravel Restify backends:
Ask Endpoint (SSE Stream)
// routes/api.php
Route::post('/ask', [AiController::class, 'ask']);Request:
{
"message": "Who is available today?",
"history": [
{ "role": "user", "content": "Hello" },
{ "role": "assistant", "content": "Hi! How can I help?" }
],
"stream": true,
"files": [{ "id": "file-123", "name": "report.pdf", "backendFileId": 456 }],
"attachment_ids": [456],
"mentions": [{ "id": "emp-1", "type": "employee", "name": "John Doe" }],
"contact_support": false,
"conversation_id": "conv-abc123"
}Note: The
conversation_idfield is only sent whenuseConversationId: trueis configured. Theattachment_idsarray contains backend file IDs returned from the upload endpoint.
Response (SSE):
event: start
data: {"conversation_id":"conv-abc123"}
data: {"choices":[{"delta":{"content":"Based on"}}]}
data: {"choices":[{"delta":{"content":" the schedule..."}}]}
data: [DONE]Conversation ID Tracking
When useConversationId: true is enabled in the plugin configuration, the package will:
- Receive the conversation ID from the backend via the
startSSE event - Store it in the Pinia store (
store.conversationId) - Send it back with subsequent requests in the same conversation
- Clear it when starting a new chat
This is useful for:
- Tracking conversations across multiple requests
- Maintaining context on the backend
- Analytics and logging
- Multi-turn conversation management
Conversation History
When useConversationHistory: true is enabled, users can access a sidebar showing their previous conversations:
Features:
- 📜 View past conversations - Browse through saved chats
- ✏️ Rename conversations - Give meaningful titles to chats
- 🗑️ Delete conversations - Remove unwanted history
- 🔄 Switch conversations - Seamlessly load previous chats
- 💾 Automatic saving - Conversations save automatically
Configuration:
app.use(RestifyAiPlugin, {
useConversationId: true, // Required for history
useConversationHistory: true, // Enable history sidebar
maxConversationHistory: 10, // Max conversations to keep (default: 10)
})How it works:
- When
useConversationHistoryis enabled, a history button appears in the drawer header - Clicking the button opens a sidebar showing past conversations
- Conversations are automatically saved to
localStoragewhen completed - Each conversation shows: title (from first message or backend), date, message count
- Users can rename, delete, or switch between conversations
Backend Integration:
The backend can provide a conversation name via the start SSE event:
event: start
data: {"conversation_id":"conv-abc123","conversation_name":"Budget Analysis Q1"}If no name is provided, the title defaults to a truncated version of the first user message.
Storage:
- Conversation list:
restify_ai_conversation_history - Conversation messages:
restify_ai_conv_{conversationId} - Current conversation ID:
restify_ai_current_conversation
ConversationHistoryItem Interface:
interface ConversationHistoryItem {
id: string // Conversation ID from backend
title: string // Display title
createdAt: number // Timestamp when created
updatedAt: number // Timestamp of last update
messageCount: number // Number of messages
}Quota Endpoint
// routes/api.php
Route::get('/ai/quota', [AiController::class, 'quota']);Response:
{
"data": {
"limit": 100,
"used": 25,
"remaining": 75
}
}Rate Limiting via Headers
As an alternative to the quota endpoint, you can enable headers-based rate limiting. When useHeadersRateLimiter is enabled, the package will extract rate limit information directly from response headers instead of making separate quota endpoint calls.
Configuration:
app.use(RestifyAi, {
useQuota: true,
useHeadersRateLimiter: true, // Enable headers-based rate limiting
})Required Response Headers:
Your chat endpoint should return these headers with each response:
x-ratelimit-limit: 100 // Total allowed requests
x-ratelimit-remaining: 75 // Remaining requestsUpload Endpoint
// routes/api.php
Route::post('/ai/upload', [AiController::class, 'upload']);Response:
{
"data": {
"id": 456,
"name": "document.pdf",
"url": "/storage/uploads/document.pdf",
"type": "application/pdf",
"size": 102400,
"extracted_text": "Optional extracted text content..."
}
}Backend File ID Support
When files are uploaded, the backend can return an ID that will be automatically tracked and sent with subsequent requests. This enables the backend to reference pre-processed files.
Supported ID field names in upload response:
id(recommended)file_idattachment_idopenai_file_id
The package automatically:
- Stores the backend file ID when received from upload response
- Includes all backend file IDs in the
attachment_idsarray in ask requests - Preserves the ID through the entire conversation
Example flow:
1. User uploads file.pdf
2. Backend returns: { "id": 456, "name": "file.pdf", ... }
3. Package stores backendFileId: 456
4. User sends message with attachment
5. Request includes: { "attachment_ids": [456], "files": [...] }
6. Backend can use attachment_ids to reference stored/processed files⌨️ Keyboard Shortcuts
| Shortcut | Action |
|----------|--------|
| ⌘/Ctrl + G | Toggle drawer (configurable) |
| Escape | Close drawer |
| Enter | Send message |
| Shift + Enter | New line |
💾 Session Storage
Chat history persists in sessionStorage by default:
- Key:
restify_ai_chat_history(configurable viachatHistoryKey) - Cleared on browser close
- Persists across page navigation
📦 Package Exports
// Components
export {
AiChatDrawer,
AiEmptyState,
ChatInput,
ChatMessage,
ChatMessageActions,
MentionList,
AiAvatar,
UserAvatar,
ErrorBoundary,
} from './components'
// Store
export { useRestifyAiStore } from './store'
// Composables
export {
useAiDrawerShortcut,
useKeyboardShortcut,
usePageAiContext,
useAiContext,
useAiSuggestions,
useMentionParsing,
useChatMarkdown,
useChatScroll,
useChatErrorHandling,
useLoadingText,
useHistoryLimit,
useSuggestionFilter,
useAutoScroll,
formatMentionsForApi,
groupMentionsByType,
} from './composables'
// Config
export {
RestifyAiPlugin,
getRestifyAiConfig,
setRestifyAiConfig,
getLabel,
getConfigValue,
isConfigured,
defaultLabels,
} from './config'
// Suggestions
export {
registerSuggestionProvider,
getSuggestionsForPath,
} from './suggestions'
// Utilities
export { isImageFile, formatFileSize } from './utils'
// Types
export * from './types'🤝 Requirements
- Vue 3.3+
- Pinia 2.1+
- A backend implementing the streaming chat API (e.g., Laravel Restify)
🔗 Links
📄 License
MIT © BinarCode
Built with ❤️ by the BinarCode team.
