dot CMS

How to Implement a dotAI Site Search Widget in dotCMS (Step-by-Step Guide)

How to Implement a dotAI Site Search Widget in dotCMS (Step-by-Step Guide)
Laura

Laura Cabrerizo

Solutions Engineer

Share this article on:

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

  1. Navigate to System → Users

  2. Create a new user (+ upper right hand corner)

    1. Give it a name, and email address (doesn’t need to be real)

    2. Save

  3. Click on the new user in the list on the left then click the  ‘Roles’ tab

    • Grant the ‘Front-end User’ role

      1. We want to limit the access this token has in the system as much as possible

    • Save the Roles

image
  1. Click on the ‘API Access Tokens’ tab

  2. Click ‘Request New Token’

    • Give it a label or simply click ‘Ok’

    image
  3. 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)

  4. Close the box

 

Step 2: Configure the API VTL folder structure

  1. Navigate to or create the following folder structure under your Site Browser

    • application → apivtl → ai

  2. In the upper right corner click the + icon and select ‘Image or File’

  3. In the dropdown menu, select File → Select

  4. In the center box, select ‘<> Create New File’

  5. Name the file get.vtl

    image
  6. 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)
  7. Publish the file

 

Step 3: Create and publish the dotAI search widget VTL

Implementing the Widget

  1. Navigate to or create the following folder structure under your Site Browser

    1. application → vtl → widgets

  2. In the upper right corner click the + icon and select ‘Image or File’

  3. In the dropdown menu, select File → Select

  4. In the center box, select ‘<> Create New File’

image
  1. Name the file dotAI-search-widget.vtl

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

  3. Save the changes

  4. Publish your new file

 

Step 4: Place the search widget on a page

  1. Navigate to or create the page you would like to display your search results on

  2. 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’

  3. Drag the control to the appropriate container on your page

  4. This will open a new dialogue

    image
  5. Fill in the Title with the name for your new widget

  6. Select ‘Browse’ next to the ‘VTL File’ textbox and navigate to the folder and file we created in the previous steps

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

image

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:

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

image
  1. Navigate to and open ‘dotAI-search-widgit.vtl’ 

  2. Ctrl + F to find the phrase indexName: 'default'

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

  4. Publish your .vtl file

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

Explore dotCMS for your organization

image

dotCMS Named a Major Player

In the IDC MarketScape: Worldwide AI-Enabled Headless CMS 2025 Vendor Assessment

image

Explore an interactive tour

See how dotCMS empowers technical and content teams at compliance-led organizations.

image

Schedule a custom demo

Schedule a custom demo with one of our experts and discover the capabilities of dotCMS for your business.