diff --git a/chrome/content/zotero/preferences/embeddingsEndpoint.xhtml b/chrome/content/zotero/preferences/embeddingsEndpoint.xhtml
index 012a322794..cbb7de95eb 100644
--- a/chrome/content/zotero/preferences/embeddingsEndpoint.xhtml
+++ b/chrome/content/zotero/preferences/embeddingsEndpoint.xhtml
@@ -33,7 +33,7 @@
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:html="http://www.w3.org/1999/xhtml"
data-l10n-id="preferences-advanced-semantic-search-endpoint-dialog"
- onload="Zotero_EmbeddingsEndpointDialog.init()">
+ onload="init()">
-
+
@@ -82,98 +82,101 @@
diff --git a/chrome/content/zotero/preferences/preferences_advanced.js b/chrome/content/zotero/preferences/preferences_advanced.js
index 2e0f74b54e..5cbd885a42 100644
--- a/chrome/content/zotero/preferences/preferences_advanced.js
+++ b/chrome/content/zotero/preferences/preferences_advanced.js
@@ -113,6 +113,15 @@ Zotero_Preferences.Advanced = {
document.getElementById('semantic-search-endpoint-configure').addEventListener('command', () => {
this.openSemanticSearchEndpointDialog();
});
+
+ let diagnostics = document.getElementById('semantic-search-diagnostics');
+ let toggle = document.getElementById('semantic-search-diagnostics-toggle');
+ toggle.addEventListener('command', () => {
+ diagnostics.hidden = !diagnostics.hidden;
+ document.l10n.setAttributes(toggle, diagnostics.hidden
+ ? 'preferences-advanced-semantic-search-diagnostics-show'
+ : 'preferences-advanced-semantic-search-diagnostics-hide');
+ });
// The endpoint's stored verdict is read lazily; have it in memory
// before the status line first renders
Zotero.Embeddings.Endpoint.load().then(() => {
@@ -166,12 +175,11 @@ Zotero_Preferences.Advanced = {
statusBox.hidden = !status.enabled;
// Fulltext indexing only means something with a model selected
document.getElementById('semantic-search-index-fulltext').disabled = !status.enabled;
+ this.updateSemanticSearchEndpointUI(status.endpoint, status.enabled);
if (!status.enabled) {
return;
}
- this.updateSemanticSearchEndpointUI(status.endpoint);
-
// Phase / status message
let phaseLabel = document.getElementById('semantic-search-phase');
let hasRemaining = status.items.done < status.items.total
@@ -242,15 +250,16 @@ Zotero_Preferences.Advanced = {
document.getElementById('semantic-search-attachments-row').hidden
= !Zotero.Prefs.get('embeddings.indexFulltext');
this._updateSemanticSearchBar('attachments', status.chunks);
- this._updateSemanticSearchDiagnostics(status.diagnostics, status.eta);
+ this._updateSemanticSearchDiagnostics(status);
},
// Whether the active model can be served at all, and how the configured
// server stands (see Zotero.Embeddings.Endpoint.getStatus())
- updateSemanticSearchEndpointUI: function (endpoint) {
+ updateSemanticSearchEndpointUI: function (endpoint, enabled) {
let row = document.getElementById('semantic-search-endpoint-row');
- row.hidden = !Zotero.Embeddings.Endpoint.isSupported();
+ // Nothing can be said about serving without a model
+ row.hidden = !enabled || !Zotero.Embeddings.Endpoint.isSupported();
if (row.hidden) {
return;
}
@@ -281,7 +290,7 @@ Zotero_Preferences.Advanced = {
// Key/value rows of pipeline diagnostics for developers, so the labels
// are plain English rather than localized
- _updateSemanticSearchDiagnostics: function (diagnostics, eta) {
+ _updateSemanticSearchDiagnostics: function ({ items, chunks: indexed, diagnostics, eta }) {
let n = (value, digits = 0) => (value ?? 0).toLocaleString(undefined, {
maximumFractionDigits: digits, minimumFractionDigits: digits
});
@@ -301,6 +310,8 @@ Zotero_Preferences.Advanced = {
let rows = [];
let { window, run, engine, processes, slice, chunks } = diagnostics;
+ rows.push(['Items indexed', `${n(items.done)} / ${n(items.total)}`]);
+ rows.push(['Attachment chunks indexed', `${n(indexed.done)} / ${n(indexed.total)}`]);
rows.push(['ETA', eta === null ? '—' : duration(eta)]);
rows.push(['Throughput (2 min)', window ? speed(window) : '—']);
rows.push(['Inference speed (run)', run ? speed(run) : '—']);
@@ -341,8 +352,8 @@ Zotero_Preferences.Advanced = {
},
- // Fill one progress row with done out of total. The percentage is
- // rounded down, so it reads 100% only when everything is done.
+ // Fill one progress bar. The percentage is rounded down, so it reads
+ // 100% only when everything is done.
_updateSemanticSearchBar: function (name, { done, total }) {
let bar = document.getElementById(`semantic-search-${name}-progress`);
bar.max = Math.max(total, 1);
@@ -350,7 +361,7 @@ Zotero_Preferences.Advanced = {
document.l10n.setAttributes(
document.getElementById(`semantic-search-${name}-value`),
'preferences-advanced-semantic-search-progress-value',
- { done, total, percent: total ? Math.floor(done / total * 100) : 0 }
+ { percent: total ? Math.floor(done / total * 100) : 0 }
);
},
diff --git a/chrome/content/zotero/preferences/preferences_advanced.xhtml b/chrome/content/zotero/preferences/preferences_advanced.xhtml
index d76a66a8be..8984bb303a 100644
--- a/chrome/content/zotero/preferences/preferences_advanced.xhtml
+++ b/chrome/content/zotero/preferences/preferences_advanced.xhtml
@@ -309,37 +309,27 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
@@ -350,11 +340,6 @@
data-l10n-id="preferences-advanced-semantic-search-stop"
hidden="true"/>
-
-
-
-
@@ -367,7 +352,30 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/chrome/content/zotero/xpcom/embeddings.js b/chrome/content/zotero/xpcom/embeddings.js
index a032614905..005df60f5d 100644
--- a/chrome/content/zotero/xpcom/embeddings.js
+++ b/chrome/content/zotero/xpcom/embeddings.js
@@ -3926,7 +3926,8 @@ Zotero.Embeddings.Endpoint = new function () {
this.getStatus = function () {
let url = Zotero.Prefs.get('embeddings.endpoint') || '';
let status = { url, state: 'off', serverModel: null };
- if (!url) {
+ // A verdict is about a model, so there is none to report without one
+ if (!url || !Zotero.Embeddings.isEnabled()) {
return status;
}
if (_verdict === undefined) {
diff --git a/chrome/locale/en-US/zotero/preferences.ftl b/chrome/locale/en-US/zotero/preferences.ftl
index 1f33377dec..0a6454970b 100644
--- a/chrome/locale/en-US/zotero/preferences.ftl
+++ b/chrome/locale/en-US/zotero/preferences.ftl
@@ -102,7 +102,6 @@ preferences-advanced-semantic-search-chinese =
.label = Chinese
preferences-advanced-semantic-search-multilingual =
.label = Multilingual
-preferences-advanced-semantic-search-model-description = “{ preferences-advanced-semantic-search-english.label }” and “{ preferences-advanced-semantic-search-chinese.label }” give the best results for libraries in those languages. “{ preferences-advanced-semantic-search-multilingual.label }” supports searching in and across many languages.
preferences-advanced-semantic-search-index-fulltext =
.label = Search full text of attachment files
preferences-advanced-best-match-engine = Engine (temporary, for testing):
@@ -112,7 +111,8 @@ preferences-advanced-best-match-engine-lexical =
.label = Lexical
preferences-advanced-best-match-engine-semantic =
.label = Semantic
-preferences-advanced-best-match-margin = Drop results scoring more than this far below the best result (%):
+preferences-advanced-best-match-margin = Quality cutoff below top (%):
+preferences-advanced-best-match-margin-description = At 30%, results scoring up to 30% below the best one are kept; 100% keeps every result, 0 drops everything but the top match.
preferences-advanced-semantic-search-downloading = Downloading…
preferences-advanced-semantic-search-downloading-progress = Downloading… { $percent }%
preferences-advanced-semantic-search-extracting = Preparing documents…
@@ -127,9 +127,13 @@ preferences-advanced-semantic-search-resume =
.label = Resume
preferences-advanced-semantic-search-stop =
.label = Stop
-preferences-advanced-semantic-search-items = Items and notes
+preferences-advanced-semantic-search-items = Metadata, notes, annotations
preferences-advanced-semantic-search-attachments = Attachments
-preferences-advanced-semantic-search-progress-value = { $percent }% ({ $done } / { $total })
+preferences-advanced-semantic-search-progress-value = { $percent }%
+preferences-advanced-semantic-search-diagnostics-show =
+ .label = Show Diagnostics
+preferences-advanced-semantic-search-diagnostics-hide =
+ .label = Hide Diagnostics
preferences-advanced-semantic-search-switch-title = Change Mode?
preferences-advanced-semantic-search-switch-text = Changing the mode will rebuild the search index, which can take a long time for large libraries.
preferences-advanced-semantic-search-switch-button = Change Mode
diff --git a/scss/preferences/_advanced.scss b/scss/preferences/_advanced.scss
index 2cdc902992..115f5be347 100644
--- a/scss/preferences/_advanced.scss
+++ b/scss/preferences/_advanced.scss
@@ -98,40 +98,78 @@
margin-inline-start: 6px;
}
-#semantic-search-progress {
- display: grid;
- grid-template-columns: max-content 1fr max-content;
- align-items: center;
- column-gap: 8px;
- row-gap: 4px;
- margin-top: 4px;
+#semantic-search {
+ // One gap between rows; the controls' own block margins would make
+ // the gap depend on which control a row holds
+ row-gap: 6px;
- .semantic-search-progress-row {
- display: contents;
+ > hbox > :is(menulist, button, .html-input),
+ > vbox > hbox > :is(menulist, button) {
+ margin-block: 0;
+ }
+
+ // Sized to its first item at construction, before the models are added
+ #semantic-search-model {
+ min-width: 150px;
+ }
+
+ #best-match-margin {
+ width: 5em;
+ }
+
+ .semantic-search-hint {
+ opacity: 0.7;
+ font-size: 0.9em;
+ }
+
+ #semantic-search-status {
+ row-gap: 6px;
+ }
+
+ #semantic-search-diagnostics-toggle {
+ align-self: flex-start;
+ margin-inline: 0;
+ }
+
+ // Bars and figures share one label column
+ #semantic-search-progress,
+ #semantic-search-diagnostics {
+ display: grid;
+ align-items: center;
+ column-gap: 12px;
+ row-gap: 4px;
+ }
+
+ #semantic-search-progress {
+ grid-template-columns: max-content 1fr max-content;
+
+ .semantic-search-progress-row {
+ display: contents;
+
+ &[hidden] {
+ display: none;
+ }
+ }
+
+ progress {
+ width: 100%;
+ }
+ }
+
+ #semantic-search-diagnostics {
+ grid-template-columns: max-content 1fr;
&[hidden] {
display: none;
}
- }
-
- progress {
- width: 100%;
- }
-}
-
-#semantic-search-diagnostics {
- display: grid;
- grid-template-columns: max-content 1fr;
- column-gap: 12px;
- row-gap: 2px;
- margin-top: 10px;
-
- label:nth-child(odd) {
- color: var(--fill-secondary);
- }
-
- label:nth-child(even) {
- white-space: normal;
+
+ label:nth-child(odd) {
+ color: var(--fill-secondary);
+ }
+
+ label:nth-child(even) {
+ white-space: normal;
+ }
}
}
@@ -148,18 +186,6 @@
}
}
-#semantic-search-model-description {
- margin-bottom: 10px;
-}
-
-#semantic-search-libraries {
- padding-top: 10px;
-}
-
-#semantic-search-endpoint-row {
- margin: 4px 0;
-}
-
#zotero-embeddings-endpoint-container {
.endpoint-note {
opacity: 0.7;
diff --git a/test/tests/embeddingsTest.js b/test/tests/embeddingsTest.js
index 27ae81295c..0c9c23d55e 100644
--- a/test/tests/embeddingsTest.js
+++ b/test/tests/embeddingsTest.js
@@ -1389,6 +1389,7 @@ describe("Zotero.Embeddings", function () {
sinon.stub(Zotero.Embeddings, 'getModelVersion').returns('test-endpoint/1'),
sinon.stub(Zotero.Embeddings, 'embedMany').callsFake(async texts => texts.map(text => vectorFor(text)))
];
+ await Zotero.Embeddings.initDB();
await Zotero.DB.queryAsync("DELETE FROM embeddings.itemEmbeddingsMeta WHERE key='endpoint'");
Zotero.Embeddings.Endpoint.reset();
});
@@ -1399,6 +1400,14 @@ describe("Zotero.Embeddings", function () {
await Zotero.DB.queryAsync("DELETE FROM embeddings.itemEmbeddingsMeta WHERE key='endpoint'");
});
+ it("should report a configured endpoint as off while no model is selected", async function () {
+ Zotero.Prefs.set('embeddings.endpoint', URL);
+ Zotero.Embeddings.getModelName.returns('');
+ Zotero.Embeddings.getModelVersion.throws(new Error("Unknown embeddings model ''"));
+ assert.equal(Zotero.Embeddings.Endpoint.getStatus().state, 'off');
+ assert.doesNotThrow(() => Zotero.Embeddings.Indexing.getStatus());
+ });
+
it("should describe how to serve the model", function () {
let { command, url } = Zotero.Embeddings.Endpoint.getCommand();
assert.include(command, 'hotchpotch/bekko-embedding-v1-a8m-GGUF:F16');