From efbc330e2de3cc0eb652a65eda7f7a0ff84e81cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Konrad=20V=C3=B6lkel?= <konrad.voelkel@hhu.de> Date: Fri, 28 Feb 2025 09:03:59 +0100 Subject: [PATCH] another fix --- chatbox.html | 6 +-- index.js | 57 +++++++++++++++------ modules/feedback.js | 19 ++++--- modules/ui.js | 100 +++++++++++++++++++++++++++---------- modules/workerInterface.js | 18 +++++-- styles.css | 84 +++++++++++++++++++++++++++---- worker.js | 2 +- 7 files changed, 217 insertions(+), 69 deletions(-) diff --git a/chatbox.html b/chatbox.html index 8ae6a06..143d69b 100644 --- a/chatbox.html +++ b/chatbox.html @@ -15,12 +15,12 @@ <body> <div id="chatbox"> - <div id="chatbox-title">HHU Search Helper</div> + <div id="chatbox-title">Ask hAInrich</div> <div id="messages"></div> <div id="messageInfo"></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()">Send</button> + <input type="text" id="user-input" placeholder="Try asking: 'minimum admission grade' or 'I'm from outside the EU'"> + <button onclick="sendAsk()">Ask</button> </div> </div> diff --git a/index.js b/index.js index bf1a78e..171c923 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,4 @@ -import { sendMessage, copyToClipboard } from './modules/ui.js'; +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' @@ -38,10 +38,10 @@ localStorage.setItem('smallEmbedding', JSON.stringify(tokenEmbeddingStack)); const inputElement = document.getElementById('user-input'); const resultsIframe = parent.document.getElementById('resultsIframe'); -let content = ""; - +let topResult = null; inputElement.addEventListener('input', function (event) { - window.focus(); + + const query = event.target.value.trim(); const results = resultsIframe.contentDocument || resultsIframe.contentWindow.document; @@ -55,11 +55,13 @@ inputElement.addEventListener('input', function (event) { }, 500); return; } - let resultLinks; + resultsIframe.style.display = 'block'; resultsContainer.innerHTML = ''; + processQuery(query, worker, function (outputs) { + window.focus(); outputs.forEach(async output => { @@ -87,37 +89,60 @@ inputElement.addEventListener('input', function (event) { resultLink.appendChild(bestTokenDiv); window.focus(); + /** open the answer field in chat view for selected panel */ + console.log("inside", output.question); resultLink.onclick = function () { - window.focus(); - copyToClipboard(output.question); - + linkAndClick(output.question); }; resultsContainer.appendChild(resultLink); }); - }); -}); - + if (outputs.length > 0) { + topResult = outputs[0]; + } + }); +}); /** 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); - + if (topResult) { + linkAndClick(topResult.question); + } } }); -worker.onmessage = (events) => handleWorker(events); +export function linkAndClick(output) { + saveMessage(); + + setTimeout(() => { + const encodedQuestion = encodeURIComponent(output); + const panels = document.querySelectorAll( + `.info-panel[data-question="${encodedQuestion}"]` + ); + if (panels.length > 0) { + panels[panels.length - 1].click(); + } + }, 250); +} +worker.onmessage = (events) => handleWorker(events); -/** show results in chat window if "send" is clicked */ +/** show results in chat window if "ask" is clicked */ function saveMessage(){ return sendMessage(worker); } -window.saveMessage = saveMessage; + +function sendAsk(){ + if (topResult) { + linkAndClick(topResult.question); + } +} + +window.sendAsk = sendAsk; /** give worker work */ worker.postMessage({ act: 'initialize', textData, embeddingStack, tokenEmbeddingStack }); diff --git a/modules/feedback.js b/modules/feedback.js index 44cf2d4..a7b54eb 100644 --- a/modules/feedback.js +++ b/modules/feedback.js @@ -2,7 +2,7 @@ /** open mail client of user with prewritten message and stats */ -export function submitFeedback(query, bestToken, feedback) { +export function submitFeedback(query, bestAnswers, feedback) { /** temporary mail adress, adjust if needed */ const feedbackAdress = 'huepfen-richtlinie.7r@icloud.com'; @@ -10,22 +10,27 @@ export function submitFeedback(query, bestToken, feedback) { let subject = ''; let message = ''; + const bestAnswersAsString = Object.entries(bestAnswers) + .map(([question, token]) => `> Question: ${question} \n > Answer: ${token}\n`) + .join("\n\n"); + if (feedback === 'up') { subject = 'Positive Feedback: FAQ Search Feature'; message = `Hello, - \nI used your FAQ search feature and was pleased with the result. Details below: - \nQuery: ${query}\nBest Tokens: ${bestToken}\nFeedback: ${feedback}\nTimestamp: ${timestamp} - \nI also have some additional suggestions: + \nI used your FAQ search feature and was pleased with the result. + \nI also have some additional suggestions: ... + \nBest Tokens:\n\n${bestAnswersAsString}\nFeedback: ${feedback}\nTimestamp: ${timestamp}\nQuery: ${query} \nKeep it up! \nBest regards`; } else if (feedback === 'down') { subject = 'Negative Feedback: FAQ Search Feature'; message = `Hello, - \nI tried used FAQ search feature, and unfortunately, I was not satisfied with this result. Detail below: - \nQuery: ${query}\nBest Tokens: ${bestToken}\nFeedback: ${feedback}\nTimestamp: ${timestamp} - \nI also have some additional suggestions: + \nI tried used FAQ search feature, and unfortunately, I was not satisfied with this result. + \nI also have some additional suggestions: ... + Detail below: + \nBest Tokens: ${bestAnswersAsString}\nFeedback: ${feedback}\nTimestamp: ${timestamp}\nQuery: ${query} \nI hope this feedback lets you improve the experience. \nBest regards`; } diff --git a/modules/ui.js b/modules/ui.js index a1ca671..42907bc 100644 --- a/modules/ui.js +++ b/modules/ui.js @@ -1,21 +1,17 @@ import { submitFeedback } from "./feedback.js"; /** handle what needs to be displayed in chat container */ -export function appendMessage(content, className, linkId = null, akkordeonId = null, bestToken = null, query = null) { +export function appendMessage(content, className, linkId = null, akkordeonId = null, bestToken = null, query = null, answer = null) { const messageContainer = document.createElement('div'); if (className === 'user-message') { - const messageDiv = document.createElement('div'); messageDiv.textContent = content; messageDiv.className = className; messageContainer.appendChild(messageDiv); - - } else { - /** drawing what chat response looks like */ messageContainer.className = 'bot-message-container'; const numberBox = document.createElement('div'); @@ -29,18 +25,42 @@ export function appendMessage(content, className, linkId = null, akkordeonId = n const panelDiv = document.createElement('div'); panelDiv.textContent = '➜'; panelDiv.className = 'info-panel'; - /** now the user can copy the relevant question to clipboard and use the browser search */ - panelDiv.onclick = function () { + + panelDiv.setAttribute('data-question', encodeURIComponent(content)); + + messageDiv.onclick = function () { copyToClipboard(content); }; - messageContainer.appendChild(numberBox); - messageContainer.appendChild(messageDiv); - messageContainer.appendChild(panelDiv); - } - - + panelDiv.onclick = function (event) { + + event.stopPropagation(); + + let nextElem = messageContainer.nextElementSibling; + if (nextElem && nextElem.classList.contains('answer-container')) { + nextElem.remove(); + panelDiv.classList.remove('active'); + return; + } + + let highlightToken = answer.replace(bestToken, `<span class="highlight">${bestToken}</span>`); + + const answerContainer = document.createElement('div'); + answerContainer.className = 'answer-container'; + + answerContainer.innerHTML = highlightToken; + + messageContainer.parentNode.insertBefore(answerContainer, messageContainer.nextSibling); + panelDiv.classList.add('active'); + } + + + messageContainer.appendChild(numberBox); + messageContainer.appendChild(messageDiv); + messageContainer.appendChild(panelDiv); + } + const messagesDiv = document.getElementById('messages'); messagesDiv.appendChild(messageContainer); messagesDiv.scrollTop = messagesDiv.scrollHeight; @@ -52,6 +72,19 @@ export function sendMessage(worker) { const userInput = document.getElementById('user-input'); const message = userInput.value.trim(); + + const resultsIframe = parent.document.getElementById('resultsIframe'); + const results = resultsIframe.contentDocument || resultsIframe.contentWindow.document; + const resultsContainer = results.getElementById('results'); + + /** hide results when message is sent */ + resultsContainer.innerHTML = ''; + setTimeout(() => { + resultsIframe.style.display = 'none'; + }, 500); + + resultsIframe.style.display = 'block'; + resultsContainer.innerHTML = ''; if (message !== "") { @@ -77,21 +110,34 @@ export function copyToClipboard(text) { } /** show user that clipboard action was successful */ -export function copySuccsess() { +let currentConfirm = null; +let showTimeoutId = null; +let hideTimeoutId = null; +let removeTimeoutId = null; + +function copySuccsess() { - const confirm = document.createElement('div'); - confirm.innerText = 'Copied to clipboard!'; - confirm.classList.add('confirm'); - document.body.appendChild(confirm); + if (currentConfirm) { + clearTimeout(showTimeoutId); + clearTimeout(hideTimeoutId); + clearTimeout(removeTimeoutId); + currentConfirm.remove(); + } - setTimeout(() => { - confirm.classList.add('visible'); + currentConfirm = document.createElement('div'); + currentConfirm.innerText = 'Question copied to clipboard!'; + currentConfirm.classList.add('confirm'); + document.body.appendChild(currentConfirm); + + showTimeoutId = setTimeout(() => { + currentConfirm.classList.add('visible'); }, 10); - setTimeout(() => { - confirm.classList.remove('visible'); - setTimeout(() => { - confirm.remove(); + hideTimeoutId = setTimeout(() => { + currentConfirm.classList.remove('visible'); + removeTimeoutId = setTimeout(() => { + currentConfirm.remove(); + currentConfirm = null; }, 500); }, 2000); } @@ -132,13 +178,13 @@ export function drawUserInfo() { const messagesDiv = document.getElementById('messages'); const infoDiv = document.createElement('div'); - infoDiv.className = 'bot-message'; - infoDiv.innerHTML = "These question panels might be useful to you:<br>"; + infoDiv.className = 'bot-intro'; + infoDiv.innerHTML = "This might be useful:<br>"; messagesDiv.appendChild(infoDiv); } -/** new message to inform user of the limited usability */ +/** new message to inform user of the limited usability; out for now*/ export function discloseLimitations() { const messagesDiv = document.getElementById('messages'); diff --git a/modules/workerInterface.js b/modules/workerInterface.js index 1cb6abd..147917c 100644 --- a/modules/workerInterface.js +++ b/modules/workerInterface.js @@ -23,10 +23,17 @@ export function handleWorker(events){ case 'searchResults': const bestTokens = results.map(result => result.bestToken); + const questions = results.map(result => result.question); + const bestAnswer = {}; - drawFeedbackButtons(query, bestTokens); + for (let i = 0; i < questions.length; i++) { + bestAnswer[questions[i]] = bestTokens[i]; + } + + drawFeedbackButtons(query, bestAnswer); drawUserInfo(); + /** pass our three results to ui so they can be displayed and worked with in chat */ results.forEach(result => { appendMessage( @@ -35,11 +42,12 @@ export function handleWorker(events){ result.buttonId, result.akkordeonId, result.bestToken, - query + query, + result.answer ); }); - discloseLimitations(); + /**discloseLimitations();*/ break; @@ -75,12 +83,12 @@ function exportAsJson(data, fileName) { const jsonString = JSON.stringify(data, null, 2); const blob = new Blob([jsonString], { type: 'application/json' }); - const url = URL.createObjectURL(blob); + const url = URL.createbestAnswerectURL(blob); const link = document.createElement('a'); link.href = url; link.download = fileName; link.click(); - URL.revokeObjectURL(url); + URL.revokebestAnswerectURL(url); } \ No newline at end of file diff --git a/styles.css b/styles.css index 255f9a1..9ec13c6 100644 --- a/styles.css +++ b/styles.css @@ -9,9 +9,9 @@ width: 100%; max-height: 400px; overflow-x: hidden; - overflow-y: auto; display: flex; flex-direction: column; + overflow-y: hidden; } @@ -73,8 +73,6 @@ } - - .user-message { align-self: flex-end; background-color: #007bff; @@ -87,18 +85,41 @@ text-align: center; } - .bot-message { + + + .answer-container { + white-space: pre-wrap; align-self: flex-start; background-color: #003865; color: white; - margin-left: 10px; + margin-left: 52px; margin: 10px 2; max-width: 60%; word-wrap: break-word; - text-align: middle; + cursor: default; + opacity: 0; + animation: showUp 0.3s forwards; + + } + + .highlight { + background-color: rgb(59, 136, 172); + color: white; /* Adjust as needed for contrast */ + } + + @keyframes showUp { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } } + .info-for-user { position: relative; margin-right: auto; @@ -112,21 +133,57 @@ display: inline-flex; align-items: center; height: auto; - position: relative; gap: 20px; padding: 50px; border-radius: 4px; font-size: 14px; max-width: 50%; - vertical-align: top; margin-right: 20px; } + .bot-message { + align-self: flex-start; + background-color: #003865; + color: white; + margin-left: 10px; + margin: 10px 2; + max-width: 60%; + word-wrap: break-word; + text-align: middle; + z-index: 100; + cursor: pointer; + + } + + .bot-intro { + align-self: flex-start; + background-color: #003865; + color: white; + margin-left: 10px; + margin: 10px 2; + max-width: 60%; + word-wrap: break-word; + text-align: middle; + + } + + + .bot-message-container { display: flex; align-items: stretch; + + + } + + .bot-message:hover { + background-color: #002144; + } + + .bot-message:active { + transform: scale(1.03); } @@ -148,7 +205,7 @@ } .info-panel::before { - content: "Copy to Clipboard"; + content: "show full answer"; position: absolute; background-color: rgba(0, 0, 0, 0.8); color: white; @@ -163,13 +220,20 @@ left: 50px; max-width: 300px; } + + .info-panel.active::before { + content: "hide answer"; + } .info-panel:hover::before { opacity: 1; visibility: visible; } - + .info-panel:active { + transform: scale(1.03); + } + .number-box { background-color: #aeb5bc; color: white; diff --git a/worker.js b/worker.js index 08d5f91..9930d39 100644 --- a/worker.js +++ b/worker.js @@ -56,7 +56,7 @@ async function getResultsWithTokens(faqResults, query) { return await Promise.all( faqResults.map(async (result) => { const { bestToken } = await findBestToken(query, result.answer); - return { ...result, bestToken }; + return { ...result, bestToken}; }) ); } -- GitLab