Add user defined system message
This commit is contained in:
@@ -90,4 +90,9 @@
|
||||
<path d="m15 9-6 6"/>
|
||||
<path d="m9 9 6 6"/>
|
||||
</symbol>
|
||||
|
||||
<symbol id="settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/>
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.9 KiB |
@@ -593,6 +593,201 @@ body {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* ==================== */
|
||||
/* Settings Modal */
|
||||
/* ==================== */
|
||||
|
||||
.settings-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--s1);
|
||||
}
|
||||
|
||||
.settings-overlay.open {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.settings-panel {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border-light);
|
||||
border-radius: var(--radius);
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
padding: var(--s1);
|
||||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.settings-panel__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: var(--s1);
|
||||
}
|
||||
|
||||
.settings-panel__title {
|
||||
font-size: var(--s1);
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Close button: remove toolbar border, darken the X. */
|
||||
#settings-close {
|
||||
border: none;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
#settings-close:hover {
|
||||
border-color: transparent;
|
||||
color: var(--color-text);
|
||||
}
|
||||
}
|
||||
|
||||
/* Tabs */
|
||||
.settings-tabs {
|
||||
display: flex;
|
||||
border-bottom: 2px solid var(--color-border-light);
|
||||
margin-bottom: var(--s1);
|
||||
}
|
||||
|
||||
.settings-tab {
|
||||
padding: var(--s-2) var(--s-1);
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
margin-bottom: -2px;
|
||||
font-family: inherit;
|
||||
font-size: var(--s-1);
|
||||
color: var(--color-text-muted);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.settings-tab:hover {
|
||||
color: var(--color-text);
|
||||
}
|
||||
}
|
||||
|
||||
.settings-tab:focus-visible {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.settings-tab.active {
|
||||
color: var(--color-text);
|
||||
border-bottom-color: var(--color-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Tab panels */
|
||||
.settings-tab-panel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.settings-tab-panel.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.settings-label {
|
||||
display: block;
|
||||
font-size: var(--s-1);
|
||||
font-weight: 500;
|
||||
color: var(--color-text);
|
||||
margin-bottom: var(--s-2);
|
||||
}
|
||||
|
||||
.settings-select {
|
||||
width: 100%;
|
||||
height: var(--s2);
|
||||
padding: 0 var(--s-1);
|
||||
font-family: inherit;
|
||||
font-size: var(--s-1);
|
||||
line-height: var(--s2);
|
||||
color: var(--color-text);
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border-light);
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
margin-bottom: var(--s1);
|
||||
}
|
||||
|
||||
.settings-select:focus-visible {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.settings-textarea {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-height: calc(var(--s2) * 6);
|
||||
padding: var(--s-1);
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--s-1);
|
||||
line-height: 1.5;
|
||||
color: var(--color-text);
|
||||
background: var(--color-bg);
|
||||
border: 1px solid var(--color-border-light);
|
||||
border-radius: var(--radius);
|
||||
resize: vertical;
|
||||
margin-bottom: var(--s-1);
|
||||
}
|
||||
|
||||
.settings-textarea:focus {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.settings-save-row {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.settings-save-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--s-2) var(--s-1);
|
||||
font-family: inherit;
|
||||
font-size: var(--s-1);
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
background: var(--color-primary);
|
||||
border: none;
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s ease;
|
||||
min-width: var(--s2);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.settings-save-btn:hover {
|
||||
background: var(--color-primary-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.settings-save-btn:focus-visible {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.settings-save-btn--success {
|
||||
background: var(--color-green);
|
||||
}
|
||||
|
||||
.settings-save-btn .icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
/* ==================== */
|
||||
/* Animations */
|
||||
/* ==================== */
|
||||
@@ -668,4 +863,16 @@ body {
|
||||
max-width: 128px;
|
||||
font-size: var(--s-1);
|
||||
}
|
||||
|
||||
.settings-overlay {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.settings-panel {
|
||||
max-width: 100%;
|
||||
max-height: 100dvh;
|
||||
height: 100dvh;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ const ICONS_URL = '/static/icons.svg';
|
||||
const MODELS_ENDPOINT = '/v1/models';
|
||||
const MODEL_KEY = 'odidere_model';
|
||||
const STORAGE_KEY = 'odidere_history';
|
||||
const SYSTEM_MESSAGE_KEY = 'odidere_system_message';
|
||||
const VOICES_ENDPOINT = '/v1/voices';
|
||||
const VOICE_KEY = 'odidere_voice';
|
||||
|
||||
@@ -42,6 +43,15 @@ class Odidere {
|
||||
this.$voice = document.getElementById('voice');
|
||||
this.$mute = document.getElementById('mute');
|
||||
|
||||
// Settings modal
|
||||
this.$settings = document.getElementById('settings');
|
||||
this.$settingsOverlay = document.getElementById('settings-overlay');
|
||||
this.$settingsClose = document.getElementById('settings-close');
|
||||
this.$settingsTabs = document.querySelectorAll('.settings-tab');
|
||||
this.$settingsPanels = document.querySelectorAll('.settings-tab-panel');
|
||||
this.$systemMessageInput = document.getElementById('system-message-input');
|
||||
this.$saveSystemMessage = document.getElementById('save-system-message');
|
||||
|
||||
// Templates
|
||||
this.$tplAssistantMessage = document.getElementById(
|
||||
'tpl-assistant-message',
|
||||
@@ -151,6 +161,28 @@ class Odidere {
|
||||
});
|
||||
// Mute button
|
||||
this.$mute.addEventListener('click', () => this.#toggleMute());
|
||||
|
||||
// Settings modal
|
||||
this.$settings.addEventListener('click', () => this.openSettings());
|
||||
this.$settingsClose.addEventListener('click', () => this.closeSettings());
|
||||
this.$settingsOverlay.addEventListener('click', (e) => {
|
||||
if (e.target === this.$settingsOverlay) this.closeSettings();
|
||||
});
|
||||
this.document.addEventListener('keydown', (e) => {
|
||||
if (
|
||||
e.key === 'Escape' &&
|
||||
this.$settingsOverlay.classList.contains('open')
|
||||
) {
|
||||
e.preventDefault();
|
||||
this.closeSettings();
|
||||
}
|
||||
});
|
||||
this.$settingsTabs.forEach(($tab) => {
|
||||
$tab.addEventListener('click', () => this.#switchTab($tab.dataset.tab));
|
||||
});
|
||||
this.$saveSystemMessage.addEventListener('click', () =>
|
||||
this.#saveSystemMessage(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -557,6 +589,10 @@ class Odidere {
|
||||
voice: this.$voice.value,
|
||||
model: this.$model.value,
|
||||
};
|
||||
const systemMessage = localStorage.getItem(SYSTEM_MESSAGE_KEY);
|
||||
if (systemMessage) {
|
||||
payload.system_message = systemMessage;
|
||||
}
|
||||
if (audio) {
|
||||
payload.audio = await this.#toBase64(audio);
|
||||
}
|
||||
@@ -674,7 +710,11 @@ class Odidere {
|
||||
}
|
||||
|
||||
// Collect tool results and render once all have arrived.
|
||||
if (message.role === 'tool' && pendingTools && pendingTools.assistant) {
|
||||
if (
|
||||
message.role === 'tool' &&
|
||||
pendingTools &&
|
||||
pendingTools.assistant
|
||||
) {
|
||||
pendingTools.results.push(message);
|
||||
// Add to history (server needs it) but don't render yet.
|
||||
this.#appendHistory([message]);
|
||||
@@ -849,6 +889,67 @@ class Odidere {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* openSettings opens the settings modal and loads current values.
|
||||
*/
|
||||
openSettings() {
|
||||
this.$settingsOverlay.classList.add('open');
|
||||
this.#loadSystemMessage();
|
||||
this.#switchTab('model-voice');
|
||||
// Focus the close button for accessibility.
|
||||
this.$settingsClose.focus();
|
||||
}
|
||||
|
||||
/**
|
||||
* closeSettings closes the settings modal and restores focus.
|
||||
*/
|
||||
closeSettings() {
|
||||
this.$settingsOverlay.classList.remove('open');
|
||||
this.$settings.focus();
|
||||
}
|
||||
|
||||
/**
|
||||
* #switchTab switches the active tab in the settings modal.
|
||||
* @param {string} tabName
|
||||
*/
|
||||
#switchTab(tabName) {
|
||||
this.$settingsTabs.forEach(($tab) => {
|
||||
const isActive = $tab.dataset.tab === tabName;
|
||||
$tab.classList.toggle('active', isActive);
|
||||
$tab.setAttribute('aria-selected', String(isActive));
|
||||
});
|
||||
this.$settingsPanels.forEach(($panel) => {
|
||||
const isActive = $panel.dataset.tabPanel === tabName;
|
||||
$panel.classList.toggle('active', isActive);
|
||||
$panel.hidden = !isActive;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* #saveSystemMessage saves the textarea value to localStorage.
|
||||
*/
|
||||
#saveSystemMessage() {
|
||||
const value = this.$systemMessageInput.value;
|
||||
localStorage.setItem(SYSTEM_MESSAGE_KEY, value);
|
||||
|
||||
// Brief visual feedback on the save button.
|
||||
this.$saveSystemMessage.replaceChildren(this.#icon('check'));
|
||||
this.$saveSystemMessage.classList.add('settings-save-btn--success');
|
||||
|
||||
setTimeout(() => {
|
||||
this.$saveSystemMessage.textContent = 'Save';
|
||||
this.$saveSystemMessage.classList.remove('settings-save-btn--success');
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
/**
|
||||
* #loadSystemMessage reads from localStorage and populates the textarea.
|
||||
*/
|
||||
#loadSystemMessage() {
|
||||
const stored = localStorage.getItem(SYSTEM_MESSAGE_KEY);
|
||||
this.$systemMessageInput.value = stored || '';
|
||||
}
|
||||
|
||||
// ====================
|
||||
// RENDER: SELECTS
|
||||
// ====================
|
||||
|
||||
Reference in New Issue
Block a user