fix: return initial and enhanced chat outputs

This commit is contained in:
Hermes Agent
2026-08-30 11:29:51 +10:00
parent c7358ac97d
commit 0fdc1b6449
7 changed files with 217 additions and 235 deletions
+39 -20
View File
@@ -121,9 +121,9 @@
}
function MessageBubble(props) {
var item = props.item || {}, assistant = item.role === "assistant";
return h("div", { className: "ollama-message " + item.role, key: props.messageKey },
h("div", { className: "ollama-message-meta" }, h("small", null, assistant ? (item.model || "Ollama") : "You"), item.created_at && h("small", null, fmtDate(item.created_at))),
var item = props.item || {}, assistant = item.role === "assistant", label = item.variant === "initial" ? "Initial output" : item.variant === "enhanced" ? "Enhanced output" : "";
return h("div", { className: "ollama-message " + item.role + (item.variant ? " " + item.variant : ""), key: props.messageKey },
h("div", { className: "ollama-message-meta" }, h("small", null, label || (assistant ? (item.model || "Ollama") : "You")), assistant && label && h("small", null, item.model || "Ollama"), item.created_at && h("small", null, fmtDate(item.created_at))),
h(RichText, { content: item.content }),
h("div", { className: "ollama-message-actions" },
h("button", { type: "button", onClick: function () { copyText(item.content, props.onCopied); } }, "Copy"),
@@ -377,10 +377,10 @@
h("div", { className: "ollama-pool-grid" }, models.map(function (item) { var loaded = !!item.loaded, selected = props.poolSelection.indexOf(item.name) >= 0, placement = props.placements[item.name] || "gpu_ram"; return h("label", { className: "ollama-pool-item" + (loaded ? " loaded" : "") + (selected ? " selected" : ""), key: item.name, title: loaded ? "Loaded and resident in Ollama" : "Installed but not resident" }, h("input", { type: "checkbox", checked: selected, onChange: function () { props.onTogglePool(item.name); } }), h("span", null, h("strong", null, item.name), h("small", null, loaded ? "Loaded and resident · keep-alive active" : "Installed · not loaded", " · ", (item.capabilities || []).join(", ") || "capabilities unknown"), h("span", { className: "ollama-placement-control" }, h("small", null, "Placement"), h("select", { value: placement, onClick: function (event) { event.stopPropagation(); }, onChange: function (event) { event.stopPropagation(); props.onPlacementChange(item.name, event.target.value); } }, h("option", { value: "gpu_ram" }, "GPU + RAM (automatic offload)"), h("option", { value: "ram_only" }, "RAM only (CPU)"))))); })),
h("div", { className: "ollama-pool-actions" }, h(Button, { disabled: !props.poolSelection.length || !!props.busy, onClick: props.onLoad }, props.busy === "/models/load" ? "Loading " + props.poolSelection.length + " model" + (props.poolSelection.length === 1 ? "" : "s") + "…" : "Load selected permanently"), h(Button, { className: "secondary", disabled: !props.poolSelection.length || !!props.busy, onClick: props.onUnload }, props.busy === "/models/unload" ? "Unloading…" : "Unload selected")),
h("div", { className: "ollama-chat-model-selection" },
h("div", null, h("strong", null, "Answer harness"), h("small", null, "Choose one primary model. Add one or more validators to review its draft before the primary compiles the final answer.")),
h("div", null, h("strong", null, "Two-stage answer workflow"), h("small", null, "The primary model writes the initial output. One enhancement model rewrites it with improvements and returns a complete enhanced output.")),
loaded.length ? h("div", { className: "ollama-harness-primary" }, h("label", null, "Primary model", h("select", { value: primary, onChange: function (event) { props.onPrimaryChange(event.target.value); } }, loaded.map(function (item) { return h("option", { key: item.name, value: item.name }, item.name); })))) : h("span", null, "Load one or more models above first."),
loaded.length > 1 && h("div", { className: "ollama-harness-validators" }, h("strong", null, "Validator models"), loaded.filter(function (item) { return item.name !== primary; }).map(function (item) { return h("label", { className: "loaded", key: item.name }, h("input", { type: "checkbox", checked: validators.indexOf(item.name) >= 0, onChange: function () { props.onToggleChat(item.name); } }), item.name, " · ", (item.capabilities || []).join(", ")); })),
loaded.length > 1 && h("small", { className: validators.length >= 1 ? "ollama-harness-ready" : "ollama-harness-warning" }, validators.length >= 1 ? "Validation harness ready: the primary will compile one final answer after independent checks." : "Select at least one validator model to enable the validation harness."))
loaded.length > 1 && h("div", { className: "ollama-harness-validators" }, h("strong", null, "Enhancement model"), loaded.filter(function (item) { return item.name !== primary; }).map(function (item) { return h("label", { className: "loaded", key: item.name }, h("input", { type: "radio", name: "ollama-enhancement-model", checked: validators[0] === item.name, onChange: function () { props.onToggleChat(item.name); } }), item.name, " · ", (item.capabilities || []).join(", ")); })),
loaded.length > 1 && h("small", { className: validators.length >= 1 ? "ollama-harness-ready" : "ollama-harness-warning" }, validators.length >= 1 ? "Workflow ready: initial output will be followed by a complete enhanced output." : "Select one enhancement model to produce the enhanced output."))
);
}
@@ -389,7 +389,7 @@
var loadedModels = models.filter(function (item) { return item.loaded; });
var savedChatState = React.useState(function () { return readSavedChat(); })[0];
var modelState = React.useState(savedChatState.model || (loadedModels[0] ? loadedModels[0].name : (models[0] ? models[0].name : ""))), model = modelState[0], setModel = modelState[1];
var selectedModelsState = React.useState(savedChatState.models && savedChatState.models.length ? savedChatState.models : (loadedModels[0] ? [loadedModels[0].name] : [])), selectedModels = selectedModelsState[0], setSelectedModels = selectedModelsState[1];
var selectedModelsState = React.useState(savedChatState.models && savedChatState.models.length ? savedChatState.models.slice(0, 2) : (loadedModels[0] ? [loadedModels[0].name] : [])), selectedModels = selectedModelsState[0], setSelectedModels = selectedModelsState[1];
var poolState = React.useState(loadedModels.map(function (item) { return item.name; })), poolSelection = poolState[0], setPoolSelection = poolState[1];
var poolLoadedSignature = React.useRef("");
var chatLoadedSignature = React.useRef("");
@@ -420,12 +420,21 @@
function openConversation(id) {
if (!id) return;
fetchJSON(API + "/conversations/" + encodeURIComponent(id)).then(function (value) {
var item = value.conversation || {};
var item = value.conversation || {}, outputs = value.harness_outputs || {}, restored = [];
(value.messages || []).forEach(function (message) {
var output = outputs[String(message.request_id || "")];
if (message.role === "assistant" && output && output.initial_output) {
restored.push({ id: String(message.id) + ":initial", request_id: message.request_id, role: "assistant", content: output.initial_output, created_at: message.created_at, model: output.primary_model, variant: "initial", attachments: message.attachments || [] });
restored.push({ id: String(message.id) + ":enhanced", request_id: message.request_id, role: "assistant", content: output.enhanced_output || output.initial_output, created_at: message.created_at, model: output.enhancement_model, variant: "enhanced", attachments: message.attachments || [] });
} else {
restored.push({ id: message.id, request_id: message.request_id, role: message.role, content: message.content, created_at: message.created_at, model: message.model, attachments: message.attachments || [] });
}
});
setConversationId(item.id || id);
setHistory((value.messages || []).filter(function (message) { return message.role === "user" || message.role === "assistant"; }).map(function (message) { return { id: message.id, request_id: message.request_id, role: message.role, content: message.content, created_at: message.created_at, model: message.model, attachments: message.attachments || [] }; }));
setHistory(restored);
setMetrics(value.metrics || []);
if (item.model) setModel(item.model);
if (Array.isArray(item.models) && item.models.length) setSelectedModels(item.models);
if (Array.isArray(item.models) && item.models.length) setSelectedModels(item.models.slice(0, 2));
}).catch(function (err) { setNotice({ error: err.message || String(err) }); });
}
function refreshStorage() { fetchJSON(API + "/storage").then(setStorage).catch(function (err) { setNotice({ error: "Storage status unavailable: " + (err.message || String(err)) }); }); }
@@ -464,9 +473,9 @@
}
if (loadedSignature !== chatLoadedSignature.current) {
chatLoadedSignature.current = loadedSignature;
setSelectedModels(function (old) { var valid = old.filter(function (name) { return loadedNames.indexOf(name) >= 0; }); return valid.length ? Array.from(new Set(valid)) : (loadedNames.length ? [loadedNames[0]] : []); });
setSelectedModels(function (old) { var valid = old.filter(function (name) { return loadedNames.indexOf(name) >= 0; }); return valid.length ? Array.from(new Set(valid)).slice(0, 2) : (loadedNames.length ? [loadedNames[0]] : []); });
} else {
setSelectedModels(function (old) { return old.filter(function (name) { return loadedNames.indexOf(name) >= 0; }); });
setSelectedModels(function (old) { return old.filter(function (name) { return loadedNames.indexOf(name) >= 0; }).slice(0, 2); });
}
}, [models]);
React.useEffect(function () { saveChat(model, selectedModels, history); }, [model, selectedModels, history]);
@@ -532,8 +541,11 @@
}
function loadModel() { manageModels("/models/load", "Permanently loaded"); }
function unloadModels() { manageModels("/models/unload", "Unloaded"); }
function setPrimaryModel(name) { if (!loadedModels.some(function (item) { return item.name === name; })) return; setModel(name); setSelectedModels(function (old) { return [name].concat(old.filter(function (item) { return item !== name; })); }); }
function toggleChatModel(name) { if (name === selectedModels[0] || !loadedModels.some(function (item) { return item.name === name; })) return; toggleIn(setSelectedModels, name); }
function setPrimaryModel(name) { if (!loadedModels.some(function (item) { return item.name === name; })) return; setModel(name); setSelectedModels(function (old) { return [name].concat(old.filter(function (item) { return item !== name; })).slice(0, 2); }); }
function toggleChatModel(name) {
if (name === selectedModels[0] || !loadedModels.some(function (item) { return item.name === name; })) return;
setSelectedModels(function (old) { return old.length > 1 && old[1] === name ? [old[0]] : [old[0], name]; });
}
function togglePoolModel(name) { toggleIn(setPoolSelection, name); }
function addUrl() { if (!url.trim()) return; setAttachments(function (old) { return old.concat([{ name: url.trim(), url: url.trim(), mime_type: "" }]); }); setUrl(""); }
function addFiles(files) {
@@ -576,14 +588,22 @@
var resolvedConversationId = result.conversation_id || conversationId;
setConversationId(resolvedConversationId);
setHistory(function (old) {
if (result.initial_output) {
var requestId = result.request_id || "", alreadyShown = old.some(function (item) { return item.request_id === requestId && item.variant === "enhanced"; });
if (alreadyShown) return old;
return old.concat([
{ role: "assistant", content: result.initial_output, model: result.primary_model || "Ollama", request_id: requestId, variant: "initial" },
{ role: "assistant", content: result.enhanced_output || answer, model: result.enhancement_model || "Ollama", request_id: requestId, variant: "enhanced" }
]);
}
var last = old.length ? old[old.length - 1] : null;
return last && last.role === "assistant" && last.content === answer ? old : old.concat([{ role: "assistant", content: answer }]);
return last && last.role === "assistant" && last.content === answer ? old : old.concat([{ role: "assistant", content: answer, model: result.primary_model || "Ollama", request_id: result.request_id }]);
});
setMetrics(result.metrics || []);
setValidationReports(result.mode === "harness" ? (result.validation_reports || []) : null);
setValidationReports(null);
setAttachments([]);
setRuntime(result.runtime || runtime);
setNotice({ ok: result.mode === "harness" ? "One final answer compiled by " + result.primary_model + " after validation by " + (result.validator_models || []).join(", ") + "." : "Response complete. Shared conversation and performance metrics saved." });
setNotice({ ok: result.mode === "harness" ? "Initial output and enhanced output generated by " + result.primary_model + " and " + (result.enhancement_model || (result.validator_models || [])[0] || "the enhancement model") + "." : "Response complete. Shared conversation and performance metrics saved." });
pollRuntime(); refreshConversations();
return result;
}
@@ -620,7 +640,7 @@
fetchJSON(API + "/chat", requestOptions).then(function (result) { return result.done ? result : pollChatJob(requestId, current); }).then(applyChatResult).catch(function (err) { if (!current.stopped) setNotice({ error: err.message || String(err) }); }).finally(function () { if (!current.stopped) { setThinking(null); setActiveRequest(null); } setBusy(""); });
}
return h("section", { className: "ollama-chat" },
h("div", { className: "ollama-chat-header" }, h("div", null, h("div", { className: "ollama-eyebrow" }, "LOCAL OLLAMA CHAT"), h("h2", null, "Chat with Ollama"), h("p", null, "Direct chat is the default. Enable quality review in the controls when you want independent validator checks.")), h(Button, { className: "secondary", disabled: !history.length || busy === "send", onClick: clearChat }, "Clear chat")),
h("div", { className: "ollama-chat-header" }, h("div", null, h("div", { className: "ollama-eyebrow" }, "LOCAL OLLAMA CHAT"), h("h2", null, "Chat with Ollama"), h("p", null, "Direct chat is the default. For two-stage writing, select a primary and one enhancement model to receive both outputs.")), h(Button, { className: "secondary", disabled: !history.length || busy === "send", onClick: clearChat }, "Clear chat")),
notice && h("div", { className: "ollama-notice " + (notice.error ? "error" : notice.warning ? "warning" : "ok") }, notice.error || notice.warning || notice.ok),
h("div", { className: "ollama-chat-shell" },
h("aside", { className: "ollama-conversation-rail" },
@@ -630,8 +650,7 @@
),
h("div", { className: "ollama-chat-main" },
thinking && h(ThinkingStatus, { stage: thinkingDetails && thinkingDetails.stage ? thinkingDetails.stage : (thinkingElapsed < 1 ? thinking.stage : "Ollama is generating the response"), elapsed: thinkingDetails && thinkingDetails.elapsed != null ? thinkingDetails.elapsed : thinkingElapsed, details: thinkingDetails, expanded: thinkingOpen, onToggle: function () { setThinkingOpen(!thinkingOpen); }, onStop: stop }),
validationReports && validationReports.length > 0 && h("details", { className: "ollama-validation-evidence" }, h("summary", null, "Validation evidence · ", validationReports.length, " independent reports"), validationReports.map(function (item) { return h("div", { className: "ollama-validation-report", key: item.model }, h("strong", null, item.model), h("p", null, item.report || "No report text returned.")); })),
h("div", { className: "ollama-conversation" }, history.length ? history.map(function (item, index) { return h(MessageBubble, { item: item, messageKey: item.id || item.request_id || index, key: item.id || item.request_id || index, onCopied: function () { setNotice({ ok: "Message copied." }); }, onRetry: item.role === "assistant" ? function () { retryMessage(index); } : null }); }) : h(Empty, null, "Start a conversation. Select a loaded model in the controls, then send a message.")),
h("div", { className: "ollama-conversation" }, history.length ? history.map(function (item, index) { return h(MessageBubble, { item: item, messageKey: item.id || item.request_id || index, key: item.id || item.request_id || index, onCopied: function () { setNotice({ ok: "Message copied." }); }, onRetry: item.role === "assistant" && item.variant !== "initial" ? function () { retryMessage(index); } : null }); }) : h(Empty, null, "Start a conversation. Select a loaded model in the controls, then send a message.")),
h("div", { className: "ollama-composer" + (dragging ? " drop-active" : ""), onDragOver: onDragOver, onDragLeave: onDragLeave, onDrop: onDrop }, dragging && h("div", { className: "ollama-drop-hint" }, "Drop files here to attach"), h("textarea", { value: message, placeholder: selectedModels.length ? "Ask " + selectedModels.length + " loaded model" + (selectedModels.length === 1 ? "" : "s") + "… Press Enter to send; Shift+Enter for a new line." : "Load and select at least one model in Chat controls…", onPaste: onPaste, onChange: function (event) { setMessage(event.target.value); }, onKeyDown: function (event) { if (event.key === "Enter" && !event.shiftKey) { event.preventDefault(); send(); } } }),
h("div", { className: "ollama-attachment-actions" }, h("label", { className: "ollama-file-button" }, "Attach image / PDF / file", h("input", { type: "file", multiple: true, accept: "image/*,application/pdf,text/*,.txt,.md,.csv,.json,.log,.xml,.yaml,.yml", onChange: onFiles })), h("input", { className: "ollama-url-input", value: url, placeholder: "https://example.com/document", onChange: function (event) { setUrl(event.target.value); }, onKeyDown: function (event) { if (event.key === "Enter") addUrl(); } }), h(Button, { onClick: addUrl, disabled: !url.trim() }, "Add URL"), h(Button, { onClick: send, disabled: busy === "send" || busy === "stop" || !selectedModels.length || (!message.trim() && !attachments.length) }, busy === "send" ? "Sending…" : "Send (Enter)")),
attachments.length > 0 && h("div", { className: "ollama-attachments" }, attachments.map(function (item, index) { return h("span", { className: "ollama-attachment", key: index }, item.name || item.url, h("button", { type: "button", onClick: function () { setAttachments(function (old) { return old.filter(function (_, i) { return i !== index; }); }); } }, "×")); })),