Feature: Implement Knowledge Metadata Model and enhance library item display with rich metadata (Q&A, model details)
This commit is contained in:
@@ -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.
|
||||
</p>
|
||||
</div>
|
||||
<div className="setting-section">
|
||||
<h3>Knowledge Metadata Model</h3>
|
||||
<select
|
||||
className="select"
|
||||
value={enrichmentModel}
|
||||
onChange={handleEnrichmentModelChange}
|
||||
>
|
||||
{enrichmentModelOptions.length === 0 && <option value="">{isLoadingModelCatalog ? 'Loading models…' : '— No generation models available —'}</option>}
|
||||
{enrichmentModelOptions.map(model => (
|
||||
<option key={model.value} value={model.value}>{model.label}</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="setting-description">
|
||||
Heimgeist uses this local model for summaries, keywords, entities, and optional Deep enrichment Q&A.
|
||||
</p>
|
||||
</div>
|
||||
<div className="setting-section">
|
||||
<h3>Transcription Model</h3>
|
||||
<select
|
||||
|
||||
@@ -442,8 +442,10 @@ export default function LibraryManager({ apiBase, library, jobs, onRefresh }) {
|
||||
const isExpanded = expandedItemId === itemId
|
||||
const preview = contentPreviews[itemId]
|
||||
const metadata = item.metadata || {}
|
||||
const keywords = Array.isArray(metadata.keywords) ? metadata.keywords.slice(0, 6) : []
|
||||
const entities = Array.isArray(metadata.entities) ? metadata.entities.slice(0, 4) : []
|
||||
const keywords = Array.isArray(metadata.keywords) ? metadata.keywords : []
|
||||
const entities = Array.isArray(metadata.entities) ? metadata.entities : []
|
||||
const qaPairs = Array.isArray(metadata.qa) ? metadata.qa : []
|
||||
const qualityFlags = Array.isArray(metadata.quality_flags) ? metadata.quality_flags : []
|
||||
return (
|
||||
<article key={itemId} className={`library-content-card kind-${item.kind || 'file'} ${isExpanded ? 'expanded' : ''}`}>
|
||||
<div className="library-content-card-header">
|
||||
@@ -456,6 +458,7 @@ export default function LibraryManager({ apiBase, library, jobs, onRefresh }) {
|
||||
{item.enrich_enabled ? 'Deep enrichment' : 'Standard'}
|
||||
</div>
|
||||
<div className={`library-item-metadata status-${metadata.status || 'missing'}`}>
|
||||
{metadata.headline && <h3>{metadata.headline}</h3>}
|
||||
{metadata.summary ? (
|
||||
<p>{metadata.summary}</p>
|
||||
) : (
|
||||
@@ -475,6 +478,25 @@ export default function LibraryManager({ apiBase, library, jobs, onRefresh }) {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{qaPairs.length > 0 && (
|
||||
<div className="library-metadata-qa">
|
||||
<div className="library-metadata-label">Generated Q&A</div>
|
||||
{qaPairs.map((qa, index) => (
|
||||
<div className="library-metadata-qa-pair" key={`${qa.q}-${index}`}>
|
||||
<strong>{qa.q}</strong>
|
||||
<span>{qa.a}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{(metadata.model || metadata.language || metadata.strategy || qualityFlags.length > 0) && (
|
||||
<div className="library-metadata-details">
|
||||
{metadata.model && <span>Model: {metadata.model}</span>}
|
||||
{metadata.language && <span>Language: {metadata.language}</span>}
|
||||
{metadata.strategy && <span>Strategy: {metadata.strategy}</span>}
|
||||
{qualityFlags.length > 0 && <span>Flags: {qualityFlags.join(', ')}</span>}
|
||||
</div>
|
||||
)}
|
||||
{metadata.status === 'fallback' && <div className="library-metadata-warning">Local model metadata was unavailable; Heimgeist used a text fallback.</div>}
|
||||
{metadata.status === 'failed' && <div className="library-metadata-warning">{metadata.error || 'Metadata generation failed.'}</div>}
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user