Add conversation system message

This commit is contained in:
dwrz
2026-07-05 20:53:30 +00:00
parent 7833cf2b00
commit 23e29e2d97
5 changed files with 169 additions and 3 deletions

View File

@@ -140,4 +140,10 @@
<circle cx="17" cy="17" r="3" /> <circle cx="17" cy="17" r="3" />
<circle cx="7" cy="7" r="3" /> <circle cx="7" cy="7" r="3" />
</symbol> </symbol>
<symbol id="scroll-text" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M15 12h-5"/>
<path d="M15 8h-5"/>
<path d="M19 17V5a2 2 0 0 0-2-2H4"/>
<path d="M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2a1 1 0 0 0 1 1h3"/>
</symbol>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

@@ -1229,3 +1229,15 @@ body {
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-word; word-break: break-word;
} }
.message--system .message__icon {
color: var(--color-text-muted);
}
.message--system .message__icon--system {
color: var(--color-text-muted);
}
.message__placeholder {
color: var(--color-text-muted);
}

View File

@@ -83,6 +83,7 @@ class Odidere {
this._currentRun = null; this._currentRun = null;
this.currentController = null; this.currentController = null;
this.totalTokens = 0; this.totalTokens = 0;
this.systemMessage = '';
// Attachments & location // Attachments & location
this.attachments = []; this.attachments = [];
@@ -122,6 +123,8 @@ class Odidere {
init() { init() {
this.conversation.load(); this.conversation.load();
this.systemMessage = localStorage.getItem(SYSTEM_MESSAGE_KEY) || '';
this.renderer.renderSystemMessage(this.systemMessage);
if (this.conversation.size > 0) { if (this.conversation.size > 0) {
this.renderer.renderMessages(this.conversation.getActivePath()); this.renderer.renderMessages(this.conversation.getActivePath());
} }
@@ -172,6 +175,8 @@ class Odidere {
this.conversation.clear(); this.conversation.clear();
this.renderer.setAutoScroll(true); this.renderer.setAutoScroll(true);
this.renderer.clearChat(); this.renderer.clearChat();
this.systemMessage = localStorage.getItem(SYSTEM_MESSAGE_KEY) || '';
this.renderer.renderSystemMessage(this.systemMessage);
this.clearAttachments(); this.clearAttachments();
this.clearLocation(); this.clearLocation();
this.$textInput.value = ''; this.$textInput.value = '';
@@ -628,9 +633,8 @@ class Odidere {
const { provider, model } = this.settings.getModel(); const { provider, model } = this.settings.getModel();
const payload = { messages, provider, model }; const payload = { messages, provider, model };
const systemMessage = localStorage.getItem(SYSTEM_MESSAGE_KEY); if (this.systemMessage) {
if (systemMessage) { payload.system_message = this.systemMessage;
payload.system_message = systemMessage;
} }
const maxTurns = this._getMaxTurns(); const maxTurns = this._getMaxTurns();
if (maxTurns > 0) { if (maxTurns > 0) {
@@ -1342,10 +1346,67 @@ class Odidere {
// ==================== // ====================
async _openEdit(messageId, field, toolCallId) { async _openEdit(messageId, field, toolCallId) {
if (messageId === 'system') {
const saved = await this._openSystemEdit();
if (saved) {
this.renderer.reRenderSystemMessage(this.systemMessage);
}
return;
}
const saved = await this.editModal.open(messageId, field, toolCallId); const saved = await this.editModal.open(messageId, field, toolCallId);
if (saved) this.renderer.reRenderActivePath(); if (saved) this.renderer.reRenderActivePath();
} }
async _openSystemEdit() {
return new Promise((resolve) => {
const $tpl = this.document.getElementById('tpl-edit-modal');
if (!$tpl) return resolve(false);
const $modal = $tpl.content.cloneNode(true).firstElementChild;
const $overlay = this.document.createElement('div');
$overlay.className = 'modal-overlay';
$overlay.appendChild($modal);
$modal.querySelector('h2').textContent = 'Edit System Message';
const textarea = $modal.querySelector('#edit-textarea');
textarea.value = this.systemMessage || '';
const saveBtn = $modal.querySelector('#edit-save');
const closeBtn = $modal.querySelector('.modal__close');
const close = () => {
$overlay.remove();
this.document.removeEventListener('keydown', handleEscape);
resolve(false);
};
const save = () => {
const newText = textarea.value.trim();
const settingsDefault = localStorage.getItem(SYSTEM_MESSAGE_KEY) || '';
this.systemMessage = newText || settingsDefault;
$overlay.remove();
this.document.removeEventListener('keydown', handleEscape);
resolve(true);
};
closeBtn.addEventListener('click', close);
saveBtn.addEventListener('click', save);
$overlay.addEventListener('click', (e) => {
if (e.target === $overlay) close();
});
const handleEscape = (e) => {
if (e.key === 'Escape') close();
};
this.document.addEventListener('keydown', handleEscape);
this.document.body.appendChild($overlay);
textarea.focus();
});
}
// ==================== // ====================
// STOP CONVERSATION // STOP CONVERSATION
// ==================== // ====================

View File

@@ -41,6 +41,7 @@ export class Renderer {
'tpl-reasoning-message', 'tpl-reasoning-message',
); );
this.$tplToolMessage = document.getElementById('tpl-tool-message'); this.$tplToolMessage = document.getElementById('tpl-tool-message');
this.$tplSystemMessage = document.getElementById('tpl-system-message');
} }
// --- Full / incremental render --- // --- Full / incremental render ---
@@ -611,6 +612,57 @@ export class Renderer {
this.scrollToBottom(); this.scrollToBottom();
} }
/**
* renderSystemMessage renders the system message block.
* @param {string} content
*/
renderSystemMessage(content) {
this._renderSystemMessage(content);
}
/**
* reRenderSystemMessage replaces the system message block in the DOM.
* @param {string} content
*/
reRenderSystemMessage(content) {
const $old = this.$chat.querySelector('[data-id="system"]');
if (!$old) return;
const $msg =
this.$tplSystemMessage.content.cloneNode(true).firstElementChild;
$msg.dataset.id = 'system';
const $content = $msg.querySelector('.message__content');
if (content?.trim()) {
$content.textContent = content;
$msg.dataset.copyText = content;
} else {
$content.innerHTML =
'<em class="message__placeholder">System Default</em>';
$msg.dataset.copyText = '';
}
$old.replaceWith($msg);
}
_renderSystemMessage(content, _meta = null) {
const $msg =
this.$tplSystemMessage.content.cloneNode(true).firstElementChild;
$msg.dataset.id = 'system';
const $content = $msg.querySelector('.message__content');
if (content?.trim()) {
$content.textContent = content;
$msg.dataset.copyText = content;
} else {
$content.innerHTML =
'<em class="message__placeholder">System Default</em>';
$msg.dataset.copyText = '';
}
this.$chat.insertBefore($msg, this.$chatLoading);
}
_populateDebugPanel($dl, meta) { _populateDebugPanel($dl, meta) {
if (meta?.voice) this._appendDebugRow($dl, 'Voice', meta.voice); if (meta?.voice) this._appendDebugRow($dl, 'Voice', meta.voice);
if (meta?.usage) { if (meta?.usage) {

View File

@@ -275,4 +275,39 @@
</div> </div>
</div> </div>
</template> </template>
<template id="tpl-system-message">
<div class="message message--system">
<div class="message__body">
<details class="message__collapsible">
<summary class="message__collapsible-summary">
<svg class="message__icon message__icon--system" aria-hidden="true"><use href="/static/icons.svg#scroll-text"></use></svg>
<span class="message__collapsible-label">System</span>
</summary>
<div class="message__content"></div>
</details>
<div class="message__actions">
<svg class="message__icon" aria-hidden="true"><use href="/static/icons.svg#scroll-text"></use></svg>
<div class="message__actions-buttons">
<button
type="button"
class="message__action-btn"
data-action="edit"
aria-label="Edit system message"
>
<svg class="icon"><use href="/static/icons.svg#edit"></use></svg>
</button>
<button
type="button"
class="message__action-btn"
data-action="copy"
aria-label="Copy to clipboard"
>
<svg class="icon"><use href="/static/icons.svg#copy"></use></svg>
</button>
</div>
</div>
</div>
</div>
</template>
{{ end }} {{ end }}