Skip to content
Snippets Groups Projects
Commit 60be3d75 authored by Konrad Völkel's avatar Konrad Völkel
Browse files

initial experiment

parents
No related branches found
No related tags found
Loading
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>HHU Search Helper - Inner</title>
<link rel="stylesheet" href="styles.css">
<style>
body {
overflow: hidden;
}
</style>
</head>
<body>
<div id="chatbox">
<div id="chatbox-title">HHU Search Helper</div>
<div id="messages"></div>
<div id="input-container">
<input type="text" id="user-input" placeholder="Try searching: 'minimum admission grade' or 'I'm from outside the EU'">
<button onclick="saveMessage()">Save</button>
</div>
</div>
<script>
/** make iframes grow and informs about growth */
function adjustIframeSize() {
if (window.frameElement) {
window.frameElement.style.height = document.documentElement.scrollHeight + 'px';
}
}
window.addEventListener('load', adjustIframeSize);
const observers = new MutationObserver(adjustIframeSize);
observers.observe(document.body, { childList: true, subtree: true });
function notifyParentOfResize() {
if (window.parent && window.parent.adjustIframeSize) {
window.parent.adjustIframeSize();
}
}
window.addEventListener('load', notifyParentOfResize);
const observer = new MutationObserver(notifyParentOfResize);
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true
});
</script>
<script type="module" src="index.js" async></script>
</body>
</html>
index.js 0 → 100644
import { sendMessage } from './modules/ui.js';
import { tokenizeText } from './modules/semanticSearch.js';
import { extractFAQfromHTML } from './modules/faqData.js';
import { loadEmbedding, removeOutdatedEntries } from './modules/embeddings.js'
import { handleWorker, processQuery } from './modules/workerInterface.js'
const worker = new Worker('./worker.js', { type: 'module' });
/** get the up to date FAQ from site */
const textData = await extractFAQfromHTML("https://www.heicad.hhu.de/lehre/masters-programme-ai-and-data-science/faq/");
/** import precompiled embeddings and check for missing entries */
let embeddingStack = await loadEmbedding('largeEmbedding', './embeddings/largeEmbedding.json');
let tokenEmbeddingStack = await loadEmbedding('smallEmbedding', './embeddings/smallEmbedding.json');
/** throw out embeddings for entries not present on site any more */
const checkFAQ = new Set(textData.map(faq => faq.qPlusA));
removeOutdatedEntries(embeddingStack, checkFAQ);
const checkTokensAnswer = new Set();
textData.forEach(faq => tokenizeText(faq.answer).forEach(token => checkTokensAnswer.add(token)));
removeOutdatedEntries(tokenEmbeddingStack, checkTokensAnswer);
/** load up to date embeddings into local browser storage */
localStorage.setItem('largeEmbedding', JSON.stringify(embeddingStack));
localStorage.setItem('smallEmbedding', JSON.stringify(tokenEmbeddingStack));
/** handeling results iframe */
const inputElement = document.getElementById('user-input');
const resultsIframe = parent.document.getElementById('resultsIframe');
inputElement.addEventListener('input', function (event) {
const query = event.target.value.trim();
const results = resultsIframe.contentDocument || resultsIframe.contentWindow.document;
const resultsContainer = results.getElementById('results');
/** hide results when no query is typed */
if (!query) {
resultsContainer.innerHTML = '';
setTimeout(() => {
resultsIframe.style.display = 'none';
}, 500);
return;
}
resultsIframe.style.display = 'block';
resultsContainer.innerHTML = '';
processQuery(query, worker, function (outputs) {
outputs.forEach(output => {
const resultLink = results.createElement('a');
resultLink.href = 'javascript:void(0);';
resultLink.className = 'result';
resultLink.style.display = 'block';
/** display question text in result iframe */
const questionDiv = results.createElement('div');
questionDiv.className = 'result-question';
questionDiv.innerHTML = `<strong>${output.question}</strong>`;
resultLink.appendChild(questionDiv);
/** display our most relevant token to question in result iframe */
const bestTokenDiv = results.createElement('div');
bestTokenDiv.className = 'result-token';
let tokenText = output.bestToken;
/** shorten our best token; only 100 chars displayed*/
if (tokenText.length > 100) {
tokenText = tokenText.substring(0, 100);
}
tokenText = '> ' + tokenText + ' ...';
bestTokenDiv.textContent = tokenText;
resultLink.appendChild(bestTokenDiv);
resultLink.onclick = function () {
linkToPanel(output.buttonId, output.akkordeonId, output.bestToken);
};
resultsContainer.appendChild(resultLink);
});
});
});
/** search for query on enter press and put it in chat */
document.getElementById('user-input').addEventListener('keypress', function (event) {
if (event.key === 'Enter') {
event.preventDefault();
sendMessage(worker);
}
});
worker.onmessage = (events) => handleWorker(events);
/** show results in chat window if "save" is clicked */
function saveMessage(){
return sendMessage(worker);
}
window.saveMessage = saveMessage;
/** give worker work */
worker.postMessage({ act: 'initialize', textData, embeddingStack, tokenEmbeddingStack });
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Search Results</title>
<link rel="stylesheet" href="styles.css">
<style>
body {
overflow: hidden;
}
</style>
</head>
<body>
<main id="results" role="main" aria-labelledby="results-heading" aria-live="polite">
<h1 id="results-heading">Search Results</h1>
</main>
<script>
/** make iframes grow and informs about growth */
function adjustIframeSize() {
if (window.frameElement) {
window.frameElement.style.height = document.documentElement.scrollHeight + 'px';
}
}
window.addEventListener('load', adjustIframeSize);
const observers = new MutationObserver(adjustIframeSize);
observers.observe(document.body, { childList: true, subtree: true });
function notifyParentOfResize() {
if (window.parent && window.parent.adjustIframeSize) {
window.parent.adjustIframeSize();
}
}
window.addEventListener('load', notifyParentOfResize);
const observer = new MutationObserver(notifyParentOfResize);
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true
});
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>HHU Search Helper</title>
<style>
body {
overflow: hidden;
}
</style>
</head>
<body>
<iframe id="chatIframe" src="chatbox.html" style="position: relative; width: 100%; border: none;" title="HHU Search Helper"></iframe>
<iframe id="resultsIframe" src="results.html" style="position: sticky; left: 15px; min-height: 300px; transition: opacity 1s ease-in-out, transform 1s ease-in-out; background-color: #f0f0f0; width: 95%; overflow: hidden; padding: 10px; border-radius: 5px; border: none; display: none; overflow: hidden;"></iframe>
<script>
/** make iframes grow with iframes inside it */
function adjustIframeSize() {
const chatIframe = document.getElementById('chatIframe');
const resultsIframe = document.getElementById('resultsIframe');
let totalHeight = 0;
if (chatIframe.offsetHeight) {
totalHeight += chatIframe.offsetHeight;
}
if (resultsIframe.style.display !== 'none' && resultsIframe.offsetHeight) {
totalHeight += resultsIframe.offsetHeight;
}
if (window.frameElement) {
window.frameElement.style.height = totalHeight + 'px';
}
}
document.getElementById('chatIframe').addEventListener('load', adjustIframeSize);
document.getElementById('resultsIframe').addEventListener('load', adjustIframeSize);
const observer = new MutationObserver(adjustIframeSize);
observer.observe(document.body, { childList: true, subtree: true, attributes: true });
</script>
</body>
</html>
body {
font-family: Arial, sans-serif;
}
#chatbox {
background-color: #d3d3d3;
position: sticky;
width: 100%;
max-height: 400px;
overflow-x: hidden;
overflow-y: auto;
display: flex;
flex-direction: column;
}
#chatbox-title {
background-color: #003865;
color: white;
text-align: center;
padding: 10px;
font-size: 20px;
font-weight: bold;
overflow: hidden;
z-index: 1231234;
overflow-x: hidden;
}
#messages {
flex: 1;
padding: 10px;
display: flex;
max-height: 270px;
flex-direction: column;
overflow-x: hidden;
z-index: -3;
overflow-y: auto;
}
#messages div {
margin-bottom: 0px;
padding: 10px;
}
#input-container {
display: flex;
padding: 10px;
height: 50px;
}
#input-container input {
flex: 1;
padding: 10px;
border: 2px solid #ccc;
font-size: 16px;
}
#input-container button {
padding: 10px;
margin-left: 10px;
border: none;
background-color: #003865;
color: white;
cursor: pointer;
font-size: 16px;
border: 2px solid #ccc;
}
#input-container button:hover {
background-color: #0056b3;
}
.user-message {
align-self: flex-end;
background-color: #007bff;
color: white;
margin: 10px 0 10px auto;
padding: 15px 20px;
width: fit-content;
max-width: 60%;
word-wrap: break-word;
text-align: center;
}
.bot-message {
align-self: flex-start;
background-color: #003865;
color: white;
margin: 10px 2;
max-width: 60%;
word-wrap: break-word;
text-align: middle;
}
.bot-message-container {
display: flex;
align-items: stretch;
}
.info-panel {
background-color: #0056b3;
color: rgb(255, 255, 255);
cursor: pointer;
max-width: 150px;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
height: auto;
position: relative;
}
.info-panel:hover {
background-color: #072665;
}
.info-panel::before {
content: "More Info";
position: absolute;
background-color: rgb(0, 0, 0, 0.8);
color: white;
padding: 10px;
border-radius: 4px;
font-size: 14px;
opacity: 0;
visibility: hidden;
pointer-events: none;
transition: opacity 0.2s;
white-space: nowrap;
left: 50px;
}
.info-panel:hover::before {
opacity: 1;
visibility: visible;
}
.number-box {
background-color: #aeb5bc;
color: white;
padding: 10px;
text-align: center;
font-weight: bold;
max-width: 150px;
display: flex;
justify-content: center;
align-items: center;
}
.bot-message-container {
position: relative;
display: flex;
align-items: stretch;
}
.feedback-container {
position: absolute;
left: 3px;
bottom: -3px;
background-color: #003865;
color: #000;
cursor: pointer;
max-width: 10px;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
height: 0px;
padding: 3px;
font-size: 12px;
border-radius: 4px;
}
.feedback-container::after {
content: "Is this helpful? Let us know.";
position: absolute;
bottom: 150%;
left: 50%;
transform: translateX(-30%);
background-color: rgb(0, 0, 0, 0.8);
color: white;
padding: 10px;
border-radius: 4px;
font-size: 14px;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s;
transition-delay: 1s;
}
.feedback-container:hover::after {
opacity: 1;
visibility: visible;
}
.feedback-button {
padding: 2px;
margin: 0 2px;
border: 0px;
background: transparent;
color: #ffffff;
cursor: pointer;
font-size: 20px;
transition: transform 0.1s ease;
}
.feedback-button:hover {
transform: scale(1.2);
}
.feedback-button:active {
transform: scale(1.4);
}
#results {
position: sticky;
background-color: #f0f0f0;
padding: 10px;
width: 95%;
display: block;
max-height: auto;
overflow: hidden;
}
#results .show {
opacity: 1;
transform: translateY(0);
visibility: visible;
overflow: hidden;
min-height: 200px;
}
#results .result {
padding: 4px 8px;
margin-bottom: 4px;
border-bottom: 1px solid #ddd;
font-size: 17px;
padding-left: 5%;
padding-right: 10%;
overflow: hidden;
text-decoration: none;
color: inherit;
}
#results .result:hover {
background-color: #e0e0e0;
overflow: hidden;
font-size: 18px;
}
import {
initializeModel,
calcFAQEmbeddings,
precalcTokenEmbeddings,
semanticSearch,
findBestToken,
getEmbeddingCache,
getTokenEmbeddingCache,
setCaches
} from './modules/semanticSearch.js';
let textData = [];
let faqEmbeddings = [];
self.onmessage = async function (event) {
const { act, query, textData: newtextData, embeddingStack, tokenEmbeddingStack } = event.data;
switch (act) {
case 'initialize':
await initializeModel();
textData = newtextData;
setCaches(embeddingStack, tokenEmbeddingStack);
faqEmbeddings = await calcFAQEmbeddings(textData, "qPlusA");
await precalcTokenEmbeddings(textData);
postMessage({ act: 'initialized', embeddingCache: getEmbeddingCache() });
postMessage({ act: 'tokeninit', tokenEmbeddingCache: getTokenEmbeddingCache() });
break;
case 'semanticSearch':
const faqResults = await semanticSearch(query, textData, faqEmbeddings);
const resultsTokenChat = await getResultsWithTokens(faqResults, query);
postMessage({ act: 'searchResults', results: resultsTokenChat, query });
break;
case 'resultsSemantic':
const faqResult = await semanticSearch(query, textData, faqEmbeddings);
const resultsTokenResults = await getResultsWithTokens(faqResult, query);
postMessage({ act: 'topResults', results: resultsTokenResults, query });
break;
}
};
/** extract the best token in side the answer of a panel for a given query */
async function getResultsWithTokens(faqResults, query) {
return await Promise.all(
faqResults.map(async (result) => {
const { bestToken } = await findBestToken(query, result.answer);
return { ...result, bestToken };
})
);
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment