Skip to content

Standalone Components API

The standalone components provide a flexible, store-independent way to build chat interfaces. These components accept data via props and communicate through events, making them perfect for integrating into existing applications with custom state management.

Overview

Standalone components are designed for maximum flexibility:

  • No store dependencies - Manage state however you want
  • Props-based - Pass data directly to components
  • Event-driven - Handle user interactions via events
  • Fully styled - Beautiful UI out of the box
  • TypeScript support - Full type safety

Installation

bash
npm install @sprout_ai_labs/sidekick

Import Standalone Components

typescript
import {
  SidekickMessages,
  SidekickChatInput,
  SidekickCollapsibleThought,
  SidekickCollapsibleThoughtStreaming
} from '@sprout_ai_labs/sidekick'

// Don't forget the CSS
import '@sprout_ai_labs/sidekick/dist/sprout-sidekick.css'

SidekickMessages

Displays a list of chat messages with support for user messages, bot responses, thinking processes, and streaming.

Props

PropTypeDefaultDescription
messagesChatMessage[][]Array of chat messages to display
isTypingbooleanfalseShow typing indicator
currentThoughtsThoughtStep[][]Real-time thoughts during streaming
currentChunkedMessagestring''Streaming message content
streamingAnimatingbooleanfalseEnable shimmer animation effect

Types

typescript
interface ChatMessage {
  id: string
  sender: 'user' | 'bot' | 'ticket'
  text: string
  isError?: boolean
  thoughtSteps?: ThoughtStep[]
  thinkingDuration?: number
  kbSources?: KBSource[]
  analyticsId?: string
}

interface ThoughtStep {
  event: string
  message: string
  agent_name?: string
  agent_id?: string
  tool_details?: Record<string, unknown>
  completed: boolean
}

interface KBSource {
  id: string
  title: string
  url?: string
  snippet?: string
}

Usage Example

vue
<template>
  <div class="messages-container">
    <SidekickMessages
      :messages="messages"
      :is-typing="isTyping"
      :current-thoughts="currentThoughts"
      :current-chunked-message="currentChunkedMessage"
      :streaming-animating="streamingAnimating"
    />
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { SidekickMessages } from '@sprout_ai_labs/sidekick'
import type { ChatMessage } from '@sprout_ai_labs/sidekick'

const messages = ref<ChatMessage[]>([
  {
    id: '1',
    sender: 'user',
    text: 'How do I use standalone components?'
  },
  {
    id: '2',
    sender: 'bot',
    text: 'Standalone components are easy to use! Just pass data via props.',
    thoughtSteps: [
      { message: 'Analyzing query', completed: true, event: 'process' },
      { message: 'Searching docs', completed: true, event: 'tool_call' }
    ],
    thinkingDuration: 3
  }
])

const isTyping = ref(false)
const currentThoughts = ref([])
const currentChunkedMessage = ref('')
const streamingAnimating = ref(false)
</script>

Features

  • Auto-scroll: Automatically scrolls to the latest message
  • Markdown rendering: Supports markdown in bot messages
  • Thought processes: Shows collapsible thinking steps
  • Knowledge sources: Displays KB sources with citations
  • Error handling: Displays error messages with appropriate styling
  • Streaming support: Shows real-time message streaming with animations

SidekickChatInput

A beautiful, auto-expanding text input for chat messages with placeholder animation support.

Props

PropTypeDefaultDescription
placeholderstring'Ask me anything...'Default placeholder text
placeholdersstring[][]Array of placeholders to cycle through
disabledbooleanfalseDisable input
showClearButtonbooleantrueShow clear button
maxHeightstring'120px'Maximum input height
minHeightstring'42px'Minimum input height
autoFocusbooleanfalseAuto-focus on mount

Events

EventPayloadDescription
submit(message: string)Fired when user submits a message
input(value: string)Fired on input change
clear()Fired when clear button is clicked

Usage Example

vue
<template>
  <SidekickChatInput
    :placeholder="placeholder"
    :placeholders="placeholderOptions"
    :disabled="isLoading"
    :show-clear-button="true"
    :auto-focus="true"
    @submit="handleSubmit"
    @input="handleInput"
    @clear="handleClear"
  />
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { SidekickChatInput } from '@sprout_ai_labs/sidekick'

const placeholder = ref('Ask me anything...')
const placeholderOptions = ref([
  'What can you help with?',
  'Ask a question...',
  'Need assistance?'
])
const isLoading = ref(false)

function handleSubmit(message: string) {
  console.log('User submitted:', message)
  // Send message to your API
  sendMessage(message)
}

