Modernizing Legacy Applications with AI Widgets
Legacy applications are the backbone of many organizations, containing decades of business logic and institutional knowledge. However, they often suffer from outdated user interfaces, limited accessibility, and poor user experience. Complete system rewrites are costly, risky, and time-consuming. What if you could modernize these applications incrementally by adding intelligent AI agents that provide a natural language interface to existing functionality?
In this tutorial, you'll learn how to use ChatBotKit AI Widgets to create a lightweight integration layer that modernizes legacy applications without touching the underlying codebase. This approach, sometimes called an "intelligent overlay," allows you to extend system lifespan while providing modern user experiences.
In This Tutorial
You'll Learn How To:
- Understand the intelligent overlay approach to legacy modernization
- Set up a ChatBotKit AI Widget as a conversational interface
- Use client-side functions to bridge the widget with legacy application functionality
- Connect to both client-side UI components and server-side APIs
- Create a seamless user experience that masks the complexity of legacy systems
- Implement this pattern without major infrastructure changes
By the end of this tutorial, you'll have a working example of an AI agent that can interact with a legacy application, demonstrating a practical approach to incremental modernization.
Why This Approach Works
The Legacy System Challenge
Enterprise organizations face a significant dilemma with their legacy systems:
- Valuable but Outdated: These systems contain critical business logic but lack modern UX
- High Maintenance Costs: According to industry research, approximately 74% of enterprise IT budgets are dedicated to maintaining existing systems rather than innovation
- Risk of Complete Replacement: Traditional replatforming or refactoring approaches require significant investment and carry substantial business risk
The Intelligent Overlay Solution
Instead of replacing or rewriting, you can:
- Preserve existing investments in legacy systems
- Add a modern AI interface that users interact with naturally
- Bridge functionality using lightweight JavaScript integration
- Minimize disruption to current operations
- Modernize incrementally by adding capabilities over time
ChatBotKit AI Widgets provide the perfect foundation for this approach because they:
- Embed easily with a single script tag
- Run entirely in the browser (no backend changes required)
- Support client-side functions to interact with existing code
- Require minimal technical overhead
Before You Begin
Prerequisites
- Active ChatBotKit account (sign up at chatbotkit.com)
- Access to your legacy application's codebase (at least the frontend HTML/JavaScript)
- Basic understanding of JavaScript and how your application works
- Ability to add a script tag to your application pages
What You'll Need
- 45-60 minutes of focused time
- A web browser with developer tools (Chrome or Firefox recommended)
- Text editor for modifying your application code
- Basic knowledge of your application's functionality and data structures
Understanding Your Legacy Application
Before starting, identify:
- Key user workflows you want to AI-enable
- Existing JavaScript functions or APIs available
- UI elements the AI might need to interact with
- Data access patterns (client-side state, localStorage, API calls)
- Security considerations for exposing functionality
Step 1: Create Your AI Widget
First, let's set up the ChatBotKit AI Widget that will serve as the intelligent interface to your legacy application.
Navigate to Widget Creation
- Log into your ChatBotKit account
- From the dashboard, click "Integrations" in the sidebar
- Click "Widget" from the integration types
- Click the "Create Widget" button
Configure the Widget
-
Name: Give it a descriptive name
App Assistant -
Description: Explain its purpose
AI agent that helps users interact with... -
Bot Configuration: Either create a new bot or select an existing one
Set Up the Bot's Backstory
This is crucial - your bot needs to understand its role as a bridge to your legacy system:
You are an intelligent assistant for our legacy enterprise application.
Key Responsibilities:
- Help users search for records and retrieve information
- Assist with creating, updating, and deleting data
- Guide users through complex multi-step processes
- Provide contextual help and documentation
- Translate user intent into appropriate system actions
Guidelines:
- Always confirm before destructive operations (update, delete)
- Provide clear explanations of what actions you're taking
- If you need more information, ask clarifying questions
- When showing results, format them in a readable way
- Help users understand the legacy system's limitations
Click "Create" or "Save" to create your widget
Get Your Widget ID
After creation, note your Widget ID - you'll need this for embedding. It looks like:
clchog8pw0001nwfirx0ymw3g
Step 2: Embed the Widget in Your Legacy Application
Now let's add the AI widget to your legacy application. This is surprisingly simple and non-invasive.
Add the Widget Script
In your legacy application's HTML, add this script tag before the closing </body> tag:
<!-- Add ChatBotKit AI Widget -->
<script src="<https://static.chatbotkit.com/integrations/widget/v2.js>"
data-widget="YOUR_WIDGET_ID">
</script>
Replace YOUR_WIDGET_ID with your actual widget ID.
Alternative: Manual Instantiation
For more control, you can initialize the widget manually:
<!-- Embed the Widget SDK -->
<script src="<https://static.chatbotkit.com/integrations/widget/v2.js>"></script>
<!-- Instantiate widget within your application -->
<chatbotkit-widget
widget="YOUR_WIDGET_ID"
/>
Step 3: Create Client-Side Functions
This is where the magic happens. Client-side functions allow your AI agent to interact with your legacy application's JavaScript code and APIs.
Understanding Client-Side Functions
Client-side functions are JavaScript functions you define that the AI can invoke to:
- Query application state (read current user data, preferences, etc.)
- Manipulate the UI (open modals, fill forms, navigate pages)
- Call legacy APIs (CRUD operations on your backend)
- Access browser storage (localStorage, sessionStorage, cookies)
- Interact with third-party libraries (jQuery, legacy frameworks, etc.)
Basic Function Structure
Each client-side function has:
- description: Explains what the function does (AI uses this to decide when to call it)
- parameters: JSON Schema defining expected input parameters
- handler or result: Either a function to execute or static data to return
Example 1: Read-Only Data Access
Let's start simple - allowing the AI to read current user information:
// Wait for widget to be ready
window.addEventListener('load', function() {
// Get the widget instance
const widgetInstance = window.chatbotkitWidget.instance;
// Define client-side functions
widgetInstance.functions = {
getCurrentUser: {
description: 'Get information about the currently logged-in user',
parameters: {},
result: {
data: {
userId: window.currentUser.id,
username: window.currentUser.name,
email: window.currentUser.email,
role: window.currentUser.role,
department: window.currentUser.department
}
}
}
};
});
Example 2: Search Legacy Database
Allow the AI to search your legacy database:
widgetInstance.functions = {
...widgetInstance.functions, // Keep existing functions
searchRecords: {
description: 'Search for records in the legacy database. Returns matching customer records.',
parameters: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query string to match against customer names, IDs, or emails'
},
limit: {
type: 'number',
description: 'Maximum number of results to return (default: 10)',
default: 10
}
},
required: ['query']
},
handler: async ({ query, limit = 10 }) => {
try {
const response = await fetch(`/api/legacy/search?q=${encodeURIComponent(query)}&limit=${limit}`);
const data = await response.json();
return {
success: true,
results: data.records,
count: data.records.length
};
} catch (error) {
return {
success: false,
error: 'Failed to search records: ' + error.message
};
}
}
}
};
Example 3: Update Legacy Data
Allow the AI to modify existing records (with proper validation):
widgetInstance.functions = {
...widgetInstance.functions,
updateRecord: {
description: 'Update an existing record in the legacy system. Use this after confirming with the user.',
parameters: {
type: 'object',
properties: {
recordId: {
type: 'string',
description: 'The unique identifier of the record to update'
},
fields: {
type: 'object',
description: 'Object containing field names and new values to update',
properties: {
name: { type: 'string' },
email: { type: 'string' },
phone: { type: 'string' },
status: { type: 'string', enum: ['active', 'inactive', 'pending'] }
}
}
},
required: ['recordId', 'fields']
},
handler: async ({ recordId, fields }) => {
try {
// validate the update
if (!recordId || !fields || Object.keys(fields).length === 0) {
throw new Error('Invalid update parameters');
}
// call your legacy update API
const response = await fetch(`/api/legacy/records/${recordId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify(fields)
});
if (!response.ok) {
throw new Error(`Update failed: ${response.statusText}`);
}
const result = await response.json();
return {
success: true,
message: 'Record updated successfully',
record: result
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
}
};
Example 4: Navigate Legacy UI
Help users navigate complex legacy interfaces:
widgetInstance.functions = {
...widgetInstance.functions,
navigateToPage: {
description: 'Navigate the user to a specific page or section in the legacy application',
parameters: {
type: 'object',
properties: {
pageName: {
type: 'string',
description: 'The page to navigate to',
enum: ['dashboard', 'customers', 'orders', 'reports', 'settings']
},
recordId: {
type: 'string',
description: 'Optional record ID to view a specific item'
}
},
required: ['pageName']
},
handler: async ({ pageName, recordId }) => {
// build the URL based on your legacy routing
let url = `/legacy/${pageName}`;
if (recordId) {
url += `/${recordId}`;
}
// navigate to the page
window.location.href = url;
return {
success: true,
message: `Navigating to ${pageName}...`
};
}
}
};
Example 5: Interact with Legacy UI Components
Manipulate existing UI elements (useful for filling forms):
widgetInstance.functions = {
...widgetInstance.functions,
fillCustomerForm: {
description: 'Pre-fill the customer creation form with provided data',
parameters: {
type: 'object',
properties: {
name: { type: 'string' },
email: { type: 'string' },
phone: { type: 'string' },
company: { type: 'string' }
}
},
handler: async ({ name, email, phone, company }) => {
try {
// find and fill form fields (adjust selectors for your legacy app)
if (name) document.getElementById('customer_name').value = name;
if (email) document.getElementById('customer_email').value = email;
if (phone) document.getElementById('customer_phone').value = phone;
if (company) document.getElementById('customer_company').value = company;
// trigger change events if needed for legacy validation
document.getElementById('customer_name').dispatchEvent(new Event('change'));
return {
success: true,
message: 'Form filled successfully'
};
} catch (error) {
return {
success: false,
error: 'Failed to fill form: ' + error.message
};
}
}
}
};
Step 4: Complete Integration Example
Here's a complete example showing how to set up the AI widget with multiple functions in a legacy application:
<!DOCTYPE html>
<html>
<head>
<title>Legacy Application</title>
<meta name="csrf-token" content="your-csrf-token">
<!-- Your existing legacy app CSS and scripts -->
</head>
<body>
<!-- Your existing legacy application HTML -->
<div id="legacy-app">
<!-- Legacy application content -->
</div>
<!-- ChatBotKit AI Widget Integration -->
<script src="<https://static.chatbotkit.com/integrations/widget/v2.js>"
data-widget="YOUR_WIDGET_ID">
</script>
<script>
// wait for both legacy app and widget to be ready
window.addEventListener('load', function() {
// ensure widget is loaded
if (!window.chatbotkitWidget || !window.chatbotkitWidget.instance) {
console.error('ChatBotKit widget not loaded');
return;
}
const widgetInstance = window.chatbotkitWidget.instance;
// define all client-side functions
widgetInstance.functions = {
// read current user information
getCurrentUser: {
description: 'Get information about the currently logged-in user',
parameters: {},
result: {
data: {
userId: window.currentUser?.id || 'unknown',
username: window.currentUser?.name || 'Guest',
email: window.currentUser?.email || '',
role: window.currentUser?.role || 'user'
}
}
},
// search records
searchRecords: {
description: 'Search for customer records in the database',
parameters: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query'
},
limit: {
type: 'number',
description: 'Maximum results to return',
default: 10
}
},
required: ['query']
},
handler: async ({ query, limit = 10 }) => {
try {
const response = await fetch(
`/api/legacy/search?q=${encodeURIComponent(query)}&limit=${limit}`
);
const data = await response.json();
return {
success: true,
results: data.records,
count: data.records.length
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
},
// fet record details
getRecordDetails: {
description: 'Retrieve detailed information about a specific record',
parameters: {
type: 'object',
properties: {
recordId: {
type: 'string',
description: 'The record ID to retrieve'
}
},
required: ['recordId']
},
handler: async ({ recordId }) => {
try {
const response = await fetch(`/api/legacy/records/${recordId}`);
const data = await response.json();
return {
success: true,
record: data
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
},
// update record
updateRecord: {
description: 'Update an existing record. Always confirm with user first.',
parameters: {
type: 'object',
properties: {
recordId: { type: 'string' },
fields: { type: 'object' }
},
required: ['recordId', 'fields']
},
handler: async ({ recordId, fields }) => {
try {
const response = await fetch(`/api/legacy/records/${recordId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify(fields)
});
if (!response.ok) throw new Error('Update failed');
const result = await response.json();
return {
success: true,
message: 'Record updated successfully',
record: result
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
},
// navigate to pages
navigateToPage: {
description: 'Navigate to a specific page in the application',
parameters: {
type: 'object',
properties: {
pageName: {
type: 'string',
enum: ['dashboard', 'customers', 'orders', 'reports']
},
recordId: { type: 'string' }
},
required: ['pageName']
},
handler: async ({ pageName, recordId }) => {
let url = `/legacy/${pageName}`;
if (recordId) url += `/${recordId}`;
window.location.href = url;
return {
success: true,
message: `Navigating to ${pageName}...`
};
}
}
};
console.log('ChatBotKit AI Widget functions registered successfully');
});
</script>
</body>
</html>
Step 5: Test Your AI-Enabled Legacy Application
Now it's time to test the integration and see your AI agent interact with your legacy system.
Initial Testing
- Open your legacy application in a web browser
- Look for the widget button (usually bottom-right corner)
- Click to open the chat interface
Test Basic Interactions
Try these example conversations:
Example 1: Get User Information
User: Who am I logged in as?
AI: Let me check... You're logged in as John Smith (john.smith@company.com) with the role of Manager in the Sales department.
Example 2: Search for Records
User: Find all customers named Smith
AI: I found 3 customers matching "Smith":
1. John Smith - john@example.com - Active
2. Jane Smith - jane@example.com - Active
3. Bob Smith - bob@example.com - Inactive
Would you like details on any of these?
Example 3: Update a Record
User: Update customer 12345's email to newemail@example.com
AI: Just to confirm, you want to update customer 12345's email address to newemail@example.com. Is that correct?
User: Yes
AI: Record updated successfully! Customer 12345 now has the email address newemail@example.com.
Example 4: Navigate the App
User: Take me to the reports page
AI: Navigating to the reports page now...
[User is redirected to /legacy/reports]
Advanced Testing
Test error handling and edge cases:
- Invalid input: "Update customer xyz" (should ask for clarification)
- Permissions: Try actions the current user shouldn't be able to perform
- Network errors: Test with network throttling enabled
- Empty results: Search for non-existent records
Step 6: Enhance the User Experience
Now that the basic integration works, let's make it more sophisticated.
Add Contextual Help
Extend your bot's backstory to provide guidance:
When users seem confused or ask "how do I...", provide step-by-step guidance:
Example workflows:
- To create a new customer: Navigate to the customers page, click "Add New", fill in required fields (name, email), and click "Save"
- To generate a report: Go to the reports page, select date range, choose report type, and click "Generate"
If a user is struggling with a task, offer to help them complete it through our conversation instead.
Implement Confirmation Patterns
For destructive operations, always confirm:
deleteRecord: {
description: 'Delete a record. MUST confirm with user first by asking "Are you sure you want to delete record [ID]? This cannot be undone."',
parameters: {
type: 'object',
properties: {
recordId: { type: 'string' },
confirmed: {
type: 'boolean',
description: 'Set to true only after user explicitly confirms'
}
},
required: ['recordId', 'confirmed']
},
handler: async ({ recordId, confirmed }) => {
if (!confirmed) {
return {
success: false,
error: 'Operation requires user confirmation'
};
}
// proceed with deletion...
}
}
Add Loading States
Provide feedback during long operations:
handler: async ({ query }) => {
// send immediate feedback
widgetInstance.sendMessage({
text: 'Searching records...',
hidden: true // don't show in conversation history
});
const results = await searchDatabase(query);
return {
success: true,
results: results
};
}
Track Analytics
Monitor how users interact with the AI:
handler: async (params) => {
// log usage
try {
await fetch('/api/analytics/ai-usage', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
function: 'searchRecords',
params: params,
timestamp: new Date().toISOString(),
userId: window.currentUser?.id
})
});
} catch (e) {
// don't fail the actual operation if analytics fails
console.warn('Analytics logging failed:', e);
}
// proceed with actual function logic
// ...
}
Best Practices and Considerations
Security
- Never expose sensitive functions without proper authentication checks
- Validate all inputs on the server-side, not just in client functions
- Use CSRF tokens for state-changing operations
- Implement rate limiting to prevent abuse
- Log all AI-triggered actions for audit trails
handler: async ({ recordId, fields }) => {
// validate authentication
if (!window.currentUser?.authenticated) {
return {
success: false,
error: 'Authentication required'
};
}
// check permissions
if (!window.currentUser.hasPermission('update_records')) {
return {
success: false,
error: 'Insufficient permissions'
};
}
// validate input
if (!recordId || typeof recordId !== 'string') {
return {
success: false,
error: 'Invalid record ID'
};
}
// proceed with operation...
}
Error Handling
Provide helpful error messages:
handler: async (params) => {
try {
// operation logic
const result = await performOperation(params);
return { success: true, data: result };
} catch (error) {
// log error for debugging
console.error('Function error:', error);
// return user-friendly message
if (error.message.includes('network')) {
return {
success: false,
error: 'Network connection issue. Please check your connection and try again.'
};
} else if (error.message.includes('permission')) {
return {
success: false,
error: 'You don\\'t have permission to perform this action. Please contact your administrator.'
};
} else {
return {
success: false,
error: 'An unexpected error occurred. Please try again or contact support.'
};
}
}
}
Gradual Migration Strategy
Don't try to AI-enable everything at once:
Phase 1: Read-Only Functions
- User information retrieval
- Search and browse functionality
- Reporting and analytics viewing
Phase 2: Guided Navigation
- Help users find features
- Provide contextual documentation
- Navigate to specific pages/sections
Phase 3: Data Modification
- Simple updates (status changes, etc.)
- Form pre-filling
- Bulk operations with confirmation
Phase 4: Complex Workflows
- Multi-step processes
- Conditional logic
- Integration with external systems
Troubleshooting
Widget Not Appearing
Problem: The chat widget doesn't show up on the page.
Solutions:
- Check browser console for JavaScript errors
- Verify the widget ID is correct
- Ensure the script tag is properly placed
- Check if your legacy app's Content Security Policy blocks the widget
- Try loading the page in an incognito window (rule out extensions)
// debug widget loading
console.log('ChatBotKit widget global:', window.chatbotkitWidget);
console.log('Widget instance:', window.chatbotkitWidget?.instance);
Functions Not Being Called
Problem: The AI doesn't invoke your client-side functions.
Solutions:
- Check function descriptions are clear and specific
- Verify parameters schema is correct
- Ensure functions are registered before user interactions
- Update bot backstory to mention available functions explicitly
- Test with direct function calls in browser console
// test function directly
window.chatbotkitWidget.instance.functions.searchRecords.handler({
query: 'test',
limit: 5
}).then(result => console.log('Function result:', result));
API Calls Failing
Problem: Legacy API calls from functions fail.
Solutions:
- Check CORS settings on your legacy backend
- Verify authentication tokens are being sent
- Ensure API endpoints are accessible from the browser
- Check for CSRF token requirements
- Look for SSL/HTTPS issues
// debug API calls
handler: async (params) => {
console.log('Calling API with params:', params);
try {
const response = await fetch('/api/legacy/endpoint', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(params)
});
console.log('API response status:', response.status);
console.log('API response headers:', response.headers);
const data = await response.json();
console.log('API response data:', data);
return { success: true, data };
} catch (error) {
console.error('API call failed:', error);
return { success: false, error: error.message };
}
}
AI Misunderstanding User Intent
Problem: The AI calls wrong functions or misinterprets requests.
Solutions:
- Make function descriptions more specific
- Add examples to your bot's backstory
- Use parameter descriptions to guide the AI
- Consider adding a validation step before executing
- Refine the bot's backstory with clearer instructions
Next Steps
Congratulations! You've successfully created an AI overlay for your legacy application. Here's what to explore next:
Immediate Next Steps
- Expand function coverage: Add more client-side functions for additional features
- Refine bot behavior: Update the backstory based on user interactions
- Add analytics: Track which functions are most used
- Gather user feedback: Survey users about their AI experience
Advanced Capabilities
- Multi-language support: Enable the widget in multiple languages
- Voice interaction: Add voice input/output for hands-free operation
- Mobile optimization: Ensure the widget works well on mobile devices
- Integration with other tools: Connect to Slack, Teams, or other platforms
Learning Resources
- ChatBotKit Widget Documentation
- Client-Side Functions Guide
- Widget SDK Reference
- Join our Discord community for support
Conclusion
Modernizing legacy applications doesn't have to mean complete rewrites or massive infrastructure changes. By using ChatBotKit AI Widgets with client-side functions, you can create an intelligent overlay that:
- Improves user experience dramatically without touching legacy code
- Reduces training costs by providing natural language interfaces
- Extends system lifespan while planning long-term modernization
- Minimizes risk through incremental, reversible changes
- Delivers quick wins that demonstrate value to stakeholders
This approach works because it meets users where they are - providing a modern, conversational interface while leveraging the robust business logic already present in your legacy systems.
Start small, prove value, then expand. Your legacy applications may be old, but with AI, they can learn new tricks.
Frequently Asked Questions
Does this approach work with very old technologies (e.g., Java applets, Flash)?
Yes, as long as you can add JavaScript to the page. The AI widget communicates through JavaScript, so even if the legacy app uses outdated tech, you can still create functions that interact with it.
What if my legacy application is desktop-based (not web)?
You have a few options:
- Embed the widget frame in a WebView within the desktop app
- Create a companion web interface with the AI widget
- Use the ChatBotKit SDK to build a native chat interface
How do I handle authentication and session management?
The widget runs in the browser with the user's existing session. Your client-side functions can access the same cookies and tokens the legacy app uses. For additional security, implement server-side proxy endpoints.
Can multiple users interact with the AI simultaneously?
Yes, each user gets their own widget instance with their own session. The AI can access user-specific data through client-side functions.
What about GDPR and data privacy?
ChatBotKit is GDPR compliant. You control what data the AI accesses through your function definitions. See our privacy features guide.
How much does this cost?
ChatBotKit offers flexible pricing based on usage. You can start with a trial to test this approach.
What if my legacy system's API is slow?
Implement caching in your client-side functions, show loading states to users, and consider running heavy operations asynchronously with notifications when complete.