Skip to content

Commit

Permalink
New chat page, for #96 Chat with your website functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
m-i-l committed Dec 3, 2023
1 parent 8816694 commit be30c6a
Show file tree
Hide file tree
Showing 3 changed files with 172 additions and 98 deletions.
28 changes: 15 additions & 13 deletions src/web/content/dynamic/searchmysite/search/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import config
import searchmysite.solr
from searchmysite.searchutils import get_search_params, get_query_vector_string, get_start, get_filter_queries, do_search, do_vector_search, get_no_of_results, get_page_range, get_links, get_display_pagination, get_display_facets, get_display_results
from searchmysite.adminutils import select_indexed_domains
import os

bp = Blueprint('search', __name__)
Expand Down Expand Up @@ -64,24 +65,25 @@ def browse(search_type='browse'):
def chat(search_type='chat'):

# Get params and data required to perform search
params = get_search_params(request, search_type)
groupbydomain = False # Browse only returns home pages, so will only have one result per domain
query = params['q']
if query != '*':
query_vector_string = get_query_vector_string(query)
#params = get_search_params(request, search_type)
#groupbydomain = False # Browse only returns home pages, so will only have one result per domain
#query = params['q']
#if query != '*':
# query_vector_string = get_query_vector_string(query)

# Perform the actual search
search_results = do_vector_search(query_vector_string)
# search_results = do_vector_search(query_vector_string)
#current_app.logger.debug('search_results: {}'.format(search_results))

# Get data required to display the results
(total_results, total_domains) = get_no_of_results(search_results, groupbydomain) # groupbydomain False so both values will be the same
links = get_links(request, params, search_type)
display_results = get_display_results(search_results, groupbydomain, params, links['query_string'])
else:
display_results = None

return render_template('search/chat.html', params=params, subtitle='Chat', results=display_results)
# (total_results, total_domains) = get_no_of_results(search_results, groupbydomain) # groupbydomain False so both values will be the same
# links = get_links(request, params, search_type)
# display_results = get_display_results(search_results, groupbydomain, params, links['query_string'])
#else:
# display_results = None
#return render_template('search/chat.html', params=params, subtitle='Chat', results=display_results)
all_indexed_domains = select_indexed_domains()
return render_template('search/chat.html', subtitle='Chat', domains=all_indexed_domains)


@bp.route('/new/', methods=['GET', 'POST'])
Expand Down

This file was deleted.

192 changes: 157 additions & 35 deletions src/web/content/dynamic/searchmysite/templates/search/chat.html
Original file line number Diff line number Diff line change
@@ -1,47 +1,169 @@
{% extends "layoutwithchatbox.html" %}
{% extends "layout.html" %}

{% block title %}Search My Site - {{ subtitle }}{% endblock %}

{% block content %}

{% if results %}

<div class="row sms-results-list sms-t-48">
<div class="col-lg-12">
{% for result in results %}
<div class="search-result sms-b-72">
<h2 class="sms-h2 sms-b-8">
{% if result['contains_adverts'] %}<img src="/static/images/containsadverts44x24.gif" alt="Page contains adverts" class="float-left" style="margin-right: 4px">{% endif %}
{% if result['published_date'] %}<span id="result-published-date">{{ result['published_date'] }}</span>:{% endif %}
<a href="{{ result['url'] }}" class="result-title">{{ result['url'] }}</a>
</h2>
{% if result['content_chunk_text'] %}
<p id="result-hightlight" class="sms-p1">
{{ result['content_chunk_text'] }}
<!-- score: {{ result['score'] }} -->
<script>
document.addEventListener("DOMContentLoaded", function () {

// API endpoints
const searchAPI = "/api/v1/knnsearch/";
const llmAPI = "/api/v1/predictions/llama2";

// Elements
const chatContainer = document.getElementById('chat-container'); // Container for the chat prompts and responses
const messageInput = document.getElementById('message-input'); // User input, i.e. chat prompt
const sendMessage = document.getElementById("send-message"); // Send message button

// Question HTML template
// The <span id="question"></span> in the HTML template will be replaced by the question text.
const questionTemplate = `
<div class="d-flex flex-row justify-content-end mb-4">
<div class="p-3 ms-3" style="border-radius: 15px; background-color: var(--bs-gray-500);">
<p class="sms-p3 mb-0">
<span id="question"></span>
</p>
{% endif %}
{% if result['subresults'] %}
{% for subresult in result['subresults'] %}
<p class="sms-p3 sms-subresult"><a href="{{ subresult['url'] }}" class="result-title-other">{{ subresult['short_title'] }}</a></p>
{% endfor %}
<p class="sms-p3 sms-subresult"><a href="?{{ result['subresults_link'] }}" class="result-title-other">{{ result['subresults_link_text'] }}</a></p>
{% endif %}
</div>
{% endfor %}
</div>
</div>
</div>
</div>`;

// Response HTML template
// The <span id="response-search"></span> in the HTML template will be replaced by the result from the vector search.
// The <span id="response-llm"></span> in the HTML template will be replaced firstly by the spinner and then by the actual response from the LLM.
const responseTemplate = `
<div class="d-flex flex-row justify-content-start mb-4">
<div class="p-3 me-3 border" style="border-radius: 15px; background-color: var(--bs-gray-200);">
<p class="sms-p3">
<span id="response-llm"></span>
</p>
<div class="sms-h-line"></div>
<p class="sms-p4 mt-2 mb-0">
<span id="response-search"></span>
</p>
</div>
</div>`;

// The main function is activated on click of the send message button
sendMessage.addEventListener("click", function () {

// Get user input
// Get the chat query, i.e. question
const question = messageInput.value;
// Get the domain to interact with, or * to interact with all domains
let domain = "*"; // Default value
if(document.getElementById('domain-datalist').value.trim() != '') {
domain = document.getElementById('domain-datalist').value.trim();
if(domain == 'All domains') {
domain = "*"
}
}
// Construct the vector search API call
let searchQuery = searchAPI + '?q=' + question + '&domain=' + domain

{% else %}
if(question != '') { // Don't do anything if nothing has been entered

<pre>
</pre>
<div class="row">
<div class="col-lg-12">
<p class="sms-p2">Please enter a question.</p>
fetch(searchQuery)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data => {
let searchResults = data;
let result = searchResults[0];

// Create HTML components for insertion into the HTML templates
// <div class="spinner-border" id="spinner" role="status"></div>
const spinnerHTML = document.createElement('div');
spinnerHTML.className = "spinner-border"
spinnerHTML.id = "spinner"
spinnerHTML.role = "status"
// <span>Source: <a href="${result.url}" target="_blank">$result.url}</a>, score:${results.score}</span>
const sourceText = document.createTextNode('Source: ');
const sourceLink = document.createElement('a');
sourceLink.setAttribute('href', result.url);
sourceLink.setAttribute('target', '_blank');
sourceLink.innerHTML = result.url;
const scoreText = document.createTextNode(', score: ' + result.score);
const responseSearch = document.createElement('span');
responseSearch.appendChild(sourceText);
responseSearch.appendChild(sourceLink);
responseSearch.appendChild(scoreText);

// Display data in the container
chatContainer.innerHTML += questionTemplate + responseTemplate;
// Insert the question into the question HTML template
const blankQuestion = document.getElementById('question');
blankQuestion.replaceWith(question);
// Insert the spinner into the response HTML template (pending response from the LLM)
const blankResponse = document.getElementById('response-llm');
blankResponse.replaceWith(spinnerHTML);
// Insert the response from the vector search into the response HTML template
const blankResponseSearch = document.getElementById('response-search');
blankResponseSearch.replaceWith(responseSearch);

// Ready the display for the next question
//chatContainer.scrollTop = chatContainer.scrollHeight; // Scroll to the bottom of the chat container
messageInput.value = ''; // Clear the input field ready for the next question

// Now construct the LLM API query and fetch the response
let llmQuery = llmAPI + '?q=' + question + '&prompt=qa&context=' + result.content_chunk_text;
return fetch(llmQuery)
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data => {
// Hide spinner
let spinner = document.getElementById("spinner"); // Spinner shown while awaiting for response
spinner.replaceWith(data);
})
.catch(error => {
// Handle errors
console.error("Fetch error: ", error);
chatContainer.innerHTML = "<p>Error loading data.</p>";
});
}

})
})
</script>
<p class="sms-p3">
Please note:
<ul class="sms-list-3">
<li>
This service is experimental, is running on a relatively low powered machine so is very slow, and may sometimes generate incoherent results.
</li>
<li>
If you don't see a result you expect it might be because the content falls outside the
page and embedding limits shown on <a href="/admin/add/">Add Site</a>.
</li>
</ul>
</p>

<div class="card">
<div class="card-body" id="chat-container">
</div>
</div>
<div class="card-footer">
<div class="input-group">
<input type="text" id="message-input" class="form-control sms-search-input sms-large-font" placeholder="Ask a question">
<button class="input-group-text sms-search-btn" id="send-message">
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="currentColor" class="bi bi-send" viewBox="0 0 16 16">
<path d="M15.854.146a.5.5 0 0 1 .11.54l-5.819 14.547a.75.75 0 0 1-1.329.124l-3.178-4.995L.643 7.184a.75.75 0 0 1 .124-1.33L15.314.037a.5.5 0 0 1 .54.11ZM6.636 10.07l2.761 4.338L14.13 2.576 6.636 10.07Zm6.787-8.201L1.591 6.602l4.339 2.76 7.494-7.493Z"/>
</svg>
</button>
</div>
<input class="form-control" list="domains" id="domain-datalist" placeholder="All domains">
<datalist id="domains">
<option value="All domains" selected>{% for domain in domains %}
<option value="{{ domain }}">{% endfor %}
</datalist>

{% endif %}
</div>
</div>

{% endblock %}

0 comments on commit be30c6a

Please sign in to comment.