lite_webui / src /store.js
blyon1995's picture
fix: silent failure when sending image after video
c43b369
// LocalStorage keys
const KEYS = {
SETTINGS: 'lw_settings',
CONVERSATIONS: 'lw_conversations',
CURRENT_CONV: 'lw_current_conv',
MODEL_CAPS: 'lw_model_caps',
AVAILABLE_MODELS: 'lw_available_models',
MODEL_SELECTIONS: 'lw_model_selections',
};
const DEFAULT_SETTINGS = {
apiKey: '',
baseUrl: 'http://127.0.0.1:8000',
theme: 'dark',
contextLimitTokens: 4096,
contextResetThresholdPercent: 85,
};
export function normalizeBaseUrl(baseUrl) {
const raw = String(baseUrl || '').trim();
if (!raw) return DEFAULT_SETTINGS.baseUrl;
return raw.replace(/\/+$/, '');
}
function isCapabilityRecord(value) {
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
return ['text', 'image', 'audio'].some((key) => key in value);
}
function isLegacyCapabilityMap(value) {
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
const entries = Object.values(value);
return entries.length > 0 && entries.every(isCapabilityRecord);
}
function normalizeSettings(settings = {}) {
const merged = { ...DEFAULT_SETTINGS, ...settings };
const contextLimitTokens = Number(merged.contextLimitTokens);
const contextResetThresholdPercent = Number(merged.contextResetThresholdPercent);
merged.baseUrl = normalizeBaseUrl(merged.baseUrl);
merged.contextLimitTokens = Number.isFinite(contextLimitTokens) && contextLimitTokens >= 1024
? Math.round(contextLimitTokens)
: DEFAULT_SETTINGS.contextLimitTokens;
merged.contextResetThresholdPercent = Number.isFinite(contextResetThresholdPercent)
? Math.min(95, Math.max(50, Math.round(contextResetThresholdPercent)))
: DEFAULT_SETTINGS.contextResetThresholdPercent;
return merged;
}
function load(key, fallback) {
try {
const raw = localStorage.getItem(key);
return raw ? JSON.parse(raw) : fallback;
} catch {
return fallback;
}
}
function save(key, value) {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (e) {
if (e?.name === 'QuotaExceededError' || e?.code === 22) {
console.warn('[store] localStorage quota exceeded — conversation not persisted');
} else {
throw e;
}
}
}
function uuid() {
return crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2) + Date.now().toString(36);
}
export const store = {
getSettings() {
return normalizeSettings(load(KEYS.SETTINGS, {}));
},
saveSettings(settings) {
save(KEYS.SETTINGS, normalizeSettings(settings));
},
getConversations() {
return load(KEYS.CONVERSATIONS, []);
},
saveConversations(conversations) {
save(KEYS.CONVERSATIONS, conversations);
},
getCurrentConversationId() {
return localStorage.getItem(KEYS.CURRENT_CONV) || null;
},
setCurrentConversationId(id) {
if (id) {
localStorage.setItem(KEYS.CURRENT_CONV, id);
} else {
localStorage.removeItem(KEYS.CURRENT_CONV);
}
},
getCurrentConversation() {
const id = this.getCurrentConversationId();
if (!id) return null;
const convs = this.getConversations();
return convs.find(c => c.id === id) || null;
},
getAvailableModels(baseUrl = this.getSettings().baseUrl) {
const catalogs = load(KEYS.AVAILABLE_MODELS, {});
const list = catalogs[normalizeBaseUrl(baseUrl)];
return Array.isArray(list) ? [...new Set(list.filter(Boolean))].sort() : [];
},
saveAvailableModels(baseUrl, models) {
const catalogs = load(KEYS.AVAILABLE_MODELS, {});
catalogs[normalizeBaseUrl(baseUrl)] = Array.isArray(models)
? [...new Set(models.filter(Boolean))].sort()
: [];
save(KEYS.AVAILABLE_MODELS, catalogs);
},
getCurrentModel(baseUrl = this.getSettings().baseUrl) {
const selections = load(KEYS.MODEL_SELECTIONS, {});
const selected = selections[normalizeBaseUrl(baseUrl)];
return typeof selected === 'string' ? selected : '';
},
setCurrentModel(baseUrl, modelId) {
const selections = load(KEYS.MODEL_SELECTIONS, {});
const normalizedUrl = normalizeBaseUrl(baseUrl);
if (modelId) {
selections[normalizedUrl] = modelId;
} else {
delete selections[normalizedUrl];
}
save(KEYS.MODEL_SELECTIONS, selections);
},
getModelCapabilities(baseUrl = this.getSettings().baseUrl) {
const raw = load(KEYS.MODEL_CAPS, {});
if (isLegacyCapabilityMap(raw)) return raw;
const caps = raw[normalizeBaseUrl(baseUrl)];
return caps && typeof caps === 'object' && !Array.isArray(caps) ? caps : {};
},
saveModelCapabilities(baseUrlOrCaps, maybeCaps) {
if (maybeCaps === undefined) {
save(KEYS.MODEL_CAPS, baseUrlOrCaps);
return;
}
const raw = load(KEYS.MODEL_CAPS, {});
const nested = isLegacyCapabilityMap(raw) ? {} : raw;
nested[normalizeBaseUrl(baseUrlOrCaps)] = maybeCaps;
save(KEYS.MODEL_CAPS, nested);
},
createConversation(model) {
const conv = {
id: uuid(),
title: 'New Chat',
model: model || '',
messages: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
const convs = this.getConversations();
convs.unshift(conv);
this.saveConversations(convs);
return conv;
},
addMessage(convId, message) {
const convs = this.getConversations();
const idx = convs.findIndex(c => c.id === convId);
if (idx === -1) return;
convs[idx].messages.push(message);
convs[idx].updatedAt = new Date().toISOString();
this.saveConversations(convs);
},
updateLastAssistantMessage(convId, content) {
const convs = this.getConversations();
const idx = convs.findIndex(c => c.id === convId);
if (idx === -1) return;
const msgs = convs[idx].messages;
// Find last assistant message
for (let i = msgs.length - 1; i >= 0; i--) {
if (msgs[i].role === 'assistant') {
msgs[i].content = content;
msgs[i].timestamp = new Date().toISOString();
break;
}
}
convs[idx].updatedAt = new Date().toISOString();
this.saveConversations(convs);
},
clearMessages(convId) {
const convs = this.getConversations();
const idx = convs.findIndex(c => c.id === convId);
if (idx === -1) return;
convs[idx].messages = [];
convs[idx].updatedAt = new Date().toISOString();
this.saveConversations(convs);
},
deleteConversation(convId) {
let convs = this.getConversations();
convs = convs.filter(c => c.id !== convId);
this.saveConversations(convs);
if (this.getCurrentConversationId() === convId) {
this.setCurrentConversationId(convs[0]?.id || null);
}
},
updateConversationTitle(convId, title) {
const convs = this.getConversations();
const idx = convs.findIndex(c => c.id === convId);
if (idx === -1) return;
convs[idx].title = title;
this.saveConversations(convs);
},
updateConversationModel(convId, model) {
const convs = this.getConversations();
const idx = convs.findIndex(c => c.id === convId);
if (idx === -1) return;
convs[idx].model = model;
this.saveConversations(convs);
},
};