diff --git a/site/assets/img/events-people.png b/site/assets/img/events-people.png new file mode 100644 index 0000000000..8a66643dca Binary files /dev/null and b/site/assets/img/events-people.png differ diff --git a/site/assets/img/standard-hero-dots-2-1024x620.png b/site/assets/img/standard-hero-dots-2-1024x620.png new file mode 100644 index 0000000000..cfae6163a3 Binary files /dev/null and b/site/assets/img/standard-hero-dots-2-1024x620.png differ diff --git a/site/index.qmd b/site/index.qmd index 5f022247ad..019ce21a54 100644 --- a/site/index.qmd +++ b/site/index.qmd @@ -3,6 +3,7 @@ pagetitle: "Welcome to our documentation" page-layout: full sidebar: false repo-actions: false +format: html resources: - assets/** filters: @@ -70,21 +71,65 @@ a:hover { background-color: #F9F9F9; } -#searchbox { +.search-wrapper { + position: relative; + width: 100%; max-width: 600px; + margin: 0; + padding: 0; +} + +.input-container { + position: relative; + width: 100%; + max-width: 400px; +} + +#searchbox { + width: 100%; padding: 10px; + padding-left: 40px; /* Add padding so the text doesn’t overlap the icon */ border-radius: 8px; - width: 100%; box-sizing: border-box; } +.search-icon { + position: absolute; + left: 10px; + top: 50%; + transform: translateY(-50%); + pointer-events: none; + fill: #4a4a4a; +} + +#toast { + position: absolute; + top: calc(100% + 5px); + left: 0; + width: 100%; + color: white; + padding-left: 10px; + text-align: left; + z-index: 1000; + font-weight: bold; + opacity: 0; + transition: opacity 0.5s ease; + background-color: transparent; + box-shadow: none; + border: none; +} + +#toast.show { + opacity: 1; +} + #hits { display: none; overflow-y: auto; - width: 100vw; - max-width: 800px; + width: 100%; + max-width: 600px; max-height: 400px; - margin: 20px auto; + margin-top: 50px; padding: 15px; background-color: #f9f9f9; border: 1px solid #ccc; @@ -99,7 +144,7 @@ a:hover { } #hits div:hover { - background-color: #EAF8FA; /* Light green background for the entire search result on hover */ + background-color: #EAF8FA; } #hits a strong { @@ -112,6 +157,23 @@ a:hover { text-decoration: none; } +#explain { + display: none; + position: relative; + z-index: 100; + overflow-y: auto; + width: 100%; + max-width: 600px; + max-height: 1000px; + margin: 20px auto; + padding: 15px; + color: #222425; + background-color: #f9f9f9; + border: 1px solid #ccc; + border-radius: 8px; + box-sizing: border-box; +} + ``` @@ -135,24 +197,33 @@ a:hover { The **purpose-built platform** for model risk management teams to test, document, validate, and govern Generative AI, AI, and statistical models with speed and confidence. :::: -[ask a question]{.smallcaps} - - - +[How do I ... ?]{.smallcaps} + + +
+
+ + + + +
+
Press Enter for more context
+
- + quickstart open-source software - ::: ::: {.w-40-ns} +
+ ::: {.image-container} ![](assets/img/platform-line-1.png) ![](assets/img/platform-line-2.png) @@ -161,6 +232,7 @@ The **purpose-built platform** for model risk management teams to test, document ![](assets/img/platform-line-5.png) ::: + ::: ::: {.w-10-ns} @@ -181,7 +253,6 @@ The **purpose-built platform** for model risk management teams to test, document ::: - ::: {.w-25-ns .mb4} :::: {.ba .b--black-10 .br3 .shadow-4 .bg-white .pt4 .pb3 .h-100 .grow} @@ -203,7 +274,6 @@ The **purpose-built platform** for model risk management teams to test, document :::: - :::: ::: @@ -227,7 +297,6 @@ The **purpose-built platform** for model risk management teams to test, document developer reference - :::: :::: @@ -259,7 +328,6 @@ The **purpose-built platform** for model risk management teams to test, document ::: - ::: {.w-10-ns} ::: diff --git a/site/scripts/validsearch.js b/site/scripts/validsearch.js index 97bffc22a2..9ab1a417c7 100644 --- a/site/scripts/validsearch.js +++ b/site/scripts/validsearch.js @@ -2,9 +2,12 @@ // See the LICENSE file in the root of this repository for details. // SPDX-License-Identifier: AGPL-3.0 AND ValidMind Commercial -// Fetch the generated Algolia search index +// Fetch the local Algolia search index const SEARCH_INDEX_URL = 'search.json'; +// Set to 'true' to disable cleaning up the streaming text (editing is done on the backend) +let disableCleanText = true; + // Function to load search.json file and return it as a JavaScript object async function loadSearchIndex() { const response = await fetch(SEARCH_INDEX_URL); @@ -17,52 +20,154 @@ async function setupLunr() { const searchData = await loadSearchIndex(); // Create Lunr.js index - const idx = lunr(function () { - // Define the fields to index + const searchIndex = lunr(function () { this.ref('href'); - this.field('title'); - this.field('text'); - this.field('section'); - this.field('crumbs'); // Make sure crumbs is also indexed + this.field('title', { boost: 10 }); + this.field('text', { boost: 5 }); + this.field('section', { boost: 0.5 }); + this.field('crumbs', { boost: 0.2 }); - // Index crumbs as a space-separated string searchData.forEach(function (doc) { const modifiedDoc = { ...doc, - crumbs: Array.isArray(doc.crumbs) ? doc.crumbs.join(' ') : doc.crumbs // Convert crumbs array to string + crumbs: Array.isArray(doc.crumbs) ? doc.crumbs.join(' ') : doc.crumbs }; - this.add(modifiedDoc); // Add the modified document with stringified crumbs + this.add(modifiedDoc); // Add the modified document }, this); }); - return { idx, searchData }; + return { searchIndex, searchData }; +} + +// Strip out text that can cause poor search ranking +function sanitizeInput(input) { + let sanitized = input.toLowerCase(); // Convert to lowercase for consistent matching + sanitized = sanitized.replace(/^how do i\s+/, ''); // Remove common phrases like "how do I" + + sanitized = sanitized.replace(/[?.!]/g, ''); // Remove punctuation marks like ? or ! + sanitized = sanitized.trim(); // Trim any leading or trailing spaces + + return sanitized; +} + +// Clean up response text for minor tweaks, disabled by default (and major changes should be done on the backend) +function cleanText(text) { + return text + .replace(/\s+([,.;!?()])/g, '$1') // Remove space before punctuation + .replace(/\(\s+/g, '(') // Remove space after opening parenthesis + .replace(/\s+\)/g, ')') // Remove space before closing parenthesis + .replace(/\s+-\s+/g, '-') // Remove spaces around hyphens + .replace(/\s+/g, ' ') // Replace multiple spaces with a single space + .trim(); // Trim any extra spaces +} + + +// Prompt the user to hit Enter for more info +function showToast() { + const toast = document.getElementById('toast'); + const searchbox = document.getElementById('searchbox'); + + // Listen to input events on the searchbox + searchbox.addEventListener('input', function () { + if (searchbox.value.trim() !== '') { + toast.classList.add('show'); + } else { + toast.classList.remove('show'); + } + }); +} + +// Function to fetch and render streaming explanation +async function fetchExplainResults(query) { + const explainUrl = 'http://localhost:3333/explain-results'; + const explainContainer = document.getElementById('explain'); + + let buffer = ''; // Buffer to accumulate incoming HTML chunks + + try { + const response = await fetch(explainUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ userQuery: query }) + }); + + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder("utf-8"); + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + let chunk = decoder.decode(value, { stream: true }); + + // Remove "data: " and "data: [DONE]" + chunk = chunk.replace(/data: /g, '').replace(/\[DONE\]/g, ''); + + // Add the cleaned chunk to the buffer + buffer += chunk; + + // Append the cleaned HTML directly to the explainContainer + explainContainer.innerHTML += buffer; + + // Clear the buffer after appending + buffer = ''; + } + + } catch (error) { + console.error("Error fetching explanation:", error); + } } // Clear the search input when the page loads window.onload = function() { - document.getElementById('searchbox').value = ''; // Clear the search box input + document.getElementById('searchbox').value = ''; + showToast(); }; -setupLunr().then(({ idx, searchData }) => { - document.getElementById('searchbox').addEventListener('input', function (event) { - const query = event.target.value.trim(); +// Function to handle the search and display results +setupLunr().then(({ searchIndex, searchData }) => { + document.getElementById('searchbox').addEventListener('input', async function (event) { + let query = event.target.value.trim(); + + // Sanitize the query + query = sanitizeInput(query); // Call the sanitizeInput function here + const hitsContainer = document.getElementById('hits'); - hitsContainer.innerHTML = ''; // Clear previous results + const explainContainer = document.getElementById('explain'); + + hitsContainer.innerHTML = ''; - // Hide the hits container if the search box is empty + // Clear the explain div when the input is empty if (query === '') { hitsContainer.style.display = 'none'; + explainContainer.innerHTML = ''; + explainContainer.style.display = 'none'; return; } - const exactResults = idx.search(`"${query}"`); - let results = exactResults.length > 0 ? exactResults : idx.search(query); + // Perform an exact match search first + let lunrResults = searchIndex.search(`"${query}"`); + + // If no exact matches found, fall back to a regular search + if (lunrResults.length === 0) { + lunrResults = searchIndex.search(query); + } + + // Ensure that you limit the results to the top 20 in both cases + lunrResults = lunrResults.slice(0, 20); + + if (lunrResults.length > 0) { + hitsContainer.style.display = 'block'; - // Show the hits container if there are results, otherwise hide it - if (results.length > 0) { - hitsContainer.style.display = 'block'; // Show the container - results.forEach((result) => { + lunrResults.forEach((result) => { const doc = searchData.find(d => d.href === result.ref); + if (doc) { const resultElement = document.createElement('div'); const crumbs = Array.isArray(doc.crumbs) ? doc.crumbs.join(' > ') : doc.crumbs; @@ -71,17 +176,32 @@ setupLunr().then(({ idx, searchData }) => { ${doc.title}
${doc.section}
- ${doc.text.substring(0, 100)}...
-
- ${crumbs} -
+ ${crumbs}
+ ${doc.text.substring(0, 100)}...
`; hitsContainer.appendChild(resultElement); } }); } else { - hitsContainer.style.display = 'none'; // Hide the container if no results + hitsContainer.style.display = 'none'; + } + }); + + // Add event listener for hitting Enter to start explanation + document.getElementById('searchbox').addEventListener('keydown', async function (event) { + if (event.key === 'Enter') { + let query = document.getElementById('searchbox').value.trim(); + + // Sanitize the query before fetching the explanation + query = sanitizeInput(query); + + const explainContainer = document.getElementById('explain'); + + if (query) { + explainContainer.style.display = 'block'; + await fetchExplainResults(query); + } } }); -}); \ No newline at end of file +}); diff --git a/site/styles.css b/site/styles.css index e85069a100..bef9cdfb16 100644 --- a/site/styles.css +++ b/site/styles.css @@ -318,7 +318,6 @@ input[type="checkbox"][checked] { transform: scale(1.05); } - .image-container { position: relative; z-index: -1; @@ -332,7 +331,6 @@ input[type="checkbox"][checked] { animation: fadeUp 25s infinite; } - .image-container img:nth-child(1) { animation-delay: 0s; }