Multimodal removal The multimodal path called nc_multimodal_search against a second hardcoded collection whose embedding service (multimodal-embeddings:7999) was never deployed and ENABLE_MULTIMODAL_RAG has always been false, so it only ever logged "multimodal search skipped". Removed rather than left as dead weight: - clinicalRetrieval: normalizeMcpMultimodalResponse, isVisualSourceQuery, isRadiologyQuery, buildMultimodalSearchQuery, classifyAndRerankMultimodalResults, selectMultimodalResults, visualIntent, visualMetadataScore, shouldRejectVisualSource, allowsFrontMatterQuery, looksLikeFrontMatterPage, looksLikeTextOnlyPage and MULTIMODAL_CANDIDATE_LIMIT (~140 lines). - clinicalMcpClient: multimodalSearch. - The route's visual/text slot split is gone; the whole search limit is text. - The "[visual PDF page match]" prompt label and the "visual PDF page" source badge are gone with it. Adding models Model availability could only be ticked from what the gateway advertised, so an admin could never offer a model discovery did not list. Each list now has a text field: a typed id joins the same checkbox list, is enabled by default, is de-duplicated, and persists through the normal allowed_models save. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BkfrkQwA4YGrGw9LZSpeAq
35 lines
2 KiB
JavaScript
35 lines
2 KiB
JavaScript
import { escapeAttr, escapeHtml } from './citations.js';
|
|
|
|
export function renderSourcesList(sources) {
|
|
if (!sources || sources.length === 0) return '<p class="assistant-muted">No citations returned.</p>';
|
|
return sources.map(function (s, idx) {
|
|
var n = s.number || idx + 1;
|
|
var page = s.page || s.page_number || s.pageNumber;
|
|
var meta = [];
|
|
if (page) meta.push('page ' + page);
|
|
if (s.visual_caption_source) meta.push('caption: ' + s.visual_caption_source);
|
|
if (s.visual_kind || s.source_priority) meta.push(s.visual_kind || s.source_priority);
|
|
if (s.category) meta.push(s.category);
|
|
if (s.doc_type || s.type) meta.push(s.doc_type || s.type);
|
|
if (s.score != null) meta.push('score ' + Number(s.score).toFixed(3));
|
|
return '<div class="assistant-source" id="assistant-source-' + escapeAttr(n) + '">' +
|
|
'<strong>[' + escapeHtml(n) + '] ' + escapeHtml(s.title || s.resource || 'Untitled source') + '</strong>' +
|
|
renderSourceBadges(s) +
|
|
'<div class="assistant-source-meta">' + escapeHtml(meta.join(' · ') || 'indexed source') + '</div>' +
|
|
(s.excerpt ? '<div class="assistant-source-excerpt"><p style="white-space: pre-wrap">' + escapeHtml(s.excerpt) + '</p></div>' : '') +
|
|
'</div>';
|
|
}).join('');
|
|
}
|
|
|
|
function renderSourceBadges(source) {
|
|
var badges = [];
|
|
if (source.source_priority) badges.push(source.source_priority);
|
|
if (source.visual_kind && badges.indexOf(source.visual_kind) === -1) badges.push(source.visual_kind);
|
|
if (source.visual_caption_source && badges.indexOf(source.visual_caption_source) === -1) badges.push(source.visual_caption_source);
|
|
if (source.category) badges.push(source.category);
|
|
if (source.source_boost && Number(source.source_boost) !== 1) badges.push(Number(source.source_boost).toFixed(2) + 'x');
|
|
if (!badges.length) return '';
|
|
return '<div class="assistant-source-badges">' + badges.slice(0, 4).map(function (badge) {
|
|
return '<span>' + escapeHtml(badge) + '</span>';
|
|
}).join('') + '</div>';
|
|
}
|