Sometimes you just want to get a thing done and move on.
Overview
In this guide we will cover:
Creating our required folder structure
Copying and pasting code from the included Code Appendix into new files in your folder structure
Creating a unique user for dotAI with permissions
Creating the search widget
Placing the search widget on your pages
Testing your search
Requirements and prerequisites
We assume the following:
You are using dotCMS version 24.04.05 or later
If you aren’t on a current version of dotCMS and want to stop worrying about it, consider upgrading your instance to Evergreen and always have access to the latest tools (and fun!)
You are using the dotCMS Enterprise Edition
Your dotAI App has already been installed and configured
For more information on this step, review “Building Your Corner of the Web: Discovering dotAI”
Your account has correct permissions to:
Access dotAI
Administer backend settings
Create and publish content
You are on a single-site implementation
If you have multiple sites under your environment, some adjustments to the API call URLs will need to be made
You know little to nothing about software development or writing code
Instructions
Step 1: Create a secure API token for dotAI
Navigate to System → Users
Create a new user (+ upper right hand corner)
Give it a name, and email address (doesn’t need to be real)
Save
Click on the new user in the list on the left then click the ‘Roles’ tab
Grant the ‘Front-end User’ role
We want to limit the access this token has in the system as much as possible
Save the Roles
Click on the ‘API Access Tokens’ tab
Click ‘Request New Token’
Give it a label or simply click ‘Ok’
Copy all the text that appears in the popup box
(paste it in a temp text file if you need to, but remember to delete it when you are finished)
Close the box
Step 2: Configure the API VTL folder structure
Navigate to or create the following folder structure under your Site Browser
application → apivtl → ai
In the upper right corner click the + icon and select ‘Image or File’
In the dropdown menu, select File → Select
In the center box, select ‘<> Create New File’
Name the file get.vtl
Paste the following .vtl code into the file
$request.getSession() #set($aiKey = 'PUT THE API KEY WE JUST COPIED HERE, MAKE SURE YOU STILL HAVE THE SINGLE QUOTES AROUND IT') $dotJSON.put('key', $aiKey)Publish the file
Step 3: Create and publish the dotAI search widget VTL
Implementing the Widget
Navigate to or create the following folder structure under your Site Browser
application → vtl → widgets
In the upper right corner click the + icon and select ‘Image or File’
In the dropdown menu, select File → Select
In the center box, select ‘<> Create New File’
Name the file dotAI-search-widget.vtl
Go to the Code Appendix and copy all the code contained in the box
This is a self-contained widget - you won’t need to edit or change any of the code inside this file in order for it to appear and work in your environment
Save the changes
Publish your new file
Step 4: Place the search widget on a page
Navigate to or create the page you would like to display your search results on
On the right side pane, scroll down until you see the ‘VTL File’ control
If you don’t see it, make sure it is available to the container type you have on your page under ‘Layout’
Drag the control to the appropriate container on your page
This will open a new dialogue
Fill in the Title with the name for your new widget
Select ‘Browse’ next to the ‘VTL File’ textbox and navigate to the folder and file we created in the previous steps
Select ‘Publish’
**Important Note: This search uses the ‘default’ index (a grouping of content types for the AI to utilize). If you would like to change which dotAI index your search uses, follow these instructions.
Congratulations! Your new search bar should appear!
Step 5: Test your search
Open the page in a new tab and try out your search.
Changing the dotAI search index:
During the setup of dotAI in the configuration tools, your administrator may have defined specific content types the AI will use when creating search results.
In order to update your dotAI-search-widgit.vtl file to use these specific content types, follow these steps:
Locate the name of the index you would like your dotAI search widget to use
Navigate to Dev Tools → dotAI → Manage Embeddings/Indexes
Note the name of the index you would like to use in the left hand column
Note: You can verify the list of content types associated with any index by hovering over the index name
Navigate to and open ‘dotAI-search-widgit.vtl’
Ctrl + F to find the phrase indexName: 'default'
Replace the word default with the name of your index
There are two instances of the word default - verify you have changed both
Verify the ‘ ‘ around the name of your index has not been removed
Publish your .vtl file
Run a search to test the correct information is being presented
Code Appendix
<style>
:root {
--dotai-primary-color: #3b82f6;
--dotai-primary-dark: #1e40af;
--dotai-success-color: #10b981;
--dotai-error-color: #dc2626;
--dotai-text-color: #374151;
--dotai-text-muted: #6b7280;
--dotai-text-light: #9ca3af;
--dotai-bg-white: #ffffff;
--dotai-bg-gray-50: #f9fafb;
--dotai-bg-gray-100: #f3f4f6;
--dotai-bg-gray-200: #e5e7eb;
--dotai-bg-blue-50: #f0f9ff;
--dotai-bg-red-50: #fef2f2;
--dotai-border-color: #e5e7eb;
--dotai-border-focus: var(--dotai-primary-color);
--dotai-radius: 0.5rem;
--dotai-radius-sm: 0.25rem;
--dotai-shadow-focus: 0 0 0 0.2rem rgba(59, 130, 246, 0.25);
--dotai-transition: all 0.2s ease;
}
.dotai-search-widget {
max-width: 800px;
margin: 2rem auto;
padding: 2rem;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
box-sizing: border-box;
}
.dotai-search-widget *, .dotai-search-widget *::before, .dotai-search-widget *::after {
box-sizing: border-box;
}
.dotai-search-widget .dotai-search-form { margin-bottom: 1rem; }
.dotai-search-widget .dotai-search-input-group { position: relative; }
.dotai-search-widget .dotai-search-input {
width: 100%;
padding: 0.75rem 80px 0.75rem 1rem;
border: 2px solid var(--dotai-border-color);
border-radius: var(--dotai-radius);
font-size: 1rem;
transition: var(--dotai-transition);
background-color: var(--dotai-bg-white);
color: var(--dotai-text-color);
}
.dotai-search-widget .dotai-search-input:focus {
outline: none;
border-color: var(--dotai-border-focus);
box-shadow: var(--dotai-shadow-focus);
}
.dotai-search-widget .dotai-search-icons {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
display: flex;
gap: 8px;
z-index: 10;
}
.dotai-search-widget .dotai-search-icon, .dotai-search-widget .dotai-clear-icon {
width: 32px;
height: 32px;
border: none;
background: none;
color: var(--dotai-text-muted);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--dotai-radius-sm);
transition: var(--dotai-transition);
font-size: 16px;
}
.dotai-search-widget .dotai-search-icon:hover {
color: var(--dotai-primary-color);
background-color: var(--dotai-bg-gray-100);
}
.dotai-search-widget .dotai-clear-icon {
opacity: 0;
visibility: hidden;
transition: opacity 0.2s, visibility 0.2s;
}
.dotai-search-widget .dotai-clear-icon.show {
opacity: 1;
visibility: visible;
}
.dotai-search-widget .dotai-clear-icon:hover {
color: var(--dotai-error-color);
background-color: var(--dotai-bg-red-50);
}
.dotai-search-widget .dotai-search-icon:disabled {
color: var(--dotai-text-light);
cursor: not-allowed;
}
.dotai-search-widget .dotai-search-options {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.dotai-search-widget .dotai-option-group {
display: flex;
align-items: center;
gap: 0.5rem;
}
.dotai-search-widget .dotai-option-group label {
font-weight: 500;
color: var(--dotai-text-color);
margin: 0;
}
.dotai-search-widget .dotai-option-group input[type="radio"] {
margin: 0 0.25rem 0 0;
}
.dotai-search-widget .dotai-error-message {
color: var(--dotai-error-color);
background-color: var(--dotai-bg-red-50);
padding: 0.75rem;
border-radius: var(--dotai-radius);
margin-bottom: 1rem;
display: none;
border: 1px solid #fecaca;
}
.dotai-search-widget .dotai-loading {
display: none;
text-align: center;
padding: 2rem;
}
.dotai-search-widget .dotai-spinner {
border: 3px solid var(--dotai-bg-gray-100);
border-top: 3px solid var(--dotai-primary-color);
border-radius: 50%;
width: 2rem;
height: 2rem;
animation: dotai-spin 1s linear infinite;
margin: 0 auto 1rem;
}
@keyframes dotai-spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
.dotai-search-widget .dotai-results-container { margin-top: 2rem; }
.dotai-search-widget .dotai-result-item {
background-color: var(--dotai-bg-gray-50);
border: 1px solid var(--dotai-border-color);
border-radius: var(--dotai-radius);
padding: 1rem;
margin-bottom: 1rem;
}
.dotai-search-widget .dotai-result-title {
font-size: 1.125rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.dotai-search-widget .dotai-result-title a {
color: var(--dotai-primary-dark);
text-decoration: none;
}
.dotai-search-widget .dotai-result-title a:hover { text-decoration: underline; }
.dotai-search-widget .dotai-result-content {
color: #4b5563;
margin-bottom: 0.5rem;
line-height: 1.5;
}
.dotai-search-widget .dotai-result-meta {
display: flex;
gap: 1rem;
font-size: 0.875rem;
color: var(--dotai-text-muted);
flex-wrap: wrap;
}
.dotai-search-widget .dotai-result-meta span {
background-color: var(--dotai-bg-gray-200);
padding: 0.25rem 0.5rem;
border-radius: var(--dotai-radius-sm);
}
.dotai-search-widget .dotai-ai-response {
background-color: var(--dotai-bg-blue-50);
border-left: 4px solid var(--dotai-primary-color);
padding: 1rem;
margin-bottom: 2rem;
border-radius: var(--dotai-radius);
}
.dotai-search-widget .dotai-ai-response h3 { margin-top: 0; color: var(--dotai-primary-dark); }
.dotai-search-widget .dotai-pagination {
margin-top: 2rem;
display: flex;
justify-content: center;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
}
.dotai-search-widget .dotai-pagination-btn {
padding: 0.5rem 1rem;
border: 1px solid #d1d5db;
background: var(--dotai-bg-white);
color: var(--dotai-text-color);
border-radius: 0.375rem;
cursor: pointer;
font-size: 0.875rem;
transition: var(--dotai-transition);
}
.dotai-search-widget .dotai-pagination-btn:hover {
background-color: var(--dotai-bg-gray-100);
border-color: var(--dotai-text-light);
}
.dotai-search-widget .dotai-pagination-btn:active {
background-color: var(--dotai-bg-gray-200);
}
.dotai-search-widget .dotai-pagination-current {
padding: 0.5rem 0.75rem;
border: 1px solid var(--dotai-primary-color);
background: var(--dotai-primary-color);
color: var(--dotai-bg-white);
border-radius: 0.375rem;
font-size: 0.875rem;
font-weight: 600;
}
.dotai-search-widget .dotai-pagination-btn-disabled {
padding: 0.5rem 1rem;
border: 1px solid #d1d5db;
background: var(--dotai-bg-gray-50);
color: var(--dotai-text-light);
border-radius: 0.375rem;
cursor: not-allowed;
font-size: 0.875rem;
}
.dotai-search-widget .dotai-page-info {
text-align: center;
margin-top: 1rem;
color: var(--dotai-text-muted);
font-size: 0.875rem;
}
.dotai-search-widget .dotai-icon-search::before { content: "🔍"; }
.dotai-search-widget .dotai-icon-clear::before { content: "✕"; }
.dotai-search-widget .dotai-icon-loading::before { content: "⟳"; }
</style>
<div class="dotai-search-widget">
<h2>Site Search</h2>
<form class="dotai-search-form" onsubmit="return false;">
<div class="dotai-search-input-group">
<input type="text" id="searchInput" class="dotai-search-input" placeholder="Ask us a question..." maxlength="500" autocomplete="off" />
<div class="dotai-search-icons">
<button type="button" id="clearButton" class="dotai-clear-icon" onclick="clearSearch()" title="Clear search"><span class="dotai-icon-clear"></span></button>
<button type="button" id="searchButton" class="dotai-search-icon" onclick="performSearch()" title="Search"><span class="dotai-icon-search"></span></button>
</div>
</div>
</form>
<div class="dotai-search-options">
<div class="dotai-option-group">
<label>Search Type:</label>
<input type="radio" id="aiChat" name="searchType" value="ai" checked>
<label for="aiChat">AI Chat</label>
<input type="radio" id="keywordSearch" name="searchType" value="keyword">
<label for="keywordSearch">Keyword Search</label>
</div>
</div>
<div id="errorMessage" class="dotai-error-message"></div>
<div id="loading" class="dotai-loading">
<div class="dotai-spinner"></div>
<div>Searching...</div>
</div>
<div id="resultsContainer" class="dotai-results-container"></div>
</div>
<script>
var SearchWidget = {
state: {
apiKeyCache: {},
currentPage: 1,
resultsPerPage: 10,
currentQuery: '',
currentSearchType: 'ai',
totalResults: 0,
allResults: [],
currentAIResponse: ''
},
elements: {},
init: function() {
this.elements = {
searchInput: document.getElementById('searchInput'),
searchButton: document.getElementById('searchButton'),
clearButton: document.getElementById('clearButton'),
errorMessage: document.getElementById('errorMessage'),
loading: document.getElementById('loading'),
resultsContainer: document.getElementById('resultsContainer'),
searchWidget: document.querySelector('.dotai-search-widget')
};
this.setupEventListeners();
this.handleURLParams();
this.prefetchApiKey();
},
setupEventListeners: function() {
var self = this;
var searchInput = this.elements.searchInput;
if (searchInput) {
searchInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
self.performSearch();
}
});
searchInput.addEventListener('input', function() {
self.toggleClearIcon();
});
searchInput.addEventListener('paste', function() {
setTimeout(function() {
self.toggleClearIcon();
}, 10);
});
this.toggleClearIcon();
}
},
handleURLParams: function() {
var urlParams = new URLSearchParams(window.location.search);
var query = urlParams.get('q');
var type = urlParams.get('type');
if (query && this.elements.searchInput) {
this.elements.searchInput.value = query;
}
if (type) {
var typeRadio = document.querySelector('input[name="searchType"][value="' + type + '"]');
if (typeRadio) typeRadio.checked = true;
}
if (query) this.performSearch();
},
sanitizeInput: function(input) {
if (!input || typeof input !== 'string') return '';
return input
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
.trim();
},
validateInput: function(input) {
if (!input || input.trim().length === 0) {
return { valid: false, message: 'Please enter a search query' };
}
if (input.trim().length < 2) {
return { valid: false, message: 'Search query must be at least 2 characters long' };
}
if (input.trim().length > 500) {
return { valid: false, message: 'Search query is too long (maximum 500 characters)' };
}
var maliciousPatterns = [
/<script/i, /javascript:/i, /on\w+\s*=/i, /data:text\/html/i
];
for (var i = 0; i < maliciousPatterns.length; i++) {
if (maliciousPatterns[i].test(input)) {
return { valid: false, message: 'Invalid characters in search query' };
}
}
return { valid: true, message: '' };
},
truncateText: function(text, maxLength) {
maxLength = maxLength || 200;
if (!text || text.length <= maxLength) return text;
return text.substring(0, maxLength) + '...';
},
showError: function(message) {
var errorMessage = this.elements.errorMessage;
if (errorMessage) {
errorMessage.textContent = message;
errorMessage.style.display = 'block';
setTimeout(function() {
errorMessage.style.display = 'none';
}, 5000);
}
},
clearError: function() {
var errorMessage = this.elements.errorMessage;
if (errorMessage) {
errorMessage.style.display = 'none';
errorMessage.textContent = '';
}
},
setLoading: function(loading) {
var loadingElement = this.elements.loading;
var searchButton = this.elements.searchButton;
if (loadingElement) {
loadingElement.style.display = loading ? 'block' : 'none';
}
if (searchButton) {
searchButton.disabled = loading;
searchButton.innerHTML = loading
? '<span class="dotai-icon-loading"></span>'
: '<span class="dotai-icon-search"></span>';
searchButton.title = loading ? 'Searching...' : 'Search';
}
},
toggleClearIcon: function() {
var searchInput = this.elements.searchInput;
var clearButton = this.elements.clearButton;
if (searchInput && clearButton) {
if (searchInput.value.trim().length > 0) {
clearButton.classList.add('show');
} else {
clearButton.classList.remove('show');
}
}
},
hideResults: function() {
var resultsContainer = this.elements.resultsContainer;
if (resultsContainer) {
resultsContainer.style.display = 'none';
resultsContainer.innerHTML = '';
}
},
showResults: function() {
var resultsContainer = this.elements.resultsContainer;
if (resultsContainer) {
resultsContainer.style.display = 'block';
}
},
getApiKey: function() {
var self = this;
if (this.state.apiKeyCache.hasOwnProperty("key")) {
return Promise.resolve(this.state.apiKeyCache);
}
return fetch('/api/vtl/ai', {
method: 'GET',
headers: { 'Content-Type': 'application/json' }
})
.then(function(response) {
if (!response.ok) {
throw new Error('HTTP error! status: ' + response.status);
}
return response.json();
})
.then(function(data) {
self.state.apiKeyCache.key = data.key;
return self.state.apiKeyCache;
})
.catch(function(error) {
console.error('Error fetching API key:', error);
throw new Error('Failed to get API key');
});
},
prefetchApiKey: function() {
this.getApiKey().catch(function(error) {
console.error('Failed to pre-fetch API key:', error);
});
},
buildSearchParams: function() {
var query = this.state.currentQuery
var searchType = this.state.currentSearchType
if (searchType === 'ai') {
return {
prompt: query,
threshold: 0.25,
model: "gpt-4o",
indexName: 'default'
};
} else {
return {
q: query,
indexName: 'default'
};
}
},
performAPISearch: function(searchType, params) {
var self = this;
var isAI = searchType === 'ai';
var endpoint = isAI ? '/api/v1/ai/completions' : '/api/v1/ai/search';
var apiParams = isAI ? {
prompt: params.prompt,
threshold: params.threshold,
searchLimit: 100,
searchOffset: 0,
stream: false,
model: params.model,
responseLengthTokens: 512,
indexName: params.indexName
} : {
prompt: params.q,
threshold: 0.25,
searchLimit: 100,
searchOffset: 0,
indexName: params.indexName
};
return fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + this.state.apiKeyCache.key
},
body: JSON.stringify(apiParams)
})
.then(function(response) {
if (!response.ok) {
throw new Error('HTTP error! status: ' + response.status);
}
return response.json();
})
.then(function(data) {
var aiResponse = '';
if (isAI && data.openAiResponse && data.openAiResponse.choices &&
data.openAiResponse.choices[0] && data.openAiResponse.choices[0].message) {
aiResponse = data.openAiResponse.choices[0].message.content;
}
self.state.allResults = data.dotCMSResults || [];
self.state.currentAIResponse = aiResponse;
self.state.totalResults = self.state.allResults.length;
return {
response: aiResponse,
dotCMSResults: self.state.allResults,
total: self.state.totalResults,
openAiResponse: data.openAiResponse
};
})
.catch(function(error) {
console.error('Error performing ' + (isAI ? 'AI' : 'keyword') + ' search:', error);
throw new Error('Failed to perform ' + (isAI ? 'AI' : 'keyword') + ' search');
});
},
createPaginationControls: function() {
var totalPages = Math.ceil(this.state.totalResults / this.state.resultsPerPage);
if (totalPages <= 1) return '';
var html = '<div class="dotai-pagination">';
if (this.state.currentPage > 1) {
html += '<button onclick="SearchWidget.goToPage(' + (this.state.currentPage - 1) + ')" class="dotai-pagination-btn">Previous</button>';
} else {
html += '<button disabled class="dotai-pagination-btn-disabled">Previous</button>';
}
var startPage = Math.max(1, this.state.currentPage - 2);
var endPage = Math.min(totalPages, this.state.currentPage + 2);
if (startPage > 1) {
html += '<button onclick="SearchWidget.goToPage(1)" class="dotai-pagination-btn">1</button>';
if (startPage > 2) {
html += '<span style="padding: 0.5rem; color: var(--dotai-text-muted);">...</span>';
}
}
for (var i = startPage; i <= endPage; i++) {
if (i === this.state.currentPage) {
html += '<button class="dotai-pagination-current">' + i + '</button>';
} else {
html += '<button onclick="SearchWidget.goToPage(' + i + ')" class="dotai-pagination-btn">' + i + '</button>';
}
}
if (endPage < totalPages) {
if (endPage < totalPages - 1) {
html += '<span style="padding: 0.5rem; color: var(--dotai-text-muted);">...</span>';
}
html += '<button onclick="SearchWidget.goToPage(' + totalPages + ')" class="dotai-pagination-btn">' + totalPages + '</button>';
}
if (this.state.currentPage < totalPages) {
html += '<button onclick="SearchWidget.goToPage(' + (this.state.currentPage + 1) + ')" class="dotai-pagination-btn">Next</button>';
} else {
html += '<button disabled class="dotai-pagination-btn-disabled">Next</button>';
}
html += '</div>';
html += '<div class="dotai-page-info">Page ' + this.state.currentPage + ' of ' + totalPages + '</div>';
return html;
},
goToPage: function(page) {
var totalPages = Math.ceil(this.state.totalResults / this.state.resultsPerPage);
if (page < 1 || page > totalPages) return;
this.state.currentPage = page;
this.displayStoredResults();
if (this.elements.searchWidget) {
var elementTop = this.elements.searchWidget.getBoundingClientRect().top + window.pageYOffset;
var offsetPosition = elementTop - 200;
window.scrollTo({
top: Math.max(0, offsetPosition),
behavior: 'smooth'
});
}
},
displayStoredResults: function() {
var resultsContainer = this.elements.resultsContainer;
if (!resultsContainer) return;
var html = '';
this.showResults();
if (this.state.currentAIResponse && this.state.currentPage === 1) {
html += '<div class="dotai-ai-response">' +
'<h3>AI Response:</h3>' +
'<p>' + this.state.currentAIResponse + '</p>' +
'</div>';
}
var startIndex = (this.state.currentPage - 1) * this.state.resultsPerPage;
var endIndex = Math.min(startIndex + this.state.resultsPerPage, this.state.totalResults);
var pageResults = this.state.allResults.slice(startIndex, endIndex);
if (pageResults && pageResults.length > 0) {
var displayStart = startIndex + 1;
var displayEnd = startIndex + pageResults.length;
html += '<h3>Search Results (Showing ' + displayStart + '-' + displayEnd + ' of ' + this.state.totalResults + ' results):</h3>';
for (var i = 0; i < pageResults.length; i++) {
var result = pageResults[i];
if (!result || typeof result !== 'object') {
continue;
}
var title = result.title || result.name || 'Untitled';
var url = result.urlMap || result.URL_MAP_FOR_CONTENT
|| result.path || '#';
var contentType = result.contentType || result.type || 'Unknown';
var content = '';
if (result.matches && result.matches[0] && result.matches[0].extractedText) {
content = result.matches[0].extractedText;
} else {
content = result.shortDescription || result.description || '';
}
content = this.truncateText(content);
var score = 'N/A';
if (result.matches && result.matches[0] && typeof result.matches[0].distance === 'number') {
score = ((1 - result.matches[0].distance) * 100).toFixed(0);
}
html += '<div class="dotai-result-item">' +
'<div class="dotai-result-title">' +
'<a href="' + url + '" target="_blank">' + title + '</a>' +
'</div>' +
'<div class="dotai-result-content">' + content + '</div>' +
'<div class="dotai-result-meta">' +
'<span>Type: ' + contentType + '</span>' +
'<span>Score: ' + score + '%</span>' +
'</div>' +
'</div>';
}
html += this.createPaginationControls();
}
if (!html || pageResults.length === 0) {
html = '<p>No results found.</p>';
}
resultsContainer.innerHTML = html;
},
updateURL: function(query, searchType) {
var url = new URL(window.location.href);
url.searchParams.set('q', this.sanitizeInput(query));
url.searchParams.set('type', searchType);
window.history.replaceState({}, '', url.href);
},
clearURL: function() {
var url = new URL(window.location.href);
url.searchParams.delete('q');
url.searchParams.delete('type');
window.history.replaceState({}, '', url.href);
},
performSearch: function() {
var self = this;
var input = this.elements.searchInput.value;
var validation = this.validateInput(input);
if (!validation.valid) {
this.showError(validation.message);
return;
}
var newQuery = this.sanitizeInput(input);
var newSearchType = document.querySelector('input[name="searchType"]:checked').value;
if (newQuery !== this.state.currentQuery || newSearchType !== this.state.currentSearchType) {
this.state.currentPage = 1;
}
this.state.currentQuery = newQuery;
this.state.currentSearchType = newSearchType;
this.hideResults();
this.clearError();
this.setLoading(true);
this.getApiKey()
.then(function() {
var params = self.buildSearchParams();
return self.performAPISearch(newSearchType, params);
})
.then(function() {
self.displayStoredResults();
self.updateURL(newQuery, newSearchType);
})
.catch(function(error) {
console.error('Search error:', error);
self.showError('Search failed. Please try again.');
})
.finally(function() {
self.setLoading(false);
});
},
clearSearch: function() {
var searchInput = this.elements.searchInput;
if (searchInput) {
searchInput.value = '';
searchInput.focus();
}
this.state.currentPage = 1;
this.state.currentQuery = '';
this.state.totalResults = 0;
this.hideResults();
this.clearError();
this.toggleClearIcon();
this.clearURL();
}
};
function performSearch() {
SearchWidget.performSearch();
}
function clearSearch() {
SearchWidget.clearSearch();
}
document.addEventListener('DOMContentLoaded', function() {
SearchWidget.init();
});
</script>