npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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.

npm version License: MIT Vue 3 TypeScript

📖 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-chatbot

Peer 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.ts if you prefer. The important thing is to import @binarcode/restify-chatbot/styles once 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 enableAudioInput is enabled
  • 📊 Animated wave indicator shows when recording is active
  • 🔴 Visual feedback with red styling during recording
  • 🎨 Customizable via audioButton, audioButtonRecording, and audioRecordingIndicator UI classes
  • 🌐 Text labels customizable via startRecording, stopRecording, and recording texts

📡 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_id field is only sent when useConversationId: true is configured. The attachment_ids array 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:

  1. Receive the conversation ID from the backend via the start SSE event
  2. Store it in the Pinia store (store.conversationId)
  3. Send it back with subsequent requests in the same conversation
  4. 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:

  1. When useConversationHistory is enabled, a history button appears in the drawer header
  2. Clicking the button opens a sidebar showing past conversations
  3. Conversations are automatically saved to localStorage when completed
  4. Each conversation shows: title (from first message or backend), date, message count
  5. 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 requests

Upload 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_id
  • attachment_id
  • openai_file_id

The package automatically:

  1. Stores the backend file ID when received from upload response
  2. Includes all backend file IDs in the attachment_ids array in ask requests
  3. 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 via chatHistoryKey)
  • 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.