text
diff --git a/web-app/django/VIM/apps/instruments/management/commands/index_data.py b/web-app/django/VIM/apps/instruments/management/commands/index_data.py
index 8ca2b99a..03ea8c36 100644
--- a/web-app/django/VIM/apps/instruments/management/commands/index_data.py
+++ b/web-app/django/VIM/apps/instruments/management/commands/index_data.py
@@ -66,6 +66,7 @@ def handle(self, *args, **options):
hbs_code = instrument["hbs_prim_cat_s"]
instrument["hbs_prim_cat_label_s"] = self.HBS_LABEL_MAP.get(hbs_code, "")
+ languages_with_name = set()
for name_entry in instrument.pop("instrument_names_by_language", []):
instrument_name_field = f"instrument_name_{name_entry['lang']}_ss"
instrument_umil_label_field = (
@@ -77,6 +78,10 @@ def handle(self, *args, **options):
instrument[instrument_name_field].append(name_entry["name"])
if name_entry.get("umil_label"):
instrument[instrument_umil_label_field] = name_entry["name"]
+ if name_entry.get("name"):
+ languages_with_name.add(name_entry["lang"])
+
+ instrument["instrument_label_count_i"] = len(languages_with_name)
# Initialize Solr client
solr = pysolr.Solr(settings.SOLR_URL, timeout=10, always_commit=True)
diff --git a/web-app/django/VIM/apps/instruments/views/solr_suggest.py b/web-app/django/VIM/apps/instruments/views/solr_suggest.py
new file mode 100644
index 00000000..5f2f5873
--- /dev/null
+++ b/web-app/django/VIM/apps/instruments/views/solr_suggest.py
@@ -0,0 +1,59 @@
+# views.py
+from django.http import JsonResponse
+from django.views import View
+import requests
+from django.conf import settings
+
+SOLR_SUGGEST_URL = f"{settings.SOLR_URL}/suggest"
+
+
+class SolrSuggest(View):
+ """
+ Returns suggestions from Solr suggester directly.
+ """
+
+ def get(self, request):
+ query = request.GET.get("q", "").strip()
+ if not query:
+ return JsonResponse({"suggestions": []})
+ solr_query = f"{query}"
+
+ # Hit Solr suggest endpoint
+ try:
+ response = requests.get(
+ SOLR_SUGGEST_URL, params={"q": solr_query, "wt": "json"}
+ )
+ response.raise_for_status()
+ except requests.RequestException:
+ return JsonResponse({"suggestions": []}, status=500)
+
+ data = response.json()
+ suggestions = []
+
+ # Extract terms from Solr response and limit to top 5 case-insensitive
+ try:
+ suggest_data = data.get("suggest", {}).get("default", {})
+ suggestions = []
+ seen = set()
+
+ if suggest_data:
+ first_key = list(suggest_data.keys())[0]
+ entries = suggest_data[first_key].get("suggestions", [])
+
+ for e in entries:
+ term = e["term"]
+ term_lower = term.lower()
+ if term_lower not in seen:
+ seen.add(term_lower)
+ # Replace matched part with query string to preserve casing
+ if term_lower.startswith(query.lower()):
+ rest_part = term[len(query) :]
+ term = f"{query}{rest_part}"
+
+ suggestions.append(term)
+ if len(suggestions) >= 5:
+ break
+ except Exception:
+ suggestions = []
+
+ return JsonResponse({"suggestions": suggestions})
diff --git a/web-app/django/VIM/templates/base.html b/web-app/django/VIM/templates/base.html
index d48cc35d..940d4833 100644
--- a/web-app/django/VIM/templates/base.html
+++ b/web-app/django/VIM/templates/base.html
@@ -25,6 +25,7 @@
{% vite_asset 'src/main.ts' %}
+ {% vite_asset 'src/instruments/Suggest.ts' %}
{% block ts_files %}
{% endblock ts_files %}
@@ -71,15 +72,20 @@
class="nav-link {% if active_tab == 'about' %}active{% endif %}">About
-
diff --git a/web-app/django/VIM/urls.py b/web-app/django/VIM/urls.py
index cb07e251..04e5c9cd 100644
--- a/web-app/django/VIM/urls.py
+++ b/web-app/django/VIM/urls.py
@@ -19,6 +19,7 @@
from django.urls import path, include
from django.conf import settings
from VIM.apps.instruments.views.instrument_list import InstrumentList
+from VIM.apps.instruments.views.solr_suggest import SolrSuggest
from VIM.apps.instruments.views.instrument_detail import InstrumentDetail
from VIM.apps.instruments.views.update_umil_db import update_umil_db
from VIM.apps.instruments.views.create_instrument import (
@@ -42,12 +43,13 @@
check_duplicate_names,
name="api-check-duplicate-names",
),
+ path("instruments/suggest/", SolrSuggest.as_view(), name="solr-suggest"),
path(
- "instrument//",
+ "instrument//",
InstrumentDetail.as_view(),
name="instrument-detail",
),
- path("instrument//names/", update_umil_db, name="update-umil-db"),
+ path("instrument//names/", update_umil_db, name="update-umil-db"),
path(
"api/instrument//delete/",
delete_instrument,
diff --git a/web-app/frontend/src/instruments/Suggest.ts b/web-app/frontend/src/instruments/Suggest.ts
new file mode 100644
index 00000000..4807b083
--- /dev/null
+++ b/web-app/frontend/src/instruments/Suggest.ts
@@ -0,0 +1,89 @@
+async function fetchSuggestions(query: string): Promise {
+ if (!query.trim()) return [];
+ try {
+ const response = await fetch(
+ `/instruments/suggest/?q=${encodeURIComponent(query)}`,
+ );
+ if (!response.ok) return [];
+ const data = await response.json();
+ return Array.isArray(data.suggestions) ? data.suggestions : [];
+ } catch {
+ return [];
+ }
+}
+
+function renderSuggestionList(
+ listElement: HTMLElement,
+ suggestions: string[],
+ input: HTMLInputElement,
+ form: HTMLFormElement,
+) {
+ listElement.innerHTML = '';
+ if (suggestions.length === 0) {
+ const empty = document.createElement('div');
+ empty.className = 'list-group-item text-muted';
+ empty.textContent = 'No suggestions';
+ listElement.appendChild(empty);
+ return;
+ }
+ suggestions.forEach((suggestion) => {
+ const item = document.createElement('div');
+ item.className =
+ 'list-group-item list-group-item-action notranslate force-ltr';
+ item.innerHTML = suggestion;
+ // On click, set input value without tags
+ item.addEventListener('click', () => {
+ const temp = document.createElement('div');
+ temp.innerHTML = suggestion;
+ input.value = temp.textContent || temp.innerText || '';
+ form.submit();
+ });
+ listElement.appendChild(item);
+ });
+}
+
+window.addEventListener('DOMContentLoaded', () => {
+ const input = document.getElementById(
+ 'instrument-search',
+ ) as HTMLInputElement | null;
+ const list = document.getElementById(
+ 'autocomplete-list',
+ ) as HTMLElement | null;
+
+ const form = input?.closest('form') as HTMLFormElement | null;
+
+ if (!input || !list || !form) return;
+
+ function showList() {
+ list.classList.remove('d-none');
+ }
+
+ function hideList() {
+ list.classList.add('d-none');
+ }
+
+ let debounceTimer: number | null = null;
+
+ input.addEventListener('input', () => {
+ const val = input.value.trim();
+ if (debounceTimer) {
+ window.clearTimeout(debounceTimer);
+ }
+ if (val.length > 1) {
+ debounceTimer = window.setTimeout(async () => {
+ const suggestions = await fetchSuggestions(val);
+ renderSuggestionList(list, suggestions, input, form);
+ showList();
+ }, 150);
+ } else {
+ hideList();
+ list.innerHTML = '';
+ }
+ });
+
+ document.addEventListener('click', (event) => {
+ if (event.target !== input && !list.contains(event.target as Node)) {
+ hideList();
+ }
+ });
+});