From d3c4f455018ca3d34522f9526e290919d83e6f20 Mon Sep 17 00:00:00 2001
From: Victor Giers
Date: Mon, 15 Jun 2026 02:42:36 +0200
Subject: [PATCH] Feature: Implement Knowledge Metadata Model and enhance
library item display with rich metadata (Q&A, model details)
---
src/GeneralSettings.jsx | 36 ++++++++++++++++++++++++++++++++
src/LibraryManager.jsx | 26 +++++++++++++++++++++--
src/styles.css | 46 +++++++++++++++++++++++++++++++++++++----
3 files changed, 102 insertions(+), 6 deletions(-)
diff --git a/src/GeneralSettings.jsx b/src/GeneralSettings.jsx
index be962dc..e1d7a33 100644
--- a/src/GeneralSettings.jsx
+++ b/src/GeneralSettings.jsx
@@ -11,6 +11,7 @@ import desktopApi from './desktop/desktopApi';
const EMBED_MODEL_KEY = 'embedModel';
const RERANK_MODEL_KEY = 'rerankModel';
+const ENRICHMENT_MODEL_KEY = 'enrichmentModel';
const MODEL_KEY = 'chatModel';
const VISION_MODEL_KEY = 'visionModel';
const TRANSCRIPTION_MODEL_KEY = 'transcriptionModel';
@@ -18,6 +19,7 @@ const DEFAULT_AUDIO_INPUT_DEVICE_ID = '';
const DEFAULT_AUDIO_INPUT_LANGUAGE = '';
const DEFAULT_BACKEND_API_URL = 'http://127.0.0.1:8000';
const DEFAULT_EMBED_MODEL = 'nomic-embed-text:latest';
+const DEFAULT_ENRICHMENT_MODEL = 'qwen3:4b';
const DEFAULT_TRANSCRIPTION_MODEL = 'base';
const DEFAULT_UPDATE_STATUS = {
state: 'idle',
@@ -79,6 +81,7 @@ export default function GeneralSettings({
const [backendApiUrl, setBackendApiUrl] = useState('');
const [embedModel, setEmbedModel] = useState(DEFAULT_EMBED_MODEL);
const [rerankModel, setRerankModel] = useState(DEFAULT_EMBED_MODEL);
+ const [enrichmentModel, setEnrichmentModel] = useState(DEFAULT_ENRICHMENT_MODEL);
const [chatModels, setChatModels] = useState([]);
const [embeddingModels, setEmbeddingModels] = useState([]);
const [visionModels, setVisionModels] = useState([]);
@@ -120,6 +123,7 @@ export default function GeneralSettings({
setBackendApiUrl(resolveBackendApiUrl(settings));
setEmbedModel(settings.embedModel || DEFAULT_EMBED_MODEL);
setRerankModel(settings.rerankModel || settings.embedModel || DEFAULT_EMBED_MODEL);
+ setEnrichmentModel(settings.enrichmentModel || DEFAULT_ENRICHMENT_MODEL);
setSelectedModel(settings.chatModel || '');
setVisionModel(settings.visionModel || settings.chatModel || '');
setTranscriptionModel(settings.transcriptionModel || DEFAULT_TRANSCRIPTION_MODEL);
@@ -271,6 +275,15 @@ export default function GeneralSettings({
desktopApi.setSetting(RERANK_MODEL_KEY, nextModel);
}, [rerankingModels, rerankModel, embedModel, panel, settingsHydrated]);
+ useEffect(() => {
+ if (panel !== 'AI Models' || !settingsHydrated || enrichmentModel) {
+ return;
+ }
+ const nextModel = chatModels[0] || DEFAULT_ENRICHMENT_MODEL;
+ setEnrichmentModel(nextModel);
+ desktopApi.setSetting(ENRICHMENT_MODEL_KEY, nextModel);
+ }, [chatModels, enrichmentModel, panel, settingsHydrated]);
+
useEffect(() => {
if (panel !== 'AI Models') {
return;
@@ -388,6 +401,12 @@ export default function GeneralSettings({
desktopApi.setSetting(RERANK_MODEL_KEY, nextModel);
};
+ const handleEnrichmentModelChange = (event) => {
+ const nextModel = event.target.value;
+ setEnrichmentModel(nextModel);
+ desktopApi.setSetting(ENRICHMENT_MODEL_KEY, nextModel);
+ };
+
const handleTranscriptionModelChange = (event) => {
const nextModel = event.target.value;
setTranscriptionModel(nextModel);
@@ -575,6 +594,7 @@ export default function GeneralSettings({
const visionModelOptions = buildSelectOptions(visionModels, visionModel, 'saved model unavailable', showMissingModelLabel);
const embeddingModelOptions = buildSelectOptions(embeddingModels, embedModel, 'saved model unavailable', showMissingModelLabel);
const rerankingModelOptions = buildSelectOptions(rerankingModels, rerankModel, 'saved model unavailable', showMissingModelLabel);
+ const enrichmentModelOptions = buildSelectOptions(chatModels, enrichmentModel, 'saved model unavailable', showMissingModelLabel);
const transcriptionModelOptions = buildSelectOptions(whisperModels, transcriptionModel, 'saved model unavailable', showMissingModelLabel);
if (panel === 'AI Models') {
@@ -640,6 +660,22 @@ export default function GeneralSettings({
Heimgeist currently uses an embedding-based reranker for web search, so this should generally be an embedding-capable Ollama model.
+
+
Knowledge Metadata Model
+
+
+ Heimgeist uses this local model for summaries, keywords, entities, and optional Deep enrichment Q&A.
+
+
Transcription Model
diff --git a/src/styles.css b/src/styles.css
index 0922d2c..f3eaa3d 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -1881,14 +1881,17 @@ input:checked + .slider:before {
}
.library-item-metadata p {
- display: -webkit-box;
margin: 0;
- overflow: hidden;
color: color-mix(in srgb, var(--text) 88%, var(--muted));
font-size: 13px;
line-height: 1.45;
- -webkit-box-orient: vertical;
- -webkit-line-clamp: 4;
+}
+
+.library-item-metadata h3 {
+ margin: 0 0 7px;
+ color: var(--text);
+ font-size: 14px;
+ line-height: 1.35;
}
.library-metadata-chips,
@@ -1912,6 +1915,41 @@ input:checked + .slider:before {
font-size: 11px;
}
+.library-metadata-qa,
+.library-metadata-details {
+ margin-top: 10px;
+}
+
+.library-metadata-label {
+ margin-bottom: 6px;
+ color: var(--muted);
+ font-size: 10px;
+ font-weight: 700;
+ letter-spacing: 0.05em;
+ text-transform: uppercase;
+}
+
+.library-metadata-qa-pair {
+ display: grid;
+ gap: 3px;
+ padding: 8px 0;
+ border-top: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
+ font-size: 12px;
+ line-height: 1.45;
+}
+
+.library-metadata-qa-pair span {
+ color: var(--muted);
+}
+
+.library-metadata-details {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 5px 12px;
+ color: var(--muted);
+ font-size: 10px;
+}
+
.library-metadata-warning {
margin-top: 8px;
color: #d6a75f;