function handleInput(value: string) {
  console.log('User is typing:', value)
}

function handleClear() {
  console.log('Input cleared')
}

async function sendMessage(message: string) {
  isLoading.value = true
  // Your API call logic here
  isLoading.value = false
}
</script>

Features

  • Auto-expanding: Grows as user types (up to maxHeight)
  • Animated placeholders: Cycles through placeholder text with typing animation
  • Keyboard shortcuts:
    • Enter - Submit message
    • Shift + Enter - New line
  • Clear button: Quick way to clear input
  • Paste handling: Smart paste with formatting cleanup
  • Disabled state: Visual feedback when disabled

SidekickCollapsibleThought

Displays completed AI thinking processes in a collapsible format.

Props

PropTypeDefaultDescription
stepsThinkingStep[][]Array of thinking steps
durationnumber0Thinking duration in seconds

Types

typescript
interface ThinkingStep {
  text: string           // The step message
  completed: boolean     // Whether step is complete
  event?: string         // Event type: 'tool_call', 'observation', 'process', etc.
  agent_name?: string    // Optional agent name
  tool_details?: Record<string, unknown>  // Optional tool metadata
}

Usage Example

vue
<template>
  <SidekickCollapsibleThought
    :steps="thoughtSteps"
    :duration="thinkingDuration"
  />
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { SidekickCollapsibleThought } from '@sprout_ai_labs/sidekick'

const thoughtSteps = ref([
  {
    text: 'Analyzing user query for intent',
    completed: true,
    event: 'process'
  },
  {
    text: 'Using Document Retriever Tool to search knowledge base',
    completed: true,
    event: 'tool_call'
  },
  {
    text: 'Found 12 relevant documents matching criteria',
    completed: true,
    event: 'observation'
  },
  {
    text: 'Synthesizing information from sources',
    completed: true,
    event: 'process'
  }
])

const thinkingDuration = ref(5)
</script>

Features

  • Collapsed by default: Keeps UI clean
  • Smart header: Shows "Thought for X seconds" or last step message
  • Event icons: Different icons for different event types
  • Markdown stripping: Header text strips markdown for clean display
  • Truncation: Long headers truncate to 60 characters
  • See more/less: Long steps can be expanded
  • All steps completed: Shows checkmarks for all steps

SidekickCollapsibleThoughtStreaming

Displays real-time AI thinking processes with animations during streaming.

Props

PropTypeDefaultDescription
stepsThinkingStep[][]Array of thinking steps
durationnumber0Thinking duration (used when completed)

Types

typescript
interface ThinkingStep {
  message: string        // Step message (note: 'message' not 'text')
  completed: boolean     // Last step should be false during streaming
  event?: string         
  agent_name?: string
  tool_details?: Record<string, unknown>
}

Usage Example

vue
<template>
  <SidekickCollapsibleThoughtStreaming
    :steps="liveThoughts"
    :duration="streamingDuration"
  />
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { SidekickCollapsibleThoughtStreaming } from '@sprout_ai_labs/sidekick'

const liveThoughts = ref([
  {
    message: 'Initializing request',
    completed: true,
    event: 'start'
  },
  {
    message: 'Analyzing context and generating response',
    completed: false,  // Last step incomplete during streaming
    event: 'process'
  }
])

const streamingDuration = ref(0)

// As you receive streaming updates:
function handleStreamingUpdate(newThought: any) {
  if (newThought) {
    liveThoughts.value.push({
      message: newThought.message,
      completed: newThought.completed,
      event: newThought.event
    })
  }
}
</script>

Features

  • Shimmer animation: Animated text in header during streaming
  • Loading spinner: Shows on last incomplete step
  • Cycling loaders: When collapsed, cycles through predefined messages
  • Real-time updates: Updates as new thoughts arrive
  • Smooth transitions: Animated state changes
  • Completion state: Shows final "Thought for X seconds" when done

Complete Chat Interface Example

Here's a complete example combining all standalone components:

vue
<template>
  <div class="chat-interface">
    <!-- Messages Area -->
    <div class="messages-wrapper">
      <SidekickMessages
        :messages="messages"
        :is-typing="isTyping"
        :current-thoughts="currentThoughts"
        :current-chunked-message="currentChunkedMessage"
        :streaming-animating="streamingAnimating"
      />
    </div>

    <!-- Input Area -->
    <div class="input-wrapper">
      <SidekickChatInput
        placeholder="Ask me anything..."
        :placeholders="['What can you help with?', 'Ask a question...']"
        :disabled="isLoading"
        @submit="handleSubmit"
      />
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import {
  SidekickMessages,
  SidekickChatInput,
  type ChatMessage
} from '@sprout_ai_labs/sidekick'

// State
const messages = ref<ChatMessage[]>([])
const isTyping = ref(false)
const currentThoughts = ref([])
const currentChunkedMessage = ref('')
const streamingAnimating = ref(false)
const isLoading = ref(false)

// Handle message submission
async function handleSubmit(message: string) {
  // Add user message
  messages.value.push({
    id: Date.now().toString(),
    sender: 'user',
    text: message
  })
  
  // Start streaming
  isTyping.value = true
  streamingAnimating.value = true
  isLoading.value = true
  
  try {
    // Your API call here
    await streamResponse(message)
  } finally {
    isTyping.value = false
    streamingAnimating.value = false
    isLoading.value = false
  }
}

async function streamResponse(userMessage: string) {
  // Implement your streaming logic here
  // Update currentThoughts and currentChunkedMessage as data arrives
  
  // Example:
  currentThoughts.value = [
    { message: 'Processing request', completed: true, event: 'start' },
    { message: 'Generating response', completed: false, event: 'process' }
  ]
  
  // Simulate streaming
  const response = 'This is a simulated response...'
  for (let i = 0; i < response.length; i++) {
    currentChunkedMessage.value += response[i]
    await new Promise(resolve => setTimeout(resolve, 30))
  }
  
  // Complete the message
  messages.value.push({
    id: Date.now().toString(),
    sender: 'bot',
    text: currentChunkedMessage.value,
    thoughtSteps: currentThoughts.value.map(t => ({ ...t, completed: true })),
    thinkingDuration: 3
  })
  
  // Reset streaming state
  currentThoughts.value = []
  currentChunkedMessage.value = ''
}
</script>

<style scoped>
.chat-interface {
  height: 100vh;
  display: flex;
  flex-direction: column;
}

.messages-wrapper {
  flex: 1;
  overflow-y: auto;
  background: #f9fafb;
  padding: 1rem;
}

.input-wrapper {
  border-top: 1px solid #e5e7eb;
  background: white;
  padding: 1rem;
}
</style>

Event Type Icons

The collapsible thought components automatically map event types to icons:

Event TypeIconDescription
tool_call🔧Tool/function calls
observation🔍Search results, observations
process⚙️Processing, thinking
route_to🔀Routing to agents
start▶️Starting actions
DefaultCompleted steps

Styling

All standalone components come pre-styled with the Sidekick design system. CSS classes use the scp- prefix to avoid conflicts:

css
/* Main containers */
.scp-messages
.scp-message-bubble
.scp-user-message-text
.scp-ai-message-text

/* Input */
.sidekick-chat-input-container
.sidekick-chat-input-editable

/* Thoughts */
.collapsible-thought
.thought-header
.thought-steps
.step-icon

Custom Styling

You can override styles by targeting these classes:

css
/* Example: Customize message bubble */
.scp-user-message-text {
  background: #your-brand-color !important;
}

/* Example: Customize input */
.sidekick-chat-input-editable {
  font-size: 16px;
  padding: 12px;
}

Best Practices

1. Message Management

typescript
// Generate unique IDs
const generateId = () => `msg-${Date.now()}-${Math.random()}`

// Add messages immutably
messages.value = [...messages.value, newMessage]

2. Streaming Handling

typescript
// Always reset streaming state after completion
function completeStreaming() {
  isTyping.value = false
  streamingAnimating.value = false
  currentThoughts.value = []
  currentChunkedMessage.value = ''
}

3. Error Handling

typescript
// Add error messages
messages.value.push({
  id: generateId(),
  sender: 'bot',
  text: 'Sorry, something went wrong.',
  isError: true
})

4. Performance

typescript
// Limit message history if needed
if (messages.value.length > 100) {
  messages.value = messages.value.slice(-50)
}

TypeScript Support

All standalone components are fully typed. Import types from the package:

typescript
import type {
  ChatMessage,
  ChatState,
  KBSource
} from '@sprout_ai_labs/sidekick'

Comparison: Unified vs Standalone

FeatureUnified ComponentsStandalone Components
StoreBuilt-in Pinia storeNo store, you manage state
SetupPlugin installDirect import
StateAutomaticManual via props
FlexibilityLessMore
Use CaseQuick setup, standard behaviorCustom state, existing apps

Choose Unified for quick setup with built-in state management.
Choose Standalone for maximum control and custom state management.

Released under the MIT License.