Files
odidere/internal/service/static/js/storage.js
T
2026-10-30 22:42:51 +00:00

185 lines
5.5 KiB
JavaScript

/**
* Storage reads and writes the browser's local cache and the saved
* client settings. It is the only module that touches localStorage.
* It does no rendering and sends no requests: main.js decides what
* to cache and when, and Storage only stores and returns it.
*
* Keys and their contents:
* - odidere_conversation: the selected conversation ID and its saved
* message rows, for offline viewing and a fast start. The server
* owns the history; this entry is a copy, replaced by every sync.
* - odidere_system_message: the system message used when the next
* conversation is created. It does not change a conversation that
* already exists.
* - odidere_provider, odidere_model, odidere_voice: selections.
* - odidere_max_turns, odidere_reasoning_budget: compose settings.
* - 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 SYSTEM_MESSAGE_KEY = 'odidere_system_message';
const PROVIDER_KEY = 'odidere_provider';
const MODEL_KEY = 'odidere_model';
const VOICE_KEY = 'odidere_voice';
const MAX_TURNS_KEY = 'odidere_max_turns';
const REASONING_BUDGET_KEY = 'odidere_reasoning_budget';
const MUTE_KEY = 'odidere_mute';
export class Storage {
/**
* loadConversation returns the cached selection: the conversation
* ID and its message rows in saved order. Rows are stored under
* their message IDs; the array order is the saved order.
* @returns {{id: string, rows: Array<Object>} | null}
*/
loadConversation() {
try {
const cached = JSON.parse(localStorage.getItem(CONVERSATION_KEY));
if (!cached?.id || !Array.isArray(cached.rows)) return null;
return { id: cached.id, rows: cached.rows };
} catch (error) {
console.error('failed to load conversation cache:', error);
return null;
}
}
/**
* saveConversation caches the selected conversation and its rows.
* The server owns the history; this is a local copy for offline
* viewing. Streaming drafts are never passed to this method.
* @param {string} id
* @param {Array<Object>} rows
*/
saveConversation(id, rows) {
try {
localStorage.setItem(CONVERSATION_KEY, JSON.stringify({ id, rows }));
} catch (error) {
console.error('failed to cache conversation:', error);
}
}
/**
* clearConversation drops the cached selection. It does not
* change or delete the conversation on the server.
*/
clearConversation() {
try {
localStorage.removeItem(CONVERSATION_KEY);
} catch (error) {
console.error('failed to clear conversation cache:', error);
}
}
// --- System message ---
/** @returns {string} the saved default system message, or '' */
getSystemMessage() {
return localStorage.getItem(SYSTEM_MESSAGE_KEY) || '';
}
/**
* setSystemMessage stores the default system message for the next
* conversation. It does not change any existing conversation.
* @param {string} value
*/
setSystemMessage(value) {
localStorage.setItem(SYSTEM_MESSAGE_KEY, value);
}
// --- Model and voice ---
/** @returns {{provider: string | null, model: string | null}} */
getModelSelection() {
return {
provider: localStorage.getItem(PROVIDER_KEY),
model: localStorage.getItem(MODEL_KEY),
};
}
/**
* setModelSelection stores the selected provider and model.
* @param {{provider: string, model: string}} selection
*/
setModelSelection({ provider, model }) {
localStorage.setItem(PROVIDER_KEY, provider);
localStorage.setItem(MODEL_KEY, model);
}
/** @returns {string} the selected voice, or '' for automatic */
getVoice() {
return localStorage.getItem(VOICE_KEY) || '';
}
/**
* setVoice stores the selected voice. '' selects the default.
* @param {string} voice
*/
setVoice(voice) {
localStorage.setItem(VOICE_KEY, voice);
}
// --- Compose settings ---
/**
* getMaxIterations returns the agent loop limit. Zero means no
* limit: the model replies without tool calls or the run fails.
* @returns {number}
*/
getMaxIterations() {
const stored = Number(localStorage.getItem(MAX_TURNS_KEY));
return Number.isFinite(stored) && stored > 0 ? stored : 0;
}
/**
* setMaxIterations stores the agent loop limit. Zero means no
* limit.
* @param {number} value
*/
setMaxIterations(value) {
localStorage.setItem(MAX_TURNS_KEY, String(value));
}
/**
* getReasoningBudget returns the llama.cpp reasoning token budget,
* or null when unset. -1 means unlimited and 0 means off.
* @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;
}
/**
* setReasoningBudget stores the reasoning token budget. null
* removes the setting and leaves the choice to the server.
* @param {number | null} value
*/
setReasoningBudget(value) {
if (value === null) {
localStorage.removeItem(REASONING_BUDGET_KEY);
} else {
localStorage.setItem(REASONING_BUDGET_KEY, String(value));
}
}
// --- Mute ---
/** @returns {boolean} whether speech playback is muted */
getMuted() {
return localStorage.getItem(MUTE_KEY) === 'true';
}
/**
* setMuted stores the speech mute state.
* @param {boolean} muted
*/
setMuted(muted) {
localStorage.setItem(MUTE_KEY, String(muted));
}
}