Add stored conversation settings
This commit is contained in:
@@ -12,17 +12,16 @@
|
||||
* conversation is created. It does not change a conversation that
|
||||
* already exists.
|
||||
* - odidere_provider, odidere_model, odidere_voice: default selections.
|
||||
* - odidere_conversation_models: per-conversation provider and model
|
||||
* selections, keyed by conversation ID.
|
||||
* - odidere_max_turns, odidere_reasoning_budget: compose settings.
|
||||
* - odidere_conversation_settings: per-conversation settings, keyed
|
||||
* by conversation ID. Each entry is complete: it holds the
|
||||
* provider, model, max iterations, and reasoning budget together.
|
||||
* - odidere_max_turns, odidere_reasoning_budget: default compose
|
||||
* settings, used when a conversation has no stored entry.
|
||||
* - odidere_mute: whether speech playback is muted.
|
||||
*
|
||||
* The browser-only history that older versions kept under
|
||||
* odidere_history is not read and is left untouched.
|
||||
*/
|
||||
|
||||
const CONVERSATION_KEY = 'odidere_conversation';
|
||||
const CONVERSATION_MODELS_KEY = 'odidere_conversation_models';
|
||||
const CONVERSATION_SETTINGS_KEY = 'odidere_conversation_settings';
|
||||
const SYSTEM_MESSAGE_KEY = 'odidere_system_message';
|
||||
const PROVIDER_KEY = 'odidere_provider';
|
||||
const MODEL_KEY = 'odidere_model';
|
||||
@@ -112,88 +111,91 @@ export class Storage {
|
||||
}
|
||||
|
||||
/**
|
||||
* _loadConversationModels reads the per-conversation model map.
|
||||
* @returns {Object<string, {provider: string, model: string}>}
|
||||
* _loadConversationSettings reads the per-conversation settings map.
|
||||
* @returns {Object<string, {provider: string, model: string, maxIterations: number, reasoningBudget: number | null}>}
|
||||
* @private
|
||||
*/
|
||||
_loadConversationModels() {
|
||||
_loadConversationSettings() {
|
||||
try {
|
||||
const parsed = JSON.parse(localStorage.getItem(CONVERSATION_MODELS_KEY));
|
||||
const parsed = JSON.parse(
|
||||
localStorage.getItem(CONVERSATION_SETTINGS_KEY),
|
||||
);
|
||||
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
||||
? parsed
|
||||
: {};
|
||||
} catch (error) {
|
||||
console.error('failed to load conversation models:', error);
|
||||
console.error('failed to load conversation settings:', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* getConversationModel returns the provider and model stored for
|
||||
* one conversation, or null fields when none is stored.
|
||||
* getConversationSettings returns the settings stored for one
|
||||
* conversation, or null when none are stored.
|
||||
* @param {string} id - conversation ID.
|
||||
* @returns {{provider: string | null, model: string | null}}
|
||||
* @returns {{provider: string, model: string, maxIterations: number, reasoningBudget: number | null} | null}
|
||||
*/
|
||||
getConversationModel(id) {
|
||||
const entry = this._loadConversationModels()[id];
|
||||
return {
|
||||
provider: entry?.provider || null,
|
||||
model: entry?.model || null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* setConversationModel stores the provider and model for one
|
||||
* conversation.
|
||||
* @param {string} id - conversation ID.
|
||||
* @param {{provider: string, model: string}} selection
|
||||
*/
|
||||
setConversationModel(id, { provider, model }) {
|
||||
if (!id || !provider || !model) return;
|
||||
try {
|
||||
const map = this._loadConversationModels();
|
||||
map[id] = { provider, model };
|
||||
localStorage.setItem(CONVERSATION_MODELS_KEY, JSON.stringify(map));
|
||||
} catch (error) {
|
||||
console.error('failed to save conversation model:', error);
|
||||
getConversationSettings(id) {
|
||||
const entry = this._loadConversationSettings()[id];
|
||||
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
||||
return null;
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* deleteConversationModel removes the stored model entry for one
|
||||
* conversation.
|
||||
* setConversationSettings stores the settings for one
|
||||
* conversation. Store the complete settings: a stored entry is
|
||||
* used as stored, and no lookup completes it from the defaults.
|
||||
* @param {string} id - conversation ID.
|
||||
* @param {{provider: string, model: string, maxIterations: number, reasoningBudget: number | null}} settings
|
||||
*/
|
||||
deleteConversationModel(id) {
|
||||
setConversationSettings(id, settings) {
|
||||
if (!id) return;
|
||||
try {
|
||||
const map = this._loadConversationModels();
|
||||
if (!(id in map)) return;
|
||||
delete map[id];
|
||||
localStorage.setItem(CONVERSATION_MODELS_KEY, JSON.stringify(map));
|
||||
const map = this._loadConversationSettings();
|
||||
map[id] = settings;
|
||||
localStorage.setItem(CONVERSATION_SETTINGS_KEY, JSON.stringify(map));
|
||||
} catch (error) {
|
||||
console.error('failed to delete conversation model:', error);
|
||||
console.error('failed to save conversation settings:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* pruneConversationModels removes stored model entries for
|
||||
* deleteConversationSettings removes the stored settings for one
|
||||
* conversation.
|
||||
* @param {string} id - conversation ID.
|
||||
*/
|
||||
deleteConversationSettings(id) {
|
||||
if (!id) return;
|
||||
try {
|
||||
const map = this._loadConversationSettings();
|
||||
if (!(id in map)) return;
|
||||
delete map[id];
|
||||
localStorage.setItem(CONVERSATION_SETTINGS_KEY, JSON.stringify(map));
|
||||
} catch (error) {
|
||||
console.error('failed to delete conversation settings:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* pruneConversationSettings removes stored settings for
|
||||
* conversations that no longer exist on the server (for example,
|
||||
* conversations deleted from another device or browser tab), so
|
||||
* the localStorage map does not grow without bound.
|
||||
* @param {Iterable<string>} validIds - conversation IDs currently
|
||||
* on the server.
|
||||
*/
|
||||
pruneConversationModels(validIds) {
|
||||
pruneConversationSettings(validIds) {
|
||||
try {
|
||||
const keep = new Set(validIds);
|
||||
const map = this._loadConversationModels();
|
||||
const map = this._loadConversationSettings();
|
||||
for (const id of Object.keys(map)) {
|
||||
if (!keep.has(id)) delete map[id];
|
||||
}
|
||||
localStorage.setItem(CONVERSATION_MODELS_KEY, JSON.stringify(map));
|
||||
localStorage.setItem(CONVERSATION_SETTINGS_KEY, JSON.stringify(map));
|
||||
} catch (error) {
|
||||
console.error('failed to prune conversation models:', error);
|
||||
console.error('failed to prune conversation settings:', error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,11 +217,12 @@ export class Storage {
|
||||
/**
|
||||
* getMaxIterations returns the agent loop limit. Zero means no
|
||||
* limit: the model replies without tool calls or the run fails.
|
||||
* The value is returned as stored and is not checked:
|
||||
* resolveConversationSettings corrects invalid values before use.
|
||||
* @returns {number}
|
||||
*/
|
||||
getMaxIterations() {
|
||||
const stored = Number(localStorage.getItem(MAX_TURNS_KEY));
|
||||
return Number.isFinite(stored) && stored > 0 ? stored : 0;
|
||||
return Number(localStorage.getItem(MAX_TURNS_KEY));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -233,14 +236,14 @@ export class Storage {
|
||||
|
||||
/**
|
||||
* getReasoningBudget returns the llama.cpp reasoning token budget,
|
||||
* or null when unset. -1 means unlimited and 0 means off.
|
||||
* or null when unset. -1 means unlimited and 0 means off. The
|
||||
* value is returned as stored and is not checked:
|
||||
* resolveConversationSettings corrects invalid values before use.
|
||||
* @returns {number | null}
|
||||
*/
|
||||
getReasoningBudget() {
|
||||
const stored = localStorage.getItem(REASONING_BUDGET_KEY);
|
||||
if (stored === null) return null;
|
||||
const value = Number(stored);
|
||||
return Number.isFinite(value) ? value : null;
|
||||
return stored === null ? null : Number(stored);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user