| layout | default |
|---|---|
| title | Chapter 4: Conversation Management |
| parent | Chatbox Tutorial |
| nav_order | 4 |
Welcome to Chapter 4: Conversation Management. In this part of Chatbox Tutorial: Building Modern AI Chat Interfaces, you will build an intuitive mental model first, then move into concrete implementation details and practical production tradeoffs.
This chapter covers managing chat conversations, including history, context, and multi-conversation workflows.
// Comprehensive conversation model
interface Conversation {
id: string
title: string
createdAt: Date
updatedAt: Date
messages: Message[]
metadata: {
provider: string
model: string
totalTokens: number
estimatedCost: number
tags: string[]
isArchived: boolean
isFavorite: boolean
}
context: {
summary: string
keyTopics: string[]
participants: string[]
sentiment: 'positive' | 'neutral' | 'negative'
}
}
interface Message {
id: string
role: 'user' | 'assistant' | 'system'
content: string
timestamp: Date
metadata: {
tokens: number
model: string
processingTime: number
attachments?: Attachment[]
}
}// Central conversation management
class ConversationManager {
private conversations: Map<string, Conversation> = new Map()
private activeConversation: string | null = null
async createConversation(title?: string): Promise<string> {
const id = this.generateId()
const conversation: Conversation = {
id,
title: title || 'New Conversation',
createdAt: new Date(),
updatedAt: new Date(),
messages: [],
metadata: {
provider: 'openai',
model: 'gpt-3.5-turbo',
totalTokens: 0,
estimatedCost: 0,
tags: [],
isArchived: false,
isFavorite: false
},
context: {
summary: '',
keyTopics: [],
participants: ['user'],
sentiment: 'neutral'
}
}
this.conversations.set(id, conversation)
this.activeConversation = id
await this.persistConversation(conversation)
return id
}
async addMessage(conversationId: string, message: Omit<Message, 'id' | 'timestamp'>): Promise<void> {
const conversation = this.conversations.get(conversationId)
if (!conversation) {
throw new Error('Conversation not found')
}
const fullMessage: Message = {
...message,
id: this.generateId(),
timestamp: new Date()
}
conversation.messages.push(fullMessage)
conversation.updatedAt = new Date()
// Update conversation metadata
this.updateConversationMetadata(conversation, fullMessage)
await this.persistConversation(conversation)
}
async getConversation(conversationId: string): Promise<Conversation | null> {
return this.conversations.get(conversationId) || null
}
async listConversations(filters?: ConversationFilters): Promise<Conversation[]> {
let conversations = Array.from(this.conversations.values())
if (filters) {
conversations = this.applyFilters(conversations, filters)
}
return conversations.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime())
}
private updateConversationMetadata(conversation: Conversation, message: Message) {
conversation.metadata.totalTokens += message.metadata.tokens || 0
conversation.metadata.estimatedCost = this.calculateCost(conversation)
// Update title if it's generic and we have content
if (conversation.title === 'New Conversation' && conversation.messages.length > 1) {
conversation.title = this.generateTitle(conversation)
}
// Update context
conversation.context = this.updateContext(conversation, message)
}
private generateTitle(conversation: Conversation): string {
const firstUserMessage = conversation.messages.find(m => m.role === 'user')
if (firstUserMessage) {
return firstUserMessage.content.slice(0, 50) + (firstUserMessage.content.length > 50 ? '...' : '')
}
return 'Conversation'
}
private updateContext(conversation: Conversation, message: Message): Conversation['context'] {
// Extract key topics from messages
const topics = this.extractTopics(conversation.messages)
// Generate conversation summary
const summary = this.generateSummary(conversation.messages)
// Analyze sentiment
const sentiment = this.analyzeSentiment(conversation.messages)
return {
summary,
keyTopics: topics,
participants: ['user', 'assistant'],
sentiment
}
}
}// Efficient message storage and retrieval
class MessageStore {
private storage: MessageStorage
private cache: Map<string, Message[]> = new Map()
async storeMessages(conversationId: string, messages: Message[]): Promise<void> {
await this.storage.store(conversationId, messages)
this.cache.set(conversationId, messages)
}
async getMessages(conversationId: string, options?: MessageQueryOptions): Promise<Message[]> {
// Check cache first
if (this.cache.has(conversationId)) {
return this.applyOptions(this.cache.get(conversationId)!, options)
}
// Load from storage
const messages = await this.storage.retrieve(conversationId)
this.cache.set(conversationId, messages)
return this.applyOptions(messages, options)
}
async searchMessages(conversationId: string, query: string): Promise<Message[]> {
const messages = await this.getMessages(conversationId)
const queryTerms = query.toLowerCase().split(' ')
return messages.filter(message =>
queryTerms.some(term =>
message.content.toLowerCase().includes(term)
)
)
}
private applyOptions(messages: Message[], options?: MessageQueryOptions): Message[] {
if (!options) return messages
let result = messages
// Apply limit
if (options.limit) {
result = result.slice(-options.limit)
}
// Apply date range
if (options.startDate) {
result = result.filter(m => m.timestamp >= options.startDate!)
}
if (options.endDate) {
result = result.filter(m => m.timestamp <= options.endDate!)
}
// Apply role filter
if (options.role) {
result = result.filter(m => m.role === options.role)
}
return result
}
}// Advanced conversation search
class ConversationSearch {
private conversations: Conversation[]
private searchIndex: SearchIndex
async search(query: string, filters?: SearchFilters): Promise<SearchResult[]> {
// Search across all conversations
const results = await this.searchIndex.search(query)
// Apply filters
let filteredResults = this.applyFilters(results, filters)
// Sort by relevance
filteredResults.sort((a, b) => b.score - a.score)
return filteredResults
}
async searchWithinConversation(conversationId: string, query: string): Promise<Message[]> {
const conversation = await this.conversationManager.getConversation(conversationId)
if (!conversation) return []
return conversation.messages.filter(message =>
message.content.toLowerCase().includes(query.toLowerCase())
)
}
private applyFilters(results: SearchResult[], filters?: SearchFilters): SearchResult[] {
if (!filters) return results
return results.filter(result => {
// Date range filter
if (filters.startDate && result.timestamp < filters.startDate) return false
if (filters.endDate && result.timestamp > filters.endDate) return false
// Conversation filter
if (filters.conversationId && result.conversationId !== filters.conversationId) return false
// Provider filter
if (filters.provider && result.provider !== filters.provider) return false
return true
})
}
async buildSearchIndex(): Promise<void> {
const conversations = await this.conversationManager.listConversations()
for (const conversation of conversations) {
for (const message of conversation.messages) {
await this.searchIndex.index({
id: message.id,
content: message.content,
conversationId: conversation.id,
timestamp: message.timestamp,
provider: conversation.metadata.provider,
score: 1.0
})
}
}
}
}// Intelligent context handling
class ContextManager {
private contextWindow = 10
private maxTokens = 4000
buildContext(conversation: Conversation, currentMessage?: string): Context {
// Get recent messages
const recentMessages = conversation.messages.slice(-this.contextWindow)
// Build context string
const contextMessages = recentMessages.map(msg => ({
role: msg.role,
content: msg.content
}))
// Add current message if provided
if (currentMessage) {
contextMessages.push({
role: 'user',
content: currentMessage
})
}
// Estimate token count
const tokenCount = this.estimateTokens(contextMessages)
// Trim if necessary
if (tokenCount > this.maxTokens) {
contextMessages.splice(0, Math.floor(contextMessages.length * 0.2))
}
return {
messages: contextMessages,
tokenCount: this.estimateTokens(contextMessages),
summary: this.generateSummary(conversation),
metadata: {
conversationId: conversation.id,
messageCount: contextMessages.length,
timeRange: this.getTimeRange(recentMessages)
}
}
}
private estimateTokens(messages: any[]): number {
// Rough estimation: 1 token ≈ 4 characters
return messages.reduce((total, msg) =>
total + Math.ceil(msg.content.length / 4), 0
)
}
private generateSummary(conversation: Conversation): string {
if (conversation.messages.length === 0) return ''
const firstMessage = conversation.messages[0].content
const recentMessages = conversation.messages.slice(-3)
return `Conversation started with: "${firstMessage.slice(0, 100)}..."`
}
private getTimeRange(messages: Message[]): { start: Date; end: Date } {
if (messages.length === 0) {
const now = new Date()
return { start: now, end: now }
}
return {
start: messages[0].timestamp,
end: messages[messages.length - 1].timestamp
}
}
}// Conversation usage analytics
class ConversationAnalytics {
private stats: Map<string, ConversationStats> = new Map()
trackConversationActivity(conversationId: string, activity: ConversationActivity) {
const stats = this.stats.get(conversationId) || this.initializeStats()
switch (activity.type) {
case 'message_sent':
stats.totalMessages++
stats.lastActivity = new Date()
break
case 'message_received':
stats.totalMessages++
stats.averageResponseTime =
(stats.averageResponseTime + activity.responseTime) / 2
break
case 'search_performed':
stats.totalSearches++
break
}
this.stats.set(conversationId, stats)
}
getConversationStats(conversationId: string): ConversationStats | null {
return this.stats.get(conversationId) || null
}
getGlobalStats(): GlobalStats {
const allStats = Array.from(this.stats.values())
return {
totalConversations: allStats.length,
totalMessages: allStats.reduce((sum, stats) => sum + stats.totalMessages, 0),
averageMessagesPerConversation: allStats.reduce((sum, stats) => sum + stats.totalMessages, 0) / allStats.length,
mostActiveConversation: this.findMostActiveConversation()
}
}
private initializeStats(): ConversationStats {
return {
totalMessages: 0,
totalSearches: 0,
averageResponseTime: 0,
createdAt: new Date(),
lastActivity: new Date()
}
}
private findMostActiveConversation(): string {
let mostActive = ''
let maxMessages = 0
for (const [id, stats] of this.stats) {
if (stats.totalMessages > maxMessages) {
maxMessages = stats.totalMessages
mostActive = id
}
}
return mostActive
}
}// Conversation import/export functionality
class ConversationImportExport {
async exportConversation(conversationId: string, format: 'json' | 'markdown' = 'json'): Promise<string> {
const conversation = await this.conversationManager.getConversation(conversationId)
if (!conversation) {
throw new Error('Conversation not found')
}
switch (format) {
case 'json':
return JSON.stringify(conversation, null, 2)
case 'markdown':
return this.convertToMarkdown(conversation)
default:
throw new Error(`Unsupported format: ${format}`)
}
}
async importConversation(data: string, format: 'json' | 'markdown' = 'json'): Promise<string> {
let conversation: Conversation
switch (format) {
case 'json':
conversation = JSON.parse(data)
break
case 'markdown':
conversation = this.parseFromMarkdown(data)
break
default:
throw new Error(`Unsupported format: ${format}`)
}
// Validate and clean imported data
conversation.id = this.generateId()
conversation.createdAt = new Date()
conversation.updatedAt = new Date()
await this.conversationManager.saveConversation(conversation)
return conversation.id
}
private convertToMarkdown(conversation: Conversation): string {
let markdown = `# ${conversation.title}\n\n`
markdown += `Created: ${conversation.createdAt.toISOString()}\n\n`
for (const message of conversation.messages) {
const role = message.role === 'user' ? 'User' : 'Assistant'
markdown += `**${role}:** ${message.content}\n\n`
}
return markdown
}
private parseFromMarkdown(markdown: string): Conversation {
const lines = markdown.split('\n')
const title = lines[0].replace('# ', '')
const messages: Message[] = []
let currentRole: 'user' | 'assistant' | null = null
let currentContent = ''
for (let i = 2; i < lines.length; i++) {
const line = lines[i]
if (line.startsWith('**User:**')) {
if (currentRole) {
messages.push(this.createMessage(currentRole, currentContent.trim()))
}
currentRole = 'user'
currentContent = line.replace('**User:**', '').trim()
} else if (line.startsWith('**Assistant:**')) {
if (currentRole) {
messages.push(this.createMessage(currentRole, currentContent.trim()))
}
currentRole = 'assistant'
currentContent = line.replace('**Assistant:**', '').trim()
} else if (line.trim() === '') {
// Empty line, process current message
if (currentRole && currentContent) {
messages.push(this.createMessage(currentRole, currentContent.trim()))
currentRole = null
currentContent = ''
}
} else if (currentRole) {
currentContent += ' ' + line.trim()
}
}
// Add final message
if (currentRole && currentContent) {
messages.push(this.createMessage(currentRole, currentContent.trim()))
}
return {
id: '',
title,
createdAt: new Date(),
updatedAt: new Date(),
messages,
metadata: {
provider: 'imported',
model: 'unknown',
totalTokens: 0,
estimatedCost: 0,
tags: ['imported'],
isArchived: false,
isFavorite: false
},
context: {
summary: `Imported conversation: ${title}`,
keyTopics: [],
participants: ['user', 'assistant'],
sentiment: 'neutral'
}
}
}
private createMessage(role: 'user' | 'assistant', content: string): Message {
return {
id: this.generateId(),
role,
content,
timestamp: new Date(),
metadata: {
tokens: Math.ceil(content.length / 4),
model: 'imported',
processingTime: 0
}
}
}
}- ✅ Built comprehensive conversation architecture
- ✅ Implemented efficient message storage
- ✅ Created advanced conversation search
- ✅ Developed intelligent context management
- ✅ Added conversation analytics
- ✅ Built import/export functionality
Key Takeaways:
- Conversation management requires careful data structure design
- Efficient storage and retrieval are crucial for performance
- Context management enables coherent conversations
- Search capabilities enhance user experience
- Analytics provide insights for improvement
- Import/export enables data portability
Most teams struggle here because the hard part is not writing more code, but deciding clear boundaries for conversation, messages, conversationId so behavior stays predictable as complexity grows.
In practical terms, this chapter helps you avoid three common failures:
- coupling core logic too tightly to one implementation path
- missing the handoff boundaries between setup, execution, and validation
- shipping changes without clear rollback or observability strategy
After working through this chapter, you should be able to reason about Chapter 4: Conversation Management as an operating subsystem inside Chatbox Tutorial: Building Modern AI Chat Interfaces, with explicit contracts for inputs, state transitions, and outputs.
Use the implementation notes around Conversation, message, stats as your checklist when adapting these patterns to your own repository.
Under the hood, Chapter 4: Conversation Management usually follows a repeatable control path:
- Context bootstrap: initialize runtime config and prerequisites for
conversation. - Input normalization: shape incoming data so
messagesreceives stable contracts. - Core execution: run the main logic branch and propagate intermediate state through
conversationId. - Policy and safety checks: enforce limits, auth scopes, and failure boundaries.
- Output composition: return canonical result payloads for downstream consumers.
- Operational telemetry: emit logs/metrics needed for debugging and performance tuning.
When debugging, walk this sequence in order and confirm each stage has explicit success/failure conditions.
Use the following upstream sources to verify implementation details while reading this chapter:
- View Repo
Why it matters: authoritative reference on
View Repo(github.com).
Suggested trace strategy:
- search upstream code for
conversationandmessagesto map concrete implementation paths - compare docs claims against actual runtime/config code before reusing patterns in production