Merge pull request #34 from LD-Reborn/28-create-a-front-end---recent-queries
28 create a front end recent queries
This commit is contained in:
@@ -105,4 +105,27 @@ public class SearchdomainController : ControllerBase
|
||||
}
|
||||
return Ok(new SearchdomainUpdateResults(){Success = true});
|
||||
}
|
||||
|
||||
[HttpGet("GetSearches")]
|
||||
public ActionResult<SearchdomainSearchesResults> GetSearches(string searchdomain)
|
||||
{
|
||||
Searchdomain searchdomain_;
|
||||
try
|
||||
{
|
||||
searchdomain_ = _domainManager.GetSearchdomain(searchdomain);
|
||||
}
|
||||
catch (SearchdomainNotFoundException)
|
||||
{
|
||||
_logger.LogError("Unable to retrieve the searchdomain {searchdomain} - it likely does not exist yet", [searchdomain]);
|
||||
return Ok(new SearchdomainSearchesResults() { Searches = [], Success = false, Message = "Searchdomain not found" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Unable to retrieve the searchdomain {searchdomain} - {ex.Message} - {ex.StackTrace}", [searchdomain, ex.Message, ex.StackTrace]);
|
||||
return Ok(new SearchdomainSearchesResults() { Searches = [], Success = false, Message = ex.Message });
|
||||
}
|
||||
Dictionary<string, DateTimedSearchResult> searchCache = searchdomain_.searchCache;
|
||||
|
||||
return Ok(new SearchdomainSearchesResults() { Searches = searchCache, Success = true });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Data.Common;
|
||||
using ElmahCore.Mvc.Logger;
|
||||
using MySql.Data.MySqlClient;
|
||||
using Server.Helper;
|
||||
using Shared.Models;
|
||||
|
||||
namespace Server;
|
||||
|
||||
@@ -13,7 +14,7 @@ public class Searchdomain
|
||||
public AIProvider aIProvider;
|
||||
public string searchdomain;
|
||||
public int id;
|
||||
public Dictionary<string, List<(DateTime, List<(float, string)>)>> searchCache; // Yeah look at this abomination. searchCache[x][0] = last accessed time, searchCache[x][1] = results for x
|
||||
public Dictionary<string, DateTimedSearchResult> searchCache; // Key: query, Value: Search results for that query (with timestamp)
|
||||
public List<Entity> entityCache;
|
||||
public List<string> modelsInUse;
|
||||
public Dictionary<string, Dictionary<string, float[]>> embeddingCache;
|
||||
@@ -22,8 +23,6 @@ public class Searchdomain
|
||||
public SQLHelper helper;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
// TODO Add settings and update cli/program.cs, as well as DatabaseInsertSearchdomain()
|
||||
|
||||
public Searchdomain(string searchdomain, string connectionString, AIProvider aIProvider, Dictionary<string, Dictionary<string, float[]>> embeddingCache, ILogger logger, string provider = "sqlserver", bool runEmpty = false)
|
||||
{
|
||||
_connectionString = connectionString;
|
||||
@@ -47,6 +46,7 @@ public class Searchdomain
|
||||
|
||||
public void UpdateEntityCache()
|
||||
{
|
||||
InvalidateSearchCache();
|
||||
Dictionary<string, dynamic> parametersIDSearchdomain = new()
|
||||
{
|
||||
["id"] = this.id
|
||||
@@ -151,8 +151,14 @@ public class Searchdomain
|
||||
embeddingCache = []; // TODO remove this and implement proper remediation to improve performance
|
||||
}
|
||||
|
||||
public List<(float, string)> Search(string query, bool sort=true)
|
||||
public List<(float, string)> Search(string query)
|
||||
{
|
||||
if (searchCache.TryGetValue(query, out DateTimedSearchResult cachedResult))
|
||||
{
|
||||
cachedResult.AccessDateTimes.Add(DateTime.Now);
|
||||
return [.. cachedResult.Results.Select(r => (r.Score, r.Name))];
|
||||
}
|
||||
|
||||
if (!embeddingCache.TryGetValue(query, out Dictionary<string, float[]>? queryEmbeddings))
|
||||
{
|
||||
queryEmbeddings = Datapoint.GenerateEmbeddings(query, modelsInUse, aIProvider);
|
||||
@@ -181,8 +187,13 @@ public class Searchdomain
|
||||
}
|
||||
result.Add((entity.probMethod(datapointProbs), entity.name));
|
||||
}
|
||||
|
||||
return [.. result.OrderByDescending(s => s.Item1)]; // [.. element] = element.ToList()
|
||||
List<(float, string)> results = [.. result.OrderByDescending(s => s.Item1)];
|
||||
List<ResultItem> searchResult = new(
|
||||
[.. results.Select(r =>
|
||||
new ResultItem(r.Item1, r.Item2 ))]
|
||||
);
|
||||
searchCache[query] = new DateTimedSearchResult(DateTime.Now, searchResult);
|
||||
return results;
|
||||
}
|
||||
|
||||
public static List<string> GetModels(List<Entity> entities)
|
||||
@@ -217,4 +228,9 @@ public class Searchdomain
|
||||
reader.Close();
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public void InvalidateSearchCache()
|
||||
{
|
||||
searchCache = [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,9 @@ public class SearchdomainManager
|
||||
|
||||
public void InvalidateSearchdomainCache(string searchdomainName)
|
||||
{
|
||||
GetSearchdomain(searchdomainName).UpdateEntityCache();
|
||||
var searchdomain = GetSearchdomain(searchdomainName);
|
||||
searchdomain.UpdateEntityCache();
|
||||
searchdomain.InvalidateSearchCache(); // TODO implement cache remediation (Suggestion: searchdomain-wide setting for cache remediation / invalidation - )
|
||||
}
|
||||
|
||||
public List<string> ListSearchdomains()
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
@using Server.Models
|
||||
@using System.Web
|
||||
@using Server.Services
|
||||
@inject LocalizationService T
|
||||
@model HomeIndexViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Home Page";
|
||||
@@ -81,15 +83,17 @@
|
||||
placeholder="filter"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@* <div class="list-row">
|
||||
<span>Some test query</span>
|
||||
<button class="btn btn-primary btn-sm">Details</button>
|
||||
</div>
|
||||
<div class="list-row">
|
||||
<span>Some other test query</span>
|
||||
<button class="btn btn-primary btn-sm">Details</button>
|
||||
</div> *@
|
||||
<div class="spinner d-none"></div>
|
||||
<table id="queriesTable" class="table table-striped" style="max-height: 60vh; overflow-y: auto; display: block;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="visually-hidden">Name</th>
|
||||
<th class="visually-hidden">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -116,15 +120,6 @@
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@* <div class="list-row">
|
||||
<span>Someentity</span>
|
||||
<button class="btn btn-primary btn-sm">Details</button>
|
||||
</div>
|
||||
<div class="list-row">
|
||||
<span>Some other test query</span>
|
||||
<button class="btn btn-primary btn-sm">Details</button>
|
||||
</div> *@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -182,10 +177,52 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Query Details Modal -->
|
||||
<div class="modal fade" id="queryDetailsModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="queryDetailsTitle">Query Details</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
|
||||
<!-- Access times -->
|
||||
<h6>Access times</h6>
|
||||
<ul id="queryAccessTimes" class="list-group mb-4"></ul>
|
||||
|
||||
<!-- Results -->
|
||||
<h6>Results</h6>
|
||||
<table class="table table-sm table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Score</th>
|
||||
<th>Name</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="queryResultsBody"></tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
var domains = JSON.parse('@Html.Raw(System.Text.Json.JsonSerializer.Serialize(domains))');
|
||||
var entities = null;
|
||||
var queries = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const filterInput = document.getElementById('entitiesFilter');
|
||||
@@ -195,36 +232,67 @@
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const entitiesFilter = document.getElementById('entitiesFilter');
|
||||
entitiesFilter.addEventListener('input', () => {
|
||||
populateEntitiesTable(entitiesFilter.value);
|
||||
});
|
||||
|
||||
const queriesFilter = document.querySelector(
|
||||
'#queriesTable'
|
||||
).closest('.card-body').querySelector('input');
|
||||
|
||||
queriesFilter.addEventListener('input', () => {
|
||||
populateQueriesTable(queriesFilter.value);
|
||||
});
|
||||
});
|
||||
|
||||
function selectDomain(domainKey) {
|
||||
// Deselect all domain items
|
||||
document.querySelectorAll('.domain-item').forEach(item => {
|
||||
item.classList.remove('active');
|
||||
});
|
||||
|
||||
// Select the clicked domain item
|
||||
var selectedItem = document.getElementById('sidebar_domain_' + domainKey);
|
||||
selectedItem.classList.add('active');
|
||||
|
||||
// Update main content header
|
||||
var domainName = domains[domainKey];
|
||||
document.querySelector('.section-card h4').innerText = domainName;
|
||||
|
||||
// Request the entities from that searchdomain
|
||||
let url = `/Entity/List?searchdomain=${encodeURIComponent(domainName)}&returnEmbeddings=false`;
|
||||
let table = document.querySelector("#entitiesTable").parentElement;
|
||||
/* ---------- ENTITIES ---------- */
|
||||
let entitiesUrl = `/Entity/List?searchdomain=${encodeURIComponent(domainName)}&returnEmbeddings=false`;
|
||||
let entitiesCard = document.querySelector("#entitiesTable").parentElement;
|
||||
clearEntitiesTable();
|
||||
showEntitiesLoading(table);
|
||||
fetch(url)
|
||||
.then(response => response.json())
|
||||
showEntitiesLoading(entitiesCard);
|
||||
|
||||
fetch(entitiesUrl)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
entities = data.Results;
|
||||
populateEntitiesTable();
|
||||
hideEntitiesLoading(table);
|
||||
hideEntitiesLoading(entitiesCard);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching entities:', error);
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
flagSearchdomainAsErroneous(domainKey);
|
||||
hideEntitiesLoading(table);
|
||||
hideEntitiesLoading(entitiesCard);
|
||||
});
|
||||
|
||||
/* ---------- QUERIES ---------- */
|
||||
let queriesUrl = `/Searchdomain/GetSearches?searchdomain=${encodeURIComponent(domainName)}`;
|
||||
let queriesCard = document.querySelector("#queriesTable").parentElement;
|
||||
clearQueriesTable();
|
||||
showQueriesLoading(queriesCard);
|
||||
|
||||
fetch(queriesUrl)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
queries = Object.entries(data.Searches).map(key => ({"Name": key[0], "AccessDateTimes": key[1].AccessDateTimes, "Results": key[1].Results}));
|
||||
populateQueriesTable();
|
||||
hideQueriesLoading(queriesCard);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Error fetching queries:', err);
|
||||
hideQueriesLoading(queriesCard);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -233,6 +301,10 @@
|
||||
tableBody.innerHTML = '';
|
||||
}
|
||||
|
||||
function clearQueriesTable() {
|
||||
document.querySelector('#queriesTable tbody').innerHTML = '';
|
||||
}
|
||||
|
||||
function populateEntitiesTable(filterText = '') {
|
||||
if (!entities) return;
|
||||
|
||||
@@ -266,6 +338,39 @@
|
||||
});
|
||||
}
|
||||
|
||||
function populateQueriesTable(filterText = '') {
|
||||
if (!queries) return;
|
||||
|
||||
const tbody = document.querySelector('#queriesTable tbody');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
const normalizedFilter = filterText.toLowerCase();
|
||||
|
||||
queries
|
||||
.filter(q => q.Name?.toLowerCase().includes(normalizedFilter))
|
||||
.forEach(query => {
|
||||
const row = document.createElement('tr');
|
||||
|
||||
const nameCell = document.createElement('td');
|
||||
nameCell.textContent = query.Name;
|
||||
nameCell.classList.add('col-md-12');
|
||||
row.appendChild(nameCell);
|
||||
|
||||
const actionCell = document.createElement('td');
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'btn btn-sm btn-primary';
|
||||
btn.textContent = '@T["Details"]';
|
||||
btn.addEventListener('click', () => {
|
||||
showQueryDetails(query);
|
||||
});
|
||||
|
||||
actionCell.appendChild(btn);
|
||||
row.appendChild(actionCell);
|
||||
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
function flagSearchdomainAsErroneous(domainKey) {
|
||||
var domainItem = document.getElementById('sidebar_domain_' + domainKey);
|
||||
domainItem.classList.add('list-group-item-danger');
|
||||
@@ -281,6 +386,16 @@
|
||||
element.querySelector('.spinner').classList.add('d-none');
|
||||
}
|
||||
|
||||
function showQueriesLoading(element = null) {
|
||||
if (!element) element = document;
|
||||
element.querySelector('.spinner').classList.remove('d-none');
|
||||
}
|
||||
|
||||
function hideQueriesLoading(element = null) {
|
||||
if (!element) element = document;
|
||||
element.querySelector('.spinner').classList.add('d-none');
|
||||
}
|
||||
|
||||
function showEntityDetails(entity) {
|
||||
// Title
|
||||
document.getElementById('entityDetailsTitle').innerText = entity.Name;
|
||||
@@ -329,4 +444,57 @@
|
||||
);
|
||||
modal.show();
|
||||
}
|
||||
|
||||
function showQueryDetails(query) {
|
||||
// Title
|
||||
document.getElementById('queryDetailsTitle').innerText =
|
||||
`Query: ${query.Name}`;
|
||||
|
||||
/* ---------- Access times ---------- */
|
||||
const accessList = document.getElementById('queryAccessTimes');
|
||||
accessList.innerHTML = '';
|
||||
|
||||
if (!query.AccessDateTimes || query.AccessDateTimes.length === 0) {
|
||||
accessList.innerHTML = `
|
||||
<li class="list-group-item text-muted text-center">
|
||||
No access times
|
||||
</li>`;
|
||||
} else {
|
||||
query.AccessDateTimes.forEach(dt => {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'list-group-item';
|
||||
li.textContent = new Date(dt).toLocaleString();
|
||||
accessList.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- Results ---------- */
|
||||
const resultsBody = document.getElementById('queryResultsBody');
|
||||
resultsBody.innerHTML = '';
|
||||
|
||||
if (!query.Results || query.Results.length === 0) {
|
||||
resultsBody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="2" class="text-muted text-center">
|
||||
No results
|
||||
</td>
|
||||
</tr>`;
|
||||
} else {
|
||||
query.Results.forEach(r => {
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `
|
||||
<td>${r.Score.toFixed(4)}</td>
|
||||
<td class="text-break">${r.Name}</td>
|
||||
`;
|
||||
resultsBody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
// Show modal
|
||||
const modal = new bootstrap.Modal(
|
||||
document.getElementById('queryDetailsModal')
|
||||
);
|
||||
modal.show();
|
||||
}
|
||||
|
||||
</script>
|
||||
19
src/Shared/Models/SearchdomainModels.cs
Normal file
19
src/Shared/Models/SearchdomainModels.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Shared.Models;
|
||||
public readonly struct ResultItem(float score, string name)
|
||||
{
|
||||
[JsonPropertyName("Score")]
|
||||
public readonly float Score { get; } = score;
|
||||
[JsonPropertyName("Name")]
|
||||
public readonly string Name { get; } = name;
|
||||
}
|
||||
|
||||
public struct DateTimedSearchResult(DateTime dateTime, List<ResultItem> results)
|
||||
{
|
||||
[JsonPropertyName("AccessDateTimes")]
|
||||
public List<DateTime> AccessDateTimes { get; set; } = [dateTime];
|
||||
[JsonPropertyName("Results")]
|
||||
public List<ResultItem> Results { get; set; } = results;
|
||||
}
|
||||
@@ -43,3 +43,14 @@ public class SearchdomainDeleteResults
|
||||
[JsonPropertyName("DeletedEntities")]
|
||||
public required int DeletedEntities { get; set; }
|
||||
}
|
||||
|
||||
public class SearchdomainSearchesResults
|
||||
{
|
||||
[JsonPropertyName("Success")]
|
||||
public required bool Success { get; set; }
|
||||
|
||||
[JsonPropertyName("Message")]
|
||||
public string? Message { get; set; }
|
||||
[JsonPropertyName("Searches")]
|
||||
public required Dictionary<string, DateTimedSearchResult> Searches { get; set; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user