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:
LD50
2025-12-14 23:22:06 +01:00
committed by GitHub
6 changed files with 277 additions and 38 deletions

View File

@@ -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 });
}
}

View File

@@ -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 = [];
}
}

View File

@@ -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()

View File

@@ -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>

View 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;
}

View File

@@ -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; }
}