|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let activeRequests = new Map(); |
|
|
|
|
|
|
|
|
let currentAbortController = null; |
|
|
let currentReader = null; |
|
|
let tokensPerSecondInterval = null; |
|
|
let tokensPerSecondIntervalChatId = null; |
|
|
let lastTokensPerSecond = null; |
|
|
|
|
|
|
|
|
const CHATS_STORAGE_KEY = 'localai_chats_data'; |
|
|
const SYSTEM_PROMPT_STORAGE_KEY = 'system_prompt'; |
|
|
|
|
|
|
|
|
let saveDebounceTimer = null; |
|
|
const SAVE_DEBOUNCE_MS = 500; |
|
|
|
|
|
|
|
|
function saveChatsToStorage() { |
|
|
if (!window.Alpine || !Alpine.store("chat")) { |
|
|
return false; |
|
|
} |
|
|
|
|
|
try { |
|
|
const chatStore = Alpine.store("chat"); |
|
|
const data = { |
|
|
chats: chatStore.chats.map(chat => ({ |
|
|
id: chat.id, |
|
|
name: chat.name, |
|
|
model: chat.model, |
|
|
history: chat.history, |
|
|
systemPrompt: chat.systemPrompt, |
|
|
mcpMode: chat.mcpMode, |
|
|
tokenUsage: chat.tokenUsage, |
|
|
contextSize: chat.contextSize, |
|
|
createdAt: chat.createdAt, |
|
|
updatedAt: chat.updatedAt |
|
|
})), |
|
|
activeChatId: chatStore.activeChatId, |
|
|
lastSaved: Date.now() |
|
|
}; |
|
|
|
|
|
const jsonData = JSON.stringify(data); |
|
|
localStorage.setItem(CHATS_STORAGE_KEY, jsonData); |
|
|
return true; |
|
|
} catch (error) { |
|
|
|
|
|
if (error.name === 'QuotaExceededError' || error.code === 22) { |
|
|
console.warn('localStorage quota exceeded. Consider cleaning up old chats.'); |
|
|
|
|
|
try { |
|
|
const chatStore = Alpine.store("chat"); |
|
|
const data = { |
|
|
chats: chatStore.chats.map(chat => ({ |
|
|
id: chat.id, |
|
|
name: chat.name, |
|
|
model: chat.model, |
|
|
history: [], |
|
|
systemPrompt: chat.systemPrompt, |
|
|
mcpMode: chat.mcpMode, |
|
|
tokenUsage: chat.tokenUsage, |
|
|
contextSize: chat.contextSize, |
|
|
createdAt: chat.createdAt, |
|
|
updatedAt: chat.updatedAt |
|
|
})), |
|
|
activeChatId: chatStore.activeChatId, |
|
|
lastSaved: Date.now() |
|
|
}; |
|
|
localStorage.setItem(CHATS_STORAGE_KEY, JSON.stringify(data)); |
|
|
return true; |
|
|
} catch (e2) { |
|
|
console.error('Failed to save chats even without history:', e2); |
|
|
return false; |
|
|
} |
|
|
} else { |
|
|
console.error('Error saving chats to localStorage:', error); |
|
|
return false; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function loadChatsFromStorage() { |
|
|
try { |
|
|
const stored = localStorage.getItem(CHATS_STORAGE_KEY); |
|
|
if (stored) { |
|
|
const data = JSON.parse(stored); |
|
|
|
|
|
|
|
|
if (data && Array.isArray(data.chats)) { |
|
|
return { |
|
|
chats: data.chats, |
|
|
activeChatId: data.activeChatId || null, |
|
|
lastSaved: data.lastSaved || null |
|
|
}; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const oldSystemPrompt = localStorage.getItem(SYSTEM_PROMPT_STORAGE_KEY); |
|
|
if (oldSystemPrompt) { |
|
|
|
|
|
const chatStore = Alpine.store("chat"); |
|
|
if (chatStore) { |
|
|
const migratedChat = chatStore.createChat( |
|
|
document.getElementById("chat-model")?.value || "", |
|
|
oldSystemPrompt, |
|
|
false |
|
|
); |
|
|
|
|
|
if (chatStore.activeChat()) { |
|
|
chatStore.activeChat().name = "Migrated Chat"; |
|
|
} |
|
|
|
|
|
saveChatsToStorage(); |
|
|
|
|
|
localStorage.removeItem(SYSTEM_PROMPT_STORAGE_KEY); |
|
|
return { |
|
|
chats: chatStore.chats, |
|
|
activeChatId: chatStore.activeChatId, |
|
|
lastSaved: Date.now() |
|
|
}; |
|
|
} |
|
|
} |
|
|
|
|
|
return null; |
|
|
} catch (error) { |
|
|
console.error('Error loading chats from localStorage:', error); |
|
|
|
|
|
try { |
|
|
localStorage.removeItem(CHATS_STORAGE_KEY); |
|
|
} catch (e) { |
|
|
console.error('Failed to clear corrupted data:', e); |
|
|
} |
|
|
return null; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function autoSaveChats() { |
|
|
if (saveDebounceTimer) { |
|
|
clearTimeout(saveDebounceTimer); |
|
|
} |
|
|
saveDebounceTimer = setTimeout(() => { |
|
|
saveChatsToStorage(); |
|
|
}, SAVE_DEBOUNCE_MS); |
|
|
} |
|
|
|
|
|
|
|
|
function isChatRequestActive(chatId) { |
|
|
if (!chatId || !activeRequests) { |
|
|
return false; |
|
|
} |
|
|
const request = activeRequests.get(chatId); |
|
|
return request && (request.controller || request.reader); |
|
|
} |
|
|
|
|
|
|
|
|
function updateRequestTracking(chatId, isActive) { |
|
|
const chatStore = Alpine.store("chat"); |
|
|
if (chatStore && typeof chatStore.updateActiveRequestTracking === 'function') { |
|
|
chatStore.updateActiveRequestTracking(chatId, isActive); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
window.autoSaveChats = autoSaveChats; |
|
|
window.createNewChat = createNewChat; |
|
|
window.switchChat = switchChat; |
|
|
window.deleteChat = deleteChat; |
|
|
window.bulkDeleteChats = bulkDeleteChats; |
|
|
window.updateChatName = updateChatName; |
|
|
window.updateUIForActiveChat = updateUIForActiveChat; |
|
|
window.isChatRequestActive = isChatRequestActive; |
|
|
|
|
|
|
|
|
function createNewChat(model, systemPrompt, mcpMode) { |
|
|
if (!window.Alpine || !Alpine.store("chat")) { |
|
|
return null; |
|
|
} |
|
|
|
|
|
const chatStore = Alpine.store("chat"); |
|
|
const chat = chatStore.createChat(model, systemPrompt, mcpMode); |
|
|
|
|
|
|
|
|
saveChatsToStorage(); |
|
|
|
|
|
|
|
|
updateUIForActiveChat(); |
|
|
|
|
|
return chat; |
|
|
} |
|
|
|
|
|
|
|
|
function switchChat(chatId) { |
|
|
if (!window.Alpine || !Alpine.store("chat")) { |
|
|
return false; |
|
|
} |
|
|
|
|
|
const chatStore = Alpine.store("chat"); |
|
|
const oldActiveChat = chatStore.activeChat(); |
|
|
|
|
|
if (chatStore.switchChat(chatId)) { |
|
|
|
|
|
|
|
|
if (tokensPerSecondInterval) { |
|
|
clearInterval(tokensPerSecondInterval); |
|
|
tokensPerSecondInterval = null; |
|
|
} |
|
|
|
|
|
|
|
|
const tokensPerSecondDisplay = document.getElementById('tokens-per-second'); |
|
|
if (tokensPerSecondDisplay) { |
|
|
tokensPerSecondDisplay.textContent = '-'; |
|
|
} |
|
|
|
|
|
|
|
|
saveChatsToStorage(); |
|
|
|
|
|
|
|
|
const maxBadge = document.getElementById('max-tokens-per-second-badge'); |
|
|
if (maxBadge) { |
|
|
maxBadge.style.display = 'none'; |
|
|
} |
|
|
|
|
|
|
|
|
const newActiveChat = chatStore.activeChat(); |
|
|
const newRequest = activeRequests.get(newActiveChat?.id); |
|
|
if (newRequest) { |
|
|
currentAbortController = newRequest.controller; |
|
|
currentReader = newRequest.reader; |
|
|
|
|
|
const hasActiveRequest = newRequest.controller || newRequest.reader; |
|
|
if (hasActiveRequest) { |
|
|
toggleLoader(true, newActiveChat.id); |
|
|
|
|
|
setTimeout(() => { |
|
|
|
|
|
const currentActiveChat = chatStore.activeChat(); |
|
|
if (currentActiveChat && currentActiveChat.id === newActiveChat.id) { |
|
|
|
|
|
if (tokensPerSecondInterval) { |
|
|
clearInterval(tokensPerSecondInterval); |
|
|
tokensPerSecondInterval = null; |
|
|
tokensPerSecondIntervalChatId = null; |
|
|
} |
|
|
|
|
|
updateTokensPerSecond(newActiveChat.id); |
|
|
|
|
|
startTokensPerSecondInterval(); |
|
|
} |
|
|
}, 100); |
|
|
} else { |
|
|
toggleLoader(false, newActiveChat.id); |
|
|
} |
|
|
} else { |
|
|
|
|
|
currentAbortController = null; |
|
|
currentReader = null; |
|
|
toggleLoader(false, newActiveChat?.id); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
updateUIForActiveChat(); |
|
|
|
|
|
return true; |
|
|
} |
|
|
return false; |
|
|
} |
|
|
|
|
|
|
|
|
function deleteChat(chatId) { |
|
|
if (!window.Alpine || !Alpine.store("chat")) { |
|
|
return false; |
|
|
} |
|
|
|
|
|
const chatStore = Alpine.store("chat"); |
|
|
|
|
|
|
|
|
if (chatStore.chats.length <= 1) { |
|
|
alert('Cannot delete the last chat. Please create a new chat first.'); |
|
|
return false; |
|
|
} |
|
|
|
|
|
if (chatStore.deleteChat(chatId)) { |
|
|
|
|
|
if (chatStore.chats.length === 0) { |
|
|
const currentModel = document.getElementById("chat-model")?.value || ""; |
|
|
chatStore.createChat(currentModel, "", false); |
|
|
} |
|
|
|
|
|
saveChatsToStorage(); |
|
|
updateUIForActiveChat(); |
|
|
return true; |
|
|
} |
|
|
return false; |
|
|
} |
|
|
|
|
|
|
|
|
function bulkDeleteChats(options) { |
|
|
if (!window.Alpine || !Alpine.store("chat")) { |
|
|
return 0; |
|
|
} |
|
|
|
|
|
const chatStore = Alpine.store("chat"); |
|
|
let deletedCount = 0; |
|
|
const now = Date.now(); |
|
|
|
|
|
if (options.deleteAll) { |
|
|
|
|
|
const activeId = chatStore.activeChatId; |
|
|
chatStore.chats = chatStore.chats.filter(chat => { |
|
|
if (chat.id === activeId && chatStore.chats.length > 1) { |
|
|
return true; |
|
|
} |
|
|
deletedCount++; |
|
|
return false; |
|
|
}); |
|
|
|
|
|
|
|
|
if (chatStore.chats.length === 0) { |
|
|
chatStore.createChat(); |
|
|
} else if (!chatStore.chats.find(c => c.id === activeId)) { |
|
|
|
|
|
if (chatStore.chats.length > 0) { |
|
|
chatStore.activeChatId = chatStore.chats[0].id; |
|
|
} |
|
|
} |
|
|
} else if (options.olderThanDays) { |
|
|
const cutoffTime = now - (options.olderThanDays * 24 * 60 * 60 * 1000); |
|
|
const activeId = chatStore.activeChatId; |
|
|
|
|
|
chatStore.chats = chatStore.chats.filter(chat => { |
|
|
if (chat.id === activeId) { |
|
|
return true; |
|
|
} |
|
|
if (chat.updatedAt < cutoffTime) { |
|
|
deletedCount++; |
|
|
return false; |
|
|
} |
|
|
return true; |
|
|
}); |
|
|
|
|
|
|
|
|
if (chatStore.chats.length === 0) { |
|
|
const currentModel = document.getElementById("chat-model")?.value || ""; |
|
|
chatStore.createChat(currentModel, "", false); |
|
|
} |
|
|
} |
|
|
|
|
|
if (deletedCount > 0) { |
|
|
saveChatsToStorage(); |
|
|
updateUIForActiveChat(); |
|
|
} |
|
|
|
|
|
return deletedCount; |
|
|
} |
|
|
|
|
|
|
|
|
function updateUIForActiveChat() { |
|
|
if (!window.Alpine || !Alpine.store("chat")) { |
|
|
return; |
|
|
} |
|
|
|
|
|
const chatStore = Alpine.store("chat"); |
|
|
|
|
|
|
|
|
if (!chatStore.chats || chatStore.chats.length === 0) { |
|
|
const currentModel = document.getElementById("chat-model")?.value || ""; |
|
|
chatStore.createChat(currentModel, "", false); |
|
|
} |
|
|
|
|
|
const activeChat = chatStore.activeChat(); |
|
|
|
|
|
if (!activeChat) { |
|
|
|
|
|
if (chatStore.chats.length > 0) { |
|
|
chatStore.activeChatId = chatStore.chats[0].id; |
|
|
} else { |
|
|
|
|
|
const currentModel = document.getElementById("chat-model")?.value || ""; |
|
|
chatStore.createChat(currentModel, "", false); |
|
|
} |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
const systemPromptInput = document.getElementById("systemPrompt"); |
|
|
if (systemPromptInput) { |
|
|
systemPromptInput.value = activeChat.systemPrompt || ""; |
|
|
} |
|
|
|
|
|
|
|
|
const mcpToggle = document.getElementById("mcp-toggle"); |
|
|
if (mcpToggle) { |
|
|
mcpToggle.checked = activeChat.mcpMode || false; |
|
|
} |
|
|
|
|
|
|
|
|
const modelSelector = document.getElementById("modelSelector"); |
|
|
if (modelSelector && activeChat.model) { |
|
|
|
|
|
for (let option of modelSelector.options) { |
|
|
if (option.value === `chat/${activeChat.model}` || option.text === activeChat.model) { |
|
|
option.selected = true; |
|
|
break; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const chatModelInput = document.getElementById("chat-model"); |
|
|
if (chatModelInput) { |
|
|
chatModelInput.value = activeChat.model || ""; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function updateChatName(chatId, name) { |
|
|
if (!window.Alpine || !Alpine.store("chat")) { |
|
|
return false; |
|
|
} |
|
|
|
|
|
const chatStore = Alpine.store("chat"); |
|
|
if (chatStore.updateChatName(chatId, name)) { |
|
|
autoSaveChats(); |
|
|
return true; |
|
|
} |
|
|
return false; |
|
|
} |
|
|
|
|
|
function toggleLoader(show, chatId = null) { |
|
|
const sendButton = document.getElementById('send-button'); |
|
|
const stopButton = document.getElementById('stop-button'); |
|
|
const headerLoadingIndicator = document.getElementById('header-loading-indicator'); |
|
|
const tokensPerSecondDisplay = document.getElementById('tokens-per-second'); |
|
|
|
|
|
if (show) { |
|
|
sendButton.style.display = 'none'; |
|
|
stopButton.style.display = 'block'; |
|
|
if (headerLoadingIndicator) headerLoadingIndicator.style.display = 'block'; |
|
|
|
|
|
|
|
|
const chatStore = Alpine.store("chat"); |
|
|
const activeChat = chatStore.activeChat(); |
|
|
|
|
|
|
|
|
if (tokensPerSecondInterval) { |
|
|
clearInterval(tokensPerSecondInterval); |
|
|
tokensPerSecondInterval = null; |
|
|
} |
|
|
|
|
|
|
|
|
const targetChatId = chatId || (activeChat ? activeChat.id : null); |
|
|
|
|
|
if (tokensPerSecondDisplay && targetChatId && activeChat && activeChat.id === targetChatId) { |
|
|
tokensPerSecondDisplay.textContent = '-'; |
|
|
|
|
|
const maxBadge = document.getElementById('max-tokens-per-second-badge'); |
|
|
if (maxBadge) { |
|
|
maxBadge.style.display = 'none'; |
|
|
} |
|
|
|
|
|
|
|
|
updateTokensPerSecond(targetChatId); |
|
|
} else if (tokensPerSecondDisplay) { |
|
|
|
|
|
tokensPerSecondDisplay.textContent = '-'; |
|
|
} |
|
|
} else { |
|
|
sendButton.style.display = 'block'; |
|
|
stopButton.style.display = 'none'; |
|
|
if (headerLoadingIndicator) headerLoadingIndicator.style.display = 'none'; |
|
|
|
|
|
const chatStore = Alpine.store("chat"); |
|
|
const activeChat = chatStore.activeChat(); |
|
|
if (chatId && activeChat && activeChat.id === chatId) { |
|
|
|
|
|
stopTokensPerSecondInterval(); |
|
|
|
|
|
if (tokensPerSecondDisplay && lastTokensPerSecond !== null) { |
|
|
tokensPerSecondDisplay.textContent = lastTokensPerSecond; |
|
|
} |
|
|
|
|
|
const activeRequest = activeRequests.get(activeChat.id); |
|
|
if (activeRequest && (activeRequest.controller || activeRequest.reader)) { |
|
|
|
|
|
startTokensPerSecondInterval(); |
|
|
} |
|
|
} else if (tokensPerSecondDisplay) { |
|
|
|
|
|
tokensPerSecondDisplay.textContent = '-'; |
|
|
} |
|
|
|
|
|
if (chatId && activeChat && activeChat.id === chatId) { |
|
|
currentAbortController = null; |
|
|
currentReader = null; |
|
|
|
|
|
|
|
|
const request = activeRequests.get(chatId); |
|
|
if (request && request.maxTokensPerSecond > 0) { |
|
|
updateMaxTokensPerSecondBadge(chatId, request.maxTokensPerSecond); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function startTokensPerSecondInterval() { |
|
|
|
|
|
stopTokensPerSecondInterval(); |
|
|
|
|
|
|
|
|
const chatStore = Alpine.store("chat"); |
|
|
if (!chatStore) { |
|
|
return; |
|
|
} |
|
|
|
|
|
const activeChat = chatStore.activeChat(); |
|
|
if (!activeChat) { |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const request = activeRequests.get(activeChat.id); |
|
|
if (!request) { |
|
|
|
|
|
return; |
|
|
} |
|
|
|
|
|
if (!request.controller) { |
|
|
|
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
tokensPerSecondIntervalChatId = activeChat.id; |
|
|
|
|
|
|
|
|
|
|
|
tokensPerSecondInterval = setInterval(() => { |
|
|
|
|
|
const currentChatStore = Alpine.store("chat"); |
|
|
if (!currentChatStore) { |
|
|
stopTokensPerSecondInterval(); |
|
|
return; |
|
|
} |
|
|
|
|
|
const currentActiveChat = currentChatStore.activeChat(); |
|
|
const tokensPerSecondDisplay = document.getElementById('tokens-per-second'); |
|
|
|
|
|
if (!tokensPerSecondDisplay) { |
|
|
stopTokensPerSecondInterval(); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
if (!currentActiveChat || currentActiveChat.id !== tokensPerSecondIntervalChatId) { |
|
|
|
|
|
const maxBadge = document.getElementById('max-tokens-per-second-badge'); |
|
|
if (maxBadge) { |
|
|
maxBadge.style.display = 'none'; |
|
|
} |
|
|
stopTokensPerSecondInterval(); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
const currentRequest = activeRequests.get(currentActiveChat.id); |
|
|
if (!currentRequest) { |
|
|
|
|
|
tokensPerSecondDisplay.textContent = '-'; |
|
|
const maxBadge = document.getElementById('max-tokens-per-second-badge'); |
|
|
if (maxBadge) { |
|
|
maxBadge.style.display = 'none'; |
|
|
} |
|
|
stopTokensPerSecondInterval(); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
if (!currentRequest.controller) { |
|
|
tokensPerSecondDisplay.textContent = '-'; |
|
|
if (currentRequest.maxTokensPerSecond > 0) { |
|
|
|
|
|
updateMaxTokensPerSecondBadge(currentActiveChat.id, currentRequest.maxTokensPerSecond); |
|
|
} else { |
|
|
|
|
|
const maxBadge = document.getElementById('max-tokens-per-second-badge'); |
|
|
if (maxBadge) { |
|
|
maxBadge.style.display = 'none'; |
|
|
} |
|
|
} |
|
|
stopTokensPerSecondInterval(); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
updateTokensPerSecond(currentActiveChat.id); |
|
|
}, 250); |
|
|
} |
|
|
|
|
|
|
|
|
function stopTokensPerSecondInterval() { |
|
|
if (tokensPerSecondInterval) { |
|
|
clearInterval(tokensPerSecondInterval); |
|
|
tokensPerSecondInterval = null; |
|
|
} |
|
|
tokensPerSecondIntervalChatId = null; |
|
|
const tokensPerSecondDisplay = document.getElementById('tokens-per-second'); |
|
|
if (tokensPerSecondDisplay) { |
|
|
tokensPerSecondDisplay.textContent = '-'; |
|
|
} |
|
|
|
|
|
lastTokensPerSecond = null; |
|
|
} |
|
|
|
|
|
function updateTokensPerSecond(chatId) { |
|
|
const tokensPerSecondDisplay = document.getElementById('tokens-per-second'); |
|
|
if (!tokensPerSecondDisplay || !chatId) { |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
const request = activeRequests.get(chatId); |
|
|
if (!request || !request.startTime) { |
|
|
tokensPerSecondDisplay.textContent = '-'; |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
if (!request.controller) { |
|
|
tokensPerSecondDisplay.textContent = '-'; |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
const chatStore = Alpine.store("chat"); |
|
|
const activeChat = chatStore ? chatStore.activeChat() : null; |
|
|
if (!activeChat || activeChat.id !== chatId) { |
|
|
|
|
|
tokensPerSecondDisplay.textContent = '-'; |
|
|
return; |
|
|
} |
|
|
|
|
|
const elapsedSeconds = (Date.now() - request.startTime) / 1000; |
|
|
|
|
|
if (elapsedSeconds > 0) { |
|
|
if (request.tokensReceived > 0) { |
|
|
const rate = request.tokensReceived / elapsedSeconds; |
|
|
|
|
|
if (rate > (request.maxTokensPerSecond || 0)) { |
|
|
request.maxTokensPerSecond = rate; |
|
|
} |
|
|
const formattedRate = `${rate.toFixed(1)} tokens/s`; |
|
|
tokensPerSecondDisplay.textContent = formattedRate; |
|
|
lastTokensPerSecond = formattedRate; |
|
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
tokensPerSecondDisplay.textContent = '0.0 tokens/s'; |
|
|
} |
|
|
} else { |
|
|
|
|
|
tokensPerSecondDisplay.textContent = '-'; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function updateMaxTokensPerSecondBadge(chatId, maxRate) { |
|
|
const maxBadge = document.getElementById('max-tokens-per-second-badge'); |
|
|
if (!maxBadge) return; |
|
|
|
|
|
|
|
|
const chatStore = Alpine.store("chat"); |
|
|
const activeChat = chatStore ? chatStore.activeChat() : null; |
|
|
if (!activeChat || activeChat.id !== chatId) { |
|
|
|
|
|
maxBadge.style.display = 'none'; |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
if (maxRate > 0) { |
|
|
maxBadge.textContent = `Peak: ${maxRate.toFixed(1)} tokens/s`; |
|
|
maxBadge.style.display = 'inline-flex'; |
|
|
} else { |
|
|
maxBadge.style.display = 'none'; |
|
|
} |
|
|
} |
|
|
|
|
|
function scrollThinkingBoxToBottom() { |
|
|
|
|
|
const thinkingBoxes = document.querySelectorAll('[data-thinking-box]'); |
|
|
thinkingBoxes.forEach(box => { |
|
|
|
|
|
if (box.offsetParent !== null && box.scrollHeight > box.clientHeight) { |
|
|
box.scrollTo({ |
|
|
top: box.scrollHeight, |
|
|
behavior: 'smooth' |
|
|
}); |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
window.scrollThinkingBoxToBottom = scrollThinkingBoxToBottom; |
|
|
|
|
|
function stopRequest() { |
|
|
|
|
|
const chatStore = Alpine.store("chat"); |
|
|
const activeChat = chatStore.activeChat(); |
|
|
if (!activeChat) return; |
|
|
|
|
|
const request = activeRequests.get(activeChat.id); |
|
|
const requestModel = request?.model || null; |
|
|
if (request) { |
|
|
if (request.controller) { |
|
|
request.controller.abort(); |
|
|
} |
|
|
if (request.reader) { |
|
|
request.reader.cancel(); |
|
|
} |
|
|
if (request.interval) { |
|
|
clearInterval(request.interval); |
|
|
} |
|
|
activeRequests.delete(activeChat.id); |
|
|
updateRequestTracking(activeChat.id, false); |
|
|
} |
|
|
|
|
|
|
|
|
if (currentAbortController) { |
|
|
currentAbortController.abort(); |
|
|
currentAbortController = null; |
|
|
} |
|
|
if (currentReader) { |
|
|
currentReader.cancel(); |
|
|
currentReader = null; |
|
|
} |
|
|
toggleLoader(false, activeChat.id); |
|
|
chatStore.add( |
|
|
"assistant", |
|
|
`<span class='error'>Request cancelled by user</span>`, |
|
|
null, |
|
|
null, |
|
|
activeChat.id, |
|
|
requestModel |
|
|
); |
|
|
} |
|
|
|
|
|
function processThinkingTags(content) { |
|
|
const thinkingRegex = /<thinking>(.*?)<\/thinking>|<think>(.*?)<\/think>/gs; |
|
|
const parts = content.split(thinkingRegex); |
|
|
|
|
|
let regularContent = ""; |
|
|
let thinkingContent = ""; |
|
|
|
|
|
for (let i = 0; i < parts.length; i++) { |
|
|
if (i % 3 === 0) { |
|
|
|
|
|
regularContent += parts[i]; |
|
|
} else if (i % 3 === 1) { |
|
|
|
|
|
thinkingContent = parts[i]; |
|
|
} else if (i % 3 === 2) { |
|
|
|
|
|
thinkingContent = parts[i]; |
|
|
} |
|
|
} |
|
|
|
|
|
return { |
|
|
regularContent: regularContent.trim(), |
|
|
thinkingContent: thinkingContent.trim() |
|
|
}; |
|
|
} |
|
|
|
|
|
function submitSystemPrompt(event) { |
|
|
event.preventDefault(); |
|
|
const chatStore = Alpine.store("chat"); |
|
|
const activeChat = chatStore.activeChat(); |
|
|
if (activeChat) { |
|
|
activeChat.systemPrompt = document.getElementById("systemPrompt").value; |
|
|
activeChat.updatedAt = Date.now(); |
|
|
autoSaveChats(); |
|
|
} |
|
|
document.getElementById("systemPrompt").blur(); |
|
|
} |
|
|
|
|
|
function handleShutdownResponse(event, modelName) { |
|
|
|
|
|
if (event.detail.successful) { |
|
|
|
|
|
console.log(`Model ${modelName} stopped successfully`); |
|
|
|
|
|
|
|
|
window.location.reload(); |
|
|
} else { |
|
|
|
|
|
console.error(`Failed to stop model ${modelName}`); |
|
|
|
|
|
|
|
|
|
|
|
window.location.reload(); |
|
|
} |
|
|
} |
|
|
|
|
|
var images = []; |
|
|
var audios = []; |
|
|
var fileContents = []; |
|
|
var currentFileNames = []; |
|
|
|
|
|
var imageFileMap = new Map(); |
|
|
var audioFileMap = new Map(); |
|
|
|
|
|
async function extractTextFromPDF(pdfData) { |
|
|
try { |
|
|
const pdf = await pdfjsLib.getDocument({ data: pdfData }).promise; |
|
|
let fullText = ''; |
|
|
|
|
|
for (let i = 1; i <= pdf.numPages; i++) { |
|
|
const page = await pdf.getPage(i); |
|
|
const textContent = await page.getTextContent(); |
|
|
const pageText = textContent.items.map(item => item.str).join(' '); |
|
|
fullText += pageText + '\n'; |
|
|
} |
|
|
|
|
|
return fullText; |
|
|
} catch (error) { |
|
|
console.error('Error extracting text from PDF:', error); |
|
|
throw error; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
window.handleFileSelection = function(event, fileType) { |
|
|
if (!event.target.files || !event.target.files.length) return; |
|
|
|
|
|
|
|
|
let inputContainer = event.target.closest('[x-data*="attachedFiles"]'); |
|
|
if (!inputContainer && window.Alpine) { |
|
|
|
|
|
inputContainer = document.querySelector('[x-data*="attachedFiles"]'); |
|
|
} |
|
|
if (!inputContainer || !window.Alpine) return; |
|
|
|
|
|
const alpineData = Alpine.$data(inputContainer); |
|
|
if (!alpineData || !alpineData.attachedFiles) return; |
|
|
|
|
|
Array.from(event.target.files).forEach(file => { |
|
|
|
|
|
const exists = alpineData.attachedFiles.some(f => f.name === file.name && f.type === fileType); |
|
|
if (!exists) { |
|
|
alpineData.attachedFiles.push({ name: file.name, type: fileType }); |
|
|
|
|
|
|
|
|
if (fileType === 'image') { |
|
|
readInputImageFile(file); |
|
|
} else if (fileType === 'audio') { |
|
|
readInputAudioFile(file); |
|
|
} else if (fileType === 'file') { |
|
|
readInputFileFile(file); |
|
|
} |
|
|
} |
|
|
}); |
|
|
}; |
|
|
|
|
|
|
|
|
window.removeFileFromInput = function(fileType, fileName) { |
|
|
|
|
|
if (fileType === 'image') { |
|
|
|
|
|
const dataURL = imageFileMap.get(fileName); |
|
|
if (dataURL) { |
|
|
const imageIndex = images.indexOf(dataURL); |
|
|
if (imageIndex !== -1) { |
|
|
images.splice(imageIndex, 1); |
|
|
} |
|
|
imageFileMap.delete(fileName); |
|
|
} |
|
|
} else if (fileType === 'audio') { |
|
|
|
|
|
const dataURL = audioFileMap.get(fileName); |
|
|
if (dataURL) { |
|
|
const audioIndex = audios.indexOf(dataURL); |
|
|
if (audioIndex !== -1) { |
|
|
audios.splice(audioIndex, 1); |
|
|
} |
|
|
audioFileMap.delete(fileName); |
|
|
} |
|
|
} else if (fileType === 'file') { |
|
|
|
|
|
const fileIndex = currentFileNames.indexOf(fileName); |
|
|
if (fileIndex !== -1) { |
|
|
currentFileNames.splice(fileIndex, 1); |
|
|
fileContents.splice(fileIndex, 1); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const inputId = fileType === 'image' ? 'input_image' : |
|
|
fileType === 'audio' ? 'input_audio' : 'input_file'; |
|
|
const input = document.getElementById(inputId); |
|
|
if (input && input.files) { |
|
|
const dt = new DataTransfer(); |
|
|
Array.from(input.files).forEach(file => { |
|
|
if (file.name !== fileName) { |
|
|
dt.items.add(file); |
|
|
} |
|
|
}); |
|
|
input.files = dt.files; |
|
|
} |
|
|
}; |
|
|
|
|
|
function readInputFile() { |
|
|
if (!this.files || !this.files.length) return; |
|
|
|
|
|
Array.from(this.files).forEach(file => { |
|
|
readInputFileFile(file); |
|
|
}); |
|
|
} |
|
|
|
|
|
function readInputFileFile(file) { |
|
|
const FR = new FileReader(); |
|
|
currentFileNames.push(file.name); |
|
|
const fileExtension = file.name.split('.').pop().toLowerCase(); |
|
|
|
|
|
FR.addEventListener("load", async function(evt) { |
|
|
if (fileExtension === 'pdf') { |
|
|
try { |
|
|
const content = await extractTextFromPDF(evt.target.result); |
|
|
fileContents.push({ name: file.name, content: content }); |
|
|
} catch (error) { |
|
|
console.error('Error processing PDF:', error); |
|
|
fileContents.push({ name: file.name, content: "Error processing PDF file" }); |
|
|
} |
|
|
} else { |
|
|
|
|
|
fileContents.push({ name: file.name, content: evt.target.result }); |
|
|
} |
|
|
}); |
|
|
|
|
|
if (fileExtension === 'pdf') { |
|
|
FR.readAsArrayBuffer(file); |
|
|
} else { |
|
|
FR.readAsText(file); |
|
|
} |
|
|
} |
|
|
|
|
|
function submitPrompt(event) { |
|
|
event.preventDefault(); |
|
|
|
|
|
const input = document.getElementById("input"); |
|
|
if (!input) return; |
|
|
|
|
|
const inputValue = input.value; |
|
|
if (!inputValue.trim()) return; |
|
|
|
|
|
|
|
|
const chatStore = Alpine.store("chat"); |
|
|
const activeChat = chatStore.activeChat(); |
|
|
if (activeChat) { |
|
|
const activeRequest = activeRequests.get(activeChat.id); |
|
|
if (activeRequest && (activeRequest.controller || activeRequest.reader)) { |
|
|
|
|
|
stopRequest(); |
|
|
|
|
|
setTimeout(() => { |
|
|
|
|
|
processAndSendMessage(inputValue); |
|
|
}, 100); |
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
processAndSendMessage(inputValue); |
|
|
} |
|
|
|
|
|
function processAndSendMessage(inputValue) { |
|
|
let fullInput = inputValue; |
|
|
|
|
|
|
|
|
if (fileContents.length > 0) { |
|
|
fullInput += "\n\nFile contents:\n"; |
|
|
fileContents.forEach(file => { |
|
|
fullInput += `\n--- ${file.name} ---\n${file.content}\n`; |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
let displayContent = inputValue; |
|
|
if (currentFileNames.length > 0) { |
|
|
displayContent += "\n\n"; |
|
|
currentFileNames.forEach(fileName => { |
|
|
displayContent += `<i class="fa-solid fa-file"></i> Attached file: ${fileName}\n`; |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
Alpine.store("chat").add("user", displayContent, images, audios); |
|
|
|
|
|
|
|
|
const chatStore = Alpine.store("chat"); |
|
|
const activeChat = chatStore.activeChat(); |
|
|
if (activeChat && activeChat.history.length > 0) { |
|
|
activeChat.history[activeChat.history.length - 1].content = fullInput; |
|
|
activeChat.updatedAt = Date.now(); |
|
|
} |
|
|
|
|
|
const input = document.getElementById("input"); |
|
|
if (input) input.value = ""; |
|
|
const systemPrompt = activeChat?.systemPrompt || ""; |
|
|
Alpine.nextTick(() => { |
|
|
const chatContainer = document.getElementById('chat'); |
|
|
if (chatContainer) { |
|
|
chatContainer.scrollTo({ |
|
|
top: chatContainer.scrollHeight, |
|
|
behavior: 'smooth' |
|
|
}); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
requestStartTime = Date.now(); |
|
|
tokensReceived = 0; |
|
|
|
|
|
promptGPT(systemPrompt, fullInput); |
|
|
|
|
|
|
|
|
fileContents = []; |
|
|
currentFileNames = []; |
|
|
images = []; |
|
|
audios = []; |
|
|
imageFileMap.clear(); |
|
|
audioFileMap.clear(); |
|
|
|
|
|
|
|
|
const inputContainer = document.querySelector('[x-data*="attachedFiles"]'); |
|
|
if (inputContainer && window.Alpine) { |
|
|
const alpineData = Alpine.$data(inputContainer); |
|
|
if (alpineData && alpineData.attachedFiles) { |
|
|
alpineData.attachedFiles = []; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
document.getElementById("input_image").value = null; |
|
|
document.getElementById("input_audio").value = null; |
|
|
document.getElementById("input_file").value = null; |
|
|
} |
|
|
|
|
|
function readInputImage() { |
|
|
if (!this.files || !this.files.length) return; |
|
|
|
|
|
Array.from(this.files).forEach(file => { |
|
|
readInputImageFile(file); |
|
|
}); |
|
|
} |
|
|
|
|
|
function readInputImageFile(file) { |
|
|
const FR = new FileReader(); |
|
|
|
|
|
FR.addEventListener("load", function(evt) { |
|
|
const dataURL = evt.target.result; |
|
|
images.push(dataURL); |
|
|
imageFileMap.set(file.name, dataURL); |
|
|
}); |
|
|
|
|
|
FR.readAsDataURL(file); |
|
|
} |
|
|
|
|
|
function readInputAudio() { |
|
|
if (!this.files || !this.files.length) return; |
|
|
|
|
|
Array.from(this.files).forEach(file => { |
|
|
readInputAudioFile(file); |
|
|
}); |
|
|
} |
|
|
|
|
|
function readInputAudioFile(file) { |
|
|
const FR = new FileReader(); |
|
|
|
|
|
FR.addEventListener("load", function(evt) { |
|
|
const dataURL = evt.target.result; |
|
|
audios.push(dataURL); |
|
|
audioFileMap.set(file.name, dataURL); |
|
|
}); |
|
|
|
|
|
FR.readAsDataURL(file); |
|
|
} |
|
|
|
|
|
async function promptGPT(systemPrompt, input) { |
|
|
const chatStore = Alpine.store("chat"); |
|
|
const activeChat = chatStore.activeChat(); |
|
|
if (!activeChat) { |
|
|
console.error('No active chat'); |
|
|
return; |
|
|
} |
|
|
|
|
|
const model = activeChat.model || document.getElementById("chat-model").value; |
|
|
const mcpMode = activeChat.mcpMode || false; |
|
|
|
|
|
|
|
|
if (activeChat.tokenUsage) { |
|
|
activeChat.tokenUsage.currentRequest = null; |
|
|
} |
|
|
|
|
|
|
|
|
const chatId = activeChat.id; |
|
|
|
|
|
toggleLoader(true, chatId); |
|
|
|
|
|
messages = chatStore.messages(); |
|
|
|
|
|
|
|
|
if (systemPrompt) { |
|
|
messages.unshift({ |
|
|
role: "system", |
|
|
content: systemPrompt |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
messages.forEach((message) => { |
|
|
if ((message.image && message.image.length > 0) || (message.audio && message.audio.length > 0)) { |
|
|
|
|
|
message.content = [ |
|
|
{ |
|
|
"type": "text", |
|
|
"text": message.content |
|
|
} |
|
|
] |
|
|
|
|
|
if (message.image && message.image.length > 0) { |
|
|
message.image.forEach(img => { |
|
|
message.content.push( |
|
|
{ |
|
|
"type": "image_url", |
|
|
"image_url": { |
|
|
"url": img, |
|
|
} |
|
|
} |
|
|
); |
|
|
}); |
|
|
delete message.image; |
|
|
} |
|
|
|
|
|
if (message.audio && message.audio.length > 0) { |
|
|
message.audio.forEach(aud => { |
|
|
message.content.push( |
|
|
{ |
|
|
"type": "audio_url", |
|
|
"audio_url": { |
|
|
"url": aud, |
|
|
} |
|
|
} |
|
|
); |
|
|
}); |
|
|
delete message.audio; |
|
|
} |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const endpoint = mcpMode ? "v1/mcp/chat/completions" : "v1/chat/completions"; |
|
|
const requestBody = { |
|
|
model: model, |
|
|
messages: messages, |
|
|
}; |
|
|
|
|
|
|
|
|
requestBody.stream = true; |
|
|
|
|
|
|
|
|
if (activeChat.temperature !== null && activeChat.temperature !== undefined) { |
|
|
requestBody.temperature = activeChat.temperature; |
|
|
} |
|
|
if (activeChat.topP !== null && activeChat.topP !== undefined) { |
|
|
requestBody.top_p = activeChat.topP; |
|
|
} |
|
|
if (activeChat.topK !== null && activeChat.topK !== undefined) { |
|
|
requestBody.top_k = activeChat.topK; |
|
|
} |
|
|
|
|
|
let response; |
|
|
try { |
|
|
|
|
|
const controller = new AbortController(); |
|
|
|
|
|
const requestStartTime = Date.now(); |
|
|
activeRequests.set(chatId, { |
|
|
controller: controller, |
|
|
reader: null, |
|
|
startTime: requestStartTime, |
|
|
tokensReceived: 0, |
|
|
interval: null, |
|
|
maxTokensPerSecond: 0, |
|
|
model: model |
|
|
}); |
|
|
|
|
|
|
|
|
updateRequestTracking(chatId, true); |
|
|
|
|
|
currentAbortController = controller; |
|
|
|
|
|
|
|
|
|
|
|
startTokensPerSecondInterval(); |
|
|
setTimeout(() => { |
|
|
|
|
|
if (!tokensPerSecondInterval) { |
|
|
startTokensPerSecondInterval(); |
|
|
} |
|
|
}, 200); |
|
|
const timeoutId = setTimeout(() => controller.abort(), mcpMode ? 300000 : 30000); |
|
|
|
|
|
response = await fetch(endpoint, { |
|
|
method: "POST", |
|
|
headers: { |
|
|
"Content-Type": "application/json", |
|
|
"Accept": "application/json", |
|
|
}, |
|
|
body: JSON.stringify(requestBody), |
|
|
signal: controller.signal |
|
|
}); |
|
|
|
|
|
clearTimeout(timeoutId); |
|
|
} catch (error) { |
|
|
|
|
|
if (error.name === 'AbortError') { |
|
|
|
|
|
|
|
|
if (!currentAbortController) { |
|
|
|
|
|
return; |
|
|
} else { |
|
|
|
|
|
const request = activeRequests.get(chatId); |
|
|
const requestModel = request?.model || null; |
|
|
chatStore.add( |
|
|
"assistant", |
|
|
`<span class='error'>Request timeout: MCP processing is taking longer than expected. Please try again.</span>`, |
|
|
null, |
|
|
null, |
|
|
chatId, |
|
|
requestModel |
|
|
); |
|
|
} |
|
|
} else { |
|
|
const request = activeRequests.get(chatId); |
|
|
const requestModel = request?.model || null; |
|
|
chatStore.add( |
|
|
"assistant", |
|
|
`<span class='error'>Network Error: ${error.message}</span>`, |
|
|
null, |
|
|
null, |
|
|
chatId, |
|
|
requestModel |
|
|
); |
|
|
} |
|
|
toggleLoader(false, chatId); |
|
|
activeRequests.delete(chatId); |
|
|
updateRequestTracking(chatId, false); |
|
|
const activeChat = chatStore.activeChat(); |
|
|
if (activeChat && activeChat.id === chatId) { |
|
|
currentAbortController = null; |
|
|
} |
|
|
return; |
|
|
} |
|
|
|
|
|
if (!response.ok) { |
|
|
const request = activeRequests.get(chatId); |
|
|
const requestModel = request?.model || null; |
|
|
chatStore.add( |
|
|
"assistant", |
|
|
`<span class='error'>Error: POST ${endpoint} ${response.status}</span>`, |
|
|
null, |
|
|
null, |
|
|
chatId, |
|
|
requestModel |
|
|
); |
|
|
toggleLoader(false, chatId); |
|
|
activeRequests.delete(chatId); |
|
|
updateRequestTracking(chatId, false); |
|
|
const activeChat = chatStore.activeChat(); |
|
|
if (activeChat && activeChat.id === chatId) { |
|
|
currentAbortController = null; |
|
|
} |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
if (mcpMode) { |
|
|
|
|
|
const reader = response.body |
|
|
?.pipeThrough(new TextDecoderStream()) |
|
|
.getReader(); |
|
|
|
|
|
if (!reader) { |
|
|
const request = activeRequests.get(chatId); |
|
|
const requestModel = request?.model || null; |
|
|
chatStore.add( |
|
|
"assistant", |
|
|
`<span class='error'>Error: Failed to decode MCP API response</span>`, |
|
|
null, |
|
|
null, |
|
|
chatId, |
|
|
requestModel |
|
|
); |
|
|
toggleLoader(false, chatId); |
|
|
activeRequests.delete(chatId); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
const mcpRequest = activeRequests.get(chatId); |
|
|
if (mcpRequest) { |
|
|
mcpRequest.reader = reader; |
|
|
|
|
|
updateRequestTracking(chatId, true); |
|
|
} |
|
|
currentReader = reader; |
|
|
|
|
|
let buffer = ""; |
|
|
let assistantContent = ""; |
|
|
let assistantContentBuffer = []; |
|
|
let thinkingContent = ""; |
|
|
let isThinking = false; |
|
|
let lastAssistantMessageIndex = -1; |
|
|
let lastThinkingMessageIndex = -1; |
|
|
let lastThinkingScrollTime = 0; |
|
|
let hasReasoningFromAPI = false; |
|
|
const THINKING_SCROLL_THROTTLE = 200; |
|
|
|
|
|
try { |
|
|
while (true) { |
|
|
const { value, done } = await reader.read(); |
|
|
if (done) break; |
|
|
|
|
|
|
|
|
const currentChat = chatStore.getChat(chatId); |
|
|
if (!currentChat) { |
|
|
|
|
|
break; |
|
|
} |
|
|
const targetHistory = currentChat.history; |
|
|
|
|
|
buffer += value; |
|
|
|
|
|
let lines = buffer.split("\n"); |
|
|
buffer = lines.pop(); |
|
|
|
|
|
lines.forEach((line) => { |
|
|
if (line.length === 0 || line.startsWith(":")) return; |
|
|
if (line === "data: [DONE]") { |
|
|
return; |
|
|
} |
|
|
|
|
|
if (line.startsWith("data: ")) { |
|
|
try { |
|
|
const eventData = JSON.parse(line.substring(6)); |
|
|
|
|
|
|
|
|
switch (eventData.type) { |
|
|
case "reasoning": |
|
|
hasReasoningFromAPI = true; |
|
|
if (eventData.content) { |
|
|
const currentChat = chatStore.getChat(chatId); |
|
|
if (!currentChat) break; |
|
|
const isMCPMode = currentChat.mcpMode || false; |
|
|
const shouldExpand = !isMCPMode; |
|
|
|
|
|
if (lastAssistantMessageIndex >= 0 && targetHistory[lastAssistantMessageIndex]?.role === "assistant") { |
|
|
targetHistory.splice(lastAssistantMessageIndex, 0, { |
|
|
role: "thinking", |
|
|
content: eventData.content, |
|
|
html: DOMPurify.sanitize(marked.parse(eventData.content)), |
|
|
image: [], |
|
|
audio: [], |
|
|
expanded: shouldExpand |
|
|
}); |
|
|
lastAssistantMessageIndex++; |
|
|
|
|
|
setTimeout(() => { |
|
|
const chatContainer = document.getElementById('chat'); |
|
|
if (chatContainer) { |
|
|
chatContainer.scrollTo({ |
|
|
top: chatContainer.scrollHeight, |
|
|
behavior: 'smooth' |
|
|
}); |
|
|
} |
|
|
}, 100); |
|
|
} else { |
|
|
|
|
|
chatStore.add("thinking", eventData.content, null, null, chatId); |
|
|
} |
|
|
} |
|
|
break; |
|
|
|
|
|
case "tool_call": |
|
|
if (eventData.name) { |
|
|
|
|
|
const toolCallData = { |
|
|
name: eventData.name, |
|
|
arguments: eventData.arguments || {}, |
|
|
reasoning: eventData.reasoning || "" |
|
|
}; |
|
|
chatStore.add("tool_call", JSON.stringify(toolCallData, null, 2), null, null, chatId); |
|
|
|
|
|
setTimeout(() => { |
|
|
const chatContainer = document.getElementById('chat'); |
|
|
if (chatContainer) { |
|
|
chatContainer.scrollTo({ |
|
|
top: chatContainer.scrollHeight, |
|
|
behavior: 'smooth' |
|
|
}); |
|
|
} |
|
|
}, 100); |
|
|
} |
|
|
break; |
|
|
|
|
|
case "tool_result": |
|
|
if (eventData.name) { |
|
|
|
|
|
const toolResultData = { |
|
|
name: eventData.name, |
|
|
result: eventData.result || "" |
|
|
}; |
|
|
chatStore.add("tool_result", JSON.stringify(toolResultData, null, 2), null, null, chatId); |
|
|
|
|
|
setTimeout(() => { |
|
|
const chatContainer = document.getElementById('chat'); |
|
|
if (chatContainer) { |
|
|
chatContainer.scrollTo({ |
|
|
top: chatContainer.scrollHeight, |
|
|
behavior: 'smooth' |
|
|
}); |
|
|
} |
|
|
}, 100); |
|
|
} |
|
|
break; |
|
|
|
|
|
case "status": |
|
|
|
|
|
console.log("[MCP Status]", eventData.message); |
|
|
break; |
|
|
|
|
|
case "assistant": |
|
|
if (eventData.content) { |
|
|
assistantContent += eventData.content; |
|
|
const contentChunk = eventData.content; |
|
|
|
|
|
|
|
|
const request = activeRequests.get(chatId); |
|
|
if (request) { |
|
|
request.tokensReceived += Math.ceil(contentChunk.length / 4); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!hasReasoningFromAPI) { |
|
|
|
|
|
if (contentChunk.includes("<thinking>") || contentChunk.includes("<think>")) { |
|
|
isThinking = true; |
|
|
thinkingContent = ""; |
|
|
lastThinkingMessageIndex = -1; |
|
|
} |
|
|
|
|
|
if (contentChunk.includes("</thinking>") || contentChunk.includes("</think>")) { |
|
|
isThinking = false; |
|
|
|
|
|
if (thinkingContent.trim()) { |
|
|
|
|
|
const thinkingMatch = thinkingContent.match(/<(?:thinking|redacted_reasoning)>(.*?)<\/(?:thinking|redacted_reasoning)>/s); |
|
|
if (thinkingMatch && thinkingMatch[1]) { |
|
|
const extractedThinking = thinkingMatch[1]; |
|
|
const currentChat = chatStore.getChat(chatId); |
|
|
if (!currentChat) break; |
|
|
const isMCPMode = currentChat.mcpMode || false; |
|
|
const shouldExpand = !isMCPMode; |
|
|
if (lastThinkingMessageIndex === -1) { |
|
|
|
|
|
if (lastAssistantMessageIndex >= 0 && targetHistory[lastAssistantMessageIndex]?.role === "assistant") { |
|
|
|
|
|
targetHistory.splice(lastAssistantMessageIndex, 0, { |
|
|
role: "thinking", |
|
|
content: extractedThinking, |
|
|
html: DOMPurify.sanitize(marked.parse(extractedThinking)), |
|
|
image: [], |
|
|
audio: [], |
|
|
expanded: shouldExpand |
|
|
}); |
|
|
lastThinkingMessageIndex = lastAssistantMessageIndex; |
|
|
lastAssistantMessageIndex++; |
|
|
} else { |
|
|
|
|
|
chatStore.add("thinking", extractedThinking, null, null, chatId); |
|
|
lastThinkingMessageIndex = targetHistory.length - 1; |
|
|
} |
|
|
} else { |
|
|
|
|
|
const lastMessage = targetHistory[lastThinkingMessageIndex]; |
|
|
if (lastMessage && lastMessage.role === "thinking") { |
|
|
lastMessage.content = extractedThinking; |
|
|
lastMessage.html = DOMPurify.sanitize(marked.parse(extractedThinking)); |
|
|
} |
|
|
} |
|
|
|
|
|
if (!isMCPMode) { |
|
|
setTimeout(() => { |
|
|
const chatContainer = document.getElementById('chat'); |
|
|
if (chatContainer) { |
|
|
chatContainer.scrollTo({ |
|
|
top: chatContainer.scrollHeight, |
|
|
behavior: 'smooth' |
|
|
}); |
|
|
} |
|
|
}, 50); |
|
|
} |
|
|
} |
|
|
thinkingContent = ""; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (!hasReasoningFromAPI && isThinking) { |
|
|
thinkingContent += contentChunk; |
|
|
const currentChat = chatStore.getChat(chatId); |
|
|
if (!currentChat) break; |
|
|
const isMCPMode = currentChat.mcpMode || false; |
|
|
const shouldExpand = !isMCPMode; |
|
|
|
|
|
if (lastThinkingMessageIndex === -1) { |
|
|
|
|
|
if (lastAssistantMessageIndex >= 0 && targetHistory[lastAssistantMessageIndex]?.role === "assistant") { |
|
|
|
|
|
targetHistory.splice(lastAssistantMessageIndex, 0, { |
|
|
role: "thinking", |
|
|
content: thinkingContent, |
|
|
html: DOMPurify.sanitize(marked.parse(thinkingContent)), |
|
|
image: [], |
|
|
audio: [], |
|
|
expanded: shouldExpand |
|
|
}); |
|
|
lastThinkingMessageIndex = lastAssistantMessageIndex; |
|
|
lastAssistantMessageIndex++; |
|
|
} else { |
|
|
|
|
|
chatStore.add("thinking", thinkingContent, null, null, chatId); |
|
|
lastThinkingMessageIndex = targetHistory.length - 1; |
|
|
} |
|
|
} else { |
|
|
|
|
|
const lastMessage = targetHistory[lastThinkingMessageIndex]; |
|
|
if (lastMessage && lastMessage.role === "thinking") { |
|
|
lastMessage.content = thinkingContent; |
|
|
lastMessage.html = DOMPurify.sanitize(marked.parse(thinkingContent)); |
|
|
} |
|
|
} |
|
|
|
|
|
if (!isMCPMode) { |
|
|
const now = Date.now(); |
|
|
if (now - lastThinkingScrollTime > THINKING_SCROLL_THROTTLE) { |
|
|
lastThinkingScrollTime = now; |
|
|
setTimeout(() => { |
|
|
const chatContainer = document.getElementById('chat'); |
|
|
if (chatContainer) { |
|
|
chatContainer.scrollTo({ |
|
|
top: chatContainer.scrollHeight, |
|
|
behavior: 'smooth' |
|
|
}); |
|
|
} |
|
|
}, 100); |
|
|
} |
|
|
} |
|
|
} else { |
|
|
|
|
|
assistantContentBuffer.push(contentChunk); |
|
|
} |
|
|
} |
|
|
break; |
|
|
|
|
|
case "error": |
|
|
const request = activeRequests.get(chatId); |
|
|
const requestModel = request?.model || null; |
|
|
chatStore.add( |
|
|
"assistant", |
|
|
`<span class='error'>MCP Error: ${eventData.message}</span>`, |
|
|
null, |
|
|
null, |
|
|
chatId, |
|
|
requestModel |
|
|
); |
|
|
break; |
|
|
} |
|
|
} catch (error) { |
|
|
console.error("Failed to parse MCP event:", line, error); |
|
|
} |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
if (assistantContentBuffer.length > 0) { |
|
|
const regularContent = assistantContentBuffer.join(""); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const { regularContent: processedRegular, thinkingContent: processedThinking } = hasReasoningFromAPI |
|
|
? { regularContent: regularContent, thinkingContent: "" } |
|
|
: processThinkingTags(regularContent); |
|
|
|
|
|
|
|
|
const currentChat = chatStore.getChat(chatId); |
|
|
if (!currentChat) break; |
|
|
const request = activeRequests.get(chatId); |
|
|
const requestModel = request?.model || null; |
|
|
if (lastAssistantMessageIndex === -1) { |
|
|
|
|
|
|
|
|
chatStore.add("assistant", processedRegular || "", null, null, chatId, requestModel); |
|
|
lastAssistantMessageIndex = targetHistory.length - 1; |
|
|
} else { |
|
|
const lastMessage = targetHistory[lastAssistantMessageIndex]; |
|
|
if (lastMessage && lastMessage.role === "assistant") { |
|
|
lastMessage.content = (lastMessage.content || "") + (processedRegular || ""); |
|
|
lastMessage.html = DOMPurify.sanitize(marked.parse(lastMessage.content)); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (processedThinking && processedThinking.trim()) { |
|
|
const isMCPMode = currentChat.mcpMode || false; |
|
|
const shouldExpand = !isMCPMode; |
|
|
|
|
|
if (lastAssistantMessageIndex >= 0 && targetHistory[lastAssistantMessageIndex]?.role === "assistant") { |
|
|
targetHistory.splice(lastAssistantMessageIndex, 0, { |
|
|
role: "thinking", |
|
|
content: processedThinking, |
|
|
html: DOMPurify.sanitize(marked.parse(processedThinking)), |
|
|
image: [], |
|
|
audio: [], |
|
|
expanded: shouldExpand |
|
|
}); |
|
|
lastAssistantMessageIndex++; |
|
|
} else { |
|
|
|
|
|
chatStore.add("thinking", processedThinking, null, null, chatId); |
|
|
} |
|
|
} |
|
|
|
|
|
assistantContentBuffer = []; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (assistantContentBuffer.length > 0) { |
|
|
const regularContent = assistantContentBuffer.join(""); |
|
|
|
|
|
|
|
|
const { regularContent: processedRegular, thinkingContent: processedThinking } = hasReasoningFromAPI |
|
|
? { regularContent: regularContent, thinkingContent: "" } |
|
|
: processThinkingTags(regularContent); |
|
|
|
|
|
const currentChat = chatStore.getChat(chatId); |
|
|
if (!currentChat) { |
|
|
|
|
|
activeRequests.delete(chatId); |
|
|
updateRequestTracking(chatId, false); |
|
|
return; |
|
|
} |
|
|
const targetHistory = currentChat.history; |
|
|
|
|
|
|
|
|
if (processedThinking && processedThinking.trim()) { |
|
|
const isMCPMode = currentChat.mcpMode || false; |
|
|
const shouldExpand = !isMCPMode; |
|
|
|
|
|
if (lastAssistantMessageIndex >= 0 && targetHistory[lastAssistantMessageIndex]?.role === "assistant") { |
|
|
targetHistory.splice(lastAssistantMessageIndex, 0, { |
|
|
role: "thinking", |
|
|
content: processedThinking, |
|
|
html: DOMPurify.sanitize(marked.parse(processedThinking)), |
|
|
image: [], |
|
|
audio: [], |
|
|
expanded: shouldExpand |
|
|
}); |
|
|
lastAssistantMessageIndex++; |
|
|
} else { |
|
|
|
|
|
chatStore.add("thinking", processedThinking, null, null, chatId); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (lastAssistantMessageIndex !== -1) { |
|
|
const lastMessage = targetHistory[lastAssistantMessageIndex]; |
|
|
if (lastMessage && lastMessage.role === "assistant") { |
|
|
lastMessage.content = (lastMessage.content || "") + (processedRegular || ""); |
|
|
lastMessage.html = DOMPurify.sanitize(marked.parse(lastMessage.content)); |
|
|
} |
|
|
} else { |
|
|
|
|
|
const request = activeRequests.get(chatId); |
|
|
const requestModel = request?.model || null; |
|
|
chatStore.add("assistant", processedRegular || "", null, null, chatId, requestModel); |
|
|
lastAssistantMessageIndex = targetHistory.length - 1; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const finalChat = chatStore.getChat(chatId); |
|
|
if (finalChat && !hasReasoningFromAPI && thinkingContent.trim() && lastThinkingMessageIndex === -1) { |
|
|
const finalHistory = finalChat.history; |
|
|
|
|
|
const thinkingMatch = thinkingContent.match(/<(?:thinking|redacted_reasoning)>(.*?)<\/(?:thinking|redacted_reasoning)>/s); |
|
|
if (thinkingMatch && thinkingMatch[1]) { |
|
|
const isMCPMode = finalChat.mcpMode || false; |
|
|
const shouldExpand = !isMCPMode; |
|
|
|
|
|
if (lastAssistantMessageIndex >= 0 && finalHistory[lastAssistantMessageIndex]?.role === "assistant") { |
|
|
finalHistory.splice(lastAssistantMessageIndex, 0, { |
|
|
role: "thinking", |
|
|
content: thinkingMatch[1], |
|
|
html: DOMPurify.sanitize(marked.parse(thinkingMatch[1])), |
|
|
image: [], |
|
|
audio: [], |
|
|
expanded: shouldExpand |
|
|
}); |
|
|
} else { |
|
|
|
|
|
chatStore.add("thinking", thinkingMatch[1], null, null, chatId); |
|
|
} |
|
|
} else { |
|
|
chatStore.add("thinking", thinkingContent, null, null, chatId); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (finalChat && assistantContent.trim()) { |
|
|
const finalHistory = finalChat.history; |
|
|
const { regularContent: finalRegular, thinkingContent: finalThinking } = processThinkingTags(assistantContent); |
|
|
|
|
|
|
|
|
if (finalRegular && finalRegular.trim()) { |
|
|
if (lastAssistantMessageIndex !== -1) { |
|
|
const lastMessage = finalHistory[lastAssistantMessageIndex]; |
|
|
if (lastMessage && lastMessage.role === "assistant") { |
|
|
lastMessage.content = finalRegular; |
|
|
lastMessage.html = DOMPurify.sanitize(marked.parse(lastMessage.content)); |
|
|
} |
|
|
} else { |
|
|
const request = activeRequests.get(chatId); |
|
|
const requestModel = request?.model || null; |
|
|
chatStore.add("assistant", finalRegular, null, null, chatId, requestModel); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (finalThinking && finalThinking.trim()) { |
|
|
const hasThinking = finalHistory.some(msg => |
|
|
msg.role === "thinking" && msg.content.trim() === finalThinking.trim() |
|
|
); |
|
|
if (!hasThinking) { |
|
|
chatStore.add("thinking", finalThinking, null, null, chatId); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
activeRequests.delete(chatId); |
|
|
updateRequestTracking(chatId, false); |
|
|
|
|
|
|
|
|
hljs.highlightAll(); |
|
|
} catch (error) { |
|
|
|
|
|
if (error.name !== 'AbortError' || !currentAbortController) { |
|
|
const errorChat = chatStore.getChat(chatId); |
|
|
if (errorChat) { |
|
|
chatStore.add( |
|
|
"assistant", |
|
|
`<span class='error'>Error: Failed to process MCP stream</span>`, |
|
|
null, |
|
|
null, |
|
|
chatId |
|
|
); |
|
|
} |
|
|
} |
|
|
} finally { |
|
|
|
|
|
if (reader) { |
|
|
reader.releaseLock(); |
|
|
} |
|
|
|
|
|
const activeChat = chatStore.activeChat(); |
|
|
if (activeChat && activeChat.id === chatId) { |
|
|
currentReader = null; |
|
|
currentAbortController = null; |
|
|
toggleLoader(false, chatId); |
|
|
} |
|
|
|
|
|
activeRequests.delete(chatId); |
|
|
updateRequestTracking(chatId, false); |
|
|
} |
|
|
} else { |
|
|
|
|
|
const reader = response.body |
|
|
?.pipeThrough(new TextDecoderStream()) |
|
|
.getReader(); |
|
|
|
|
|
if (!reader) { |
|
|
const request = activeRequests.get(chatId); |
|
|
const requestModel = request?.model || null; |
|
|
chatStore.add( |
|
|
"assistant", |
|
|
`<span class='error'>Error: Failed to decode API response</span>`, |
|
|
null, |
|
|
null, |
|
|
chatId, |
|
|
requestModel |
|
|
); |
|
|
toggleLoader(false, chatId); |
|
|
activeRequests.delete(chatId); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
const request = activeRequests.get(chatId); |
|
|
if (request) { |
|
|
request.reader = reader; |
|
|
|
|
|
updateRequestTracking(chatId, true); |
|
|
|
|
|
startTokensPerSecondInterval(); |
|
|
} |
|
|
currentReader = reader; |
|
|
|
|
|
|
|
|
let targetChat = chatStore.getChat(chatId); |
|
|
if (!targetChat) { |
|
|
|
|
|
activeRequests.delete(chatId); |
|
|
updateRequestTracking(chatId, false); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
const addToChat = (token) => { |
|
|
const currentChat = chatStore.getChat(chatId); |
|
|
if (!currentChat) return; |
|
|
|
|
|
const request = activeRequests.get(chatId); |
|
|
const requestModel = request?.model || null; |
|
|
chatStore.add("assistant", token, null, null, chatId, requestModel); |
|
|
|
|
|
if (request) { |
|
|
const tokenCount = Math.ceil(token.length / 4); |
|
|
request.tokensReceived += tokenCount; |
|
|
} |
|
|
|
|
|
|
|
|
}; |
|
|
|
|
|
let buffer = ""; |
|
|
let contentBuffer = []; |
|
|
let thinkingContent = ""; |
|
|
let reasoningContent = ""; |
|
|
let isThinking = false; |
|
|
let lastThinkingMessageIndex = -1; |
|
|
let lastReasoningMessageIndex = -1; |
|
|
let lastAssistantMessageIndex = -1; |
|
|
let lastThinkingScrollTime = 0; |
|
|
let hasReasoningFromAPI = false; |
|
|
const THINKING_SCROLL_THROTTLE = 200; |
|
|
|
|
|
try { |
|
|
while (true) { |
|
|
const { value, done } = await reader.read(); |
|
|
if (done) break; |
|
|
|
|
|
|
|
|
targetChat = chatStore.getChat(chatId); |
|
|
if (!targetChat) { |
|
|
|
|
|
break; |
|
|
} |
|
|
const targetHistory = targetChat.history; |
|
|
|
|
|
buffer += value; |
|
|
|
|
|
let lines = buffer.split("\n"); |
|
|
buffer = lines.pop(); |
|
|
|
|
|
lines.forEach((line) => { |
|
|
if (line.length === 0 || line.startsWith(":")) return; |
|
|
if (line === "data: [DONE]") { |
|
|
return; |
|
|
} |
|
|
|
|
|
if (line.startsWith("data: ")) { |
|
|
try { |
|
|
const jsonData = JSON.parse(line.substring(6)); |
|
|
|
|
|
|
|
|
if (jsonData.usage) { |
|
|
chatStore.updateTokenUsage(jsonData.usage, chatId); |
|
|
} |
|
|
|
|
|
const token = jsonData.choices?.[0]?.delta?.content; |
|
|
const reasoningDelta = jsonData.choices?.[0]?.delta?.reasoning; |
|
|
|
|
|
|
|
|
if (reasoningDelta && reasoningDelta.trim() !== "") { |
|
|
hasReasoningFromAPI = true; |
|
|
reasoningContent += reasoningDelta; |
|
|
const currentChat = chatStore.getChat(chatId); |
|
|
if (!currentChat) { |
|
|
|
|
|
return; |
|
|
} |
|
|
const isMCPMode = currentChat.mcpMode || false; |
|
|
const shouldExpand = !isMCPMode; |
|
|
|
|
|
|
|
|
if (reasoningContent.trim() !== "") { |
|
|
|
|
|
if (lastReasoningMessageIndex === -1) { |
|
|
|
|
|
const targetHistory = currentChat.history; |
|
|
const assistantIndex = targetHistory.length - 1; |
|
|
if (assistantIndex >= 0 && targetHistory[assistantIndex]?.role === "assistant") { |
|
|
|
|
|
targetHistory.splice(assistantIndex, 0, { |
|
|
role: "thinking", |
|
|
content: reasoningContent, |
|
|
html: DOMPurify.sanitize(marked.parse(reasoningContent)), |
|
|
image: [], |
|
|
audio: [], |
|
|
expanded: shouldExpand |
|
|
}); |
|
|
lastReasoningMessageIndex = assistantIndex; |
|
|
lastAssistantMessageIndex = assistantIndex + 1; |
|
|
} else { |
|
|
|
|
|
chatStore.add("thinking", reasoningContent, null, null, chatId); |
|
|
lastReasoningMessageIndex = currentChat.history.length - 1; |
|
|
} |
|
|
} else { |
|
|
|
|
|
const targetHistory = currentChat.history; |
|
|
if (lastReasoningMessageIndex >= 0 && lastReasoningMessageIndex < targetHistory.length) { |
|
|
const thinkingMessage = targetHistory[lastReasoningMessageIndex]; |
|
|
if (thinkingMessage && thinkingMessage.role === "thinking") { |
|
|
thinkingMessage.content = reasoningContent; |
|
|
thinkingMessage.html = DOMPurify.sanitize(marked.parse(reasoningContent)); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const now = Date.now(); |
|
|
if (now - lastThinkingScrollTime > THINKING_SCROLL_THROTTLE) { |
|
|
lastThinkingScrollTime = now; |
|
|
setTimeout(() => { |
|
|
const chatContainer = document.getElementById('chat'); |
|
|
if (chatContainer) { |
|
|
chatContainer.scrollTo({ |
|
|
top: chatContainer.scrollHeight, |
|
|
behavior: 'smooth' |
|
|
}); |
|
|
} |
|
|
scrollThinkingBoxToBottom(); |
|
|
}, 100); |
|
|
} |
|
|
} |
|
|
|
|
|
if (token && token.trim() !== "") { |
|
|
|
|
|
|
|
|
if (!hasReasoningFromAPI) { |
|
|
|
|
|
if (token.includes("<thinking>") || token.includes("<think>")) { |
|
|
isThinking = true; |
|
|
thinkingContent = ""; |
|
|
lastThinkingMessageIndex = -1; |
|
|
return; |
|
|
} |
|
|
if (token.includes("</thinking>") || token.includes("</think>")) { |
|
|
isThinking = false; |
|
|
if (thinkingContent.trim()) { |
|
|
|
|
|
if (lastThinkingMessageIndex === -1) { |
|
|
chatStore.add("thinking", thinkingContent, null, null, chatId); |
|
|
} |
|
|
} |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
if (isThinking) { |
|
|
thinkingContent += token; |
|
|
|
|
|
const request = activeRequests.get(chatId); |
|
|
if (request) { |
|
|
request.tokensReceived += Math.ceil(token.length / 4); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (lastThinkingMessageIndex === -1) { |
|
|
|
|
|
chatStore.add("thinking", thinkingContent, null, null, chatId); |
|
|
const targetChat = chatStore.getChat(chatId); |
|
|
lastThinkingMessageIndex = targetChat ? targetChat.history.length - 1 : -1; |
|
|
} else { |
|
|
|
|
|
const currentChat = chatStore.getChat(chatId); |
|
|
if (currentChat && lastThinkingMessageIndex >= 0) { |
|
|
const lastMessage = currentChat.history[lastThinkingMessageIndex]; |
|
|
if (lastMessage && lastMessage.role === "thinking") { |
|
|
lastMessage.content = thinkingContent; |
|
|
lastMessage.html = DOMPurify.sanitize(marked.parse(thinkingContent)); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
const now = Date.now(); |
|
|
if (now - lastThinkingScrollTime > THINKING_SCROLL_THROTTLE) { |
|
|
lastThinkingScrollTime = now; |
|
|
setTimeout(() => { |
|
|
|
|
|
const chatContainer = document.getElementById('chat'); |
|
|
if (chatContainer) { |
|
|
chatContainer.scrollTo({ |
|
|
top: chatContainer.scrollHeight, |
|
|
behavior: 'smooth' |
|
|
}); |
|
|
} |
|
|
|
|
|
scrollThinkingBoxToBottom(); |
|
|
}, 100); |
|
|
} |
|
|
} else { |
|
|
|
|
|
contentBuffer.push(token); |
|
|
|
|
|
if (lastAssistantMessageIndex === -1) { |
|
|
const currentChat = chatStore.getChat(chatId); |
|
|
if (currentChat) { |
|
|
const targetHistory = currentChat.history; |
|
|
|
|
|
for (let i = targetHistory.length - 1; i >= 0; i--) { |
|
|
if (targetHistory[i].role === "assistant") { |
|
|
lastAssistantMessageIndex = i; |
|
|
break; |
|
|
} |
|
|
} |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
} else { |
|
|
|
|
|
contentBuffer.push(token); |
|
|
|
|
|
if (lastAssistantMessageIndex === -1) { |
|
|
const currentChat = chatStore.getChat(chatId); |
|
|
if (currentChat) { |
|
|
const targetHistory = currentChat.history; |
|
|
|
|
|
for (let i = targetHistory.length - 1; i >= 0; i--) { |
|
|
if (targetHistory[i].role === "assistant") { |
|
|
lastAssistantMessageIndex = i; |
|
|
break; |
|
|
} |
|
|
} |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} catch (error) { |
|
|
console.error("Failed to parse line:", line, error); |
|
|
} |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
if (contentBuffer.length > 0) { |
|
|
addToChat(contentBuffer.join("")); |
|
|
|
|
|
const currentChat = chatStore.getChat(chatId); |
|
|
if (currentChat) { |
|
|
const targetHistory = currentChat.history; |
|
|
for (let i = targetHistory.length - 1; i >= 0; i--) { |
|
|
if (targetHistory[i].role === "assistant") { |
|
|
lastAssistantMessageIndex = i; |
|
|
break; |
|
|
} |
|
|
} |
|
|
} |
|
|
contentBuffer = []; |
|
|
|
|
|
setTimeout(() => { |
|
|
const chatContainer = document.getElementById('chat'); |
|
|
if (chatContainer) { |
|
|
chatContainer.scrollTo({ |
|
|
top: chatContainer.scrollHeight, |
|
|
behavior: 'smooth' |
|
|
}); |
|
|
} |
|
|
}, 50); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (contentBuffer.length > 0) { |
|
|
addToChat(contentBuffer.join("")); |
|
|
} |
|
|
|
|
|
|
|
|
const finalChat = chatStore.getChat(chatId); |
|
|
if (finalChat && reasoningContent.trim() && lastReasoningMessageIndex === -1) { |
|
|
const isMCPMode = finalChat.mcpMode || false; |
|
|
const shouldExpand = !isMCPMode; |
|
|
const targetHistory = finalChat.history; |
|
|
|
|
|
const assistantIndex = targetHistory.length - 1; |
|
|
if (assistantIndex >= 0 && targetHistory[assistantIndex]?.role === "assistant") { |
|
|
targetHistory.splice(assistantIndex, 0, { |
|
|
role: "thinking", |
|
|
content: reasoningContent, |
|
|
html: DOMPurify.sanitize(marked.parse(reasoningContent)), |
|
|
image: [], |
|
|
audio: [], |
|
|
expanded: shouldExpand |
|
|
}); |
|
|
} else { |
|
|
chatStore.add("thinking", reasoningContent, null, null, chatId); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (finalChat && thinkingContent.trim() && lastThinkingMessageIndex === -1) { |
|
|
chatStore.add("thinking", thinkingContent, null, null, chatId); |
|
|
} |
|
|
|
|
|
|
|
|
hljs.highlightAll(); |
|
|
} catch (error) { |
|
|
|
|
|
if (error.name !== 'AbortError' || !currentAbortController) { |
|
|
const currentChat = chatStore.getChat(chatId); |
|
|
if (currentChat) { |
|
|
const request = activeRequests.get(chatId); |
|
|
const requestModel = request?.model || null; |
|
|
chatStore.add( |
|
|
"assistant", |
|
|
`<span class='error'>Error: Failed to process stream</span>`, |
|
|
null, |
|
|
null, |
|
|
chatId, |
|
|
requestModel |
|
|
); |
|
|
} |
|
|
} |
|
|
} finally { |
|
|
|
|
|
if (reader) { |
|
|
reader.releaseLock(); |
|
|
} |
|
|
|
|
|
const activeChat = chatStore.activeChat(); |
|
|
if (activeChat && activeChat.id === chatId) { |
|
|
currentReader = null; |
|
|
currentAbortController = null; |
|
|
toggleLoader(false, chatId); |
|
|
} |
|
|
|
|
|
activeRequests.delete(chatId); |
|
|
updateRequestTracking(chatId, false); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const finalActiveChat = chatStore.activeChat(); |
|
|
if (finalActiveChat && finalActiveChat.id === chatId) { |
|
|
toggleLoader(false, chatId); |
|
|
} |
|
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
const chatContainer = document.getElementById('chat'); |
|
|
if (chatContainer) { |
|
|
chatContainer.scrollTo({ |
|
|
top: chatContainer.scrollHeight, |
|
|
behavior: 'smooth' |
|
|
}); |
|
|
} |
|
|
}, 100); |
|
|
|
|
|
|
|
|
document.getElementById("input").focus(); |
|
|
} |
|
|
|
|
|
document.getElementById("system_prompt").addEventListener("submit", submitSystemPrompt); |
|
|
document.getElementById("prompt").addEventListener("submit", submitPrompt); |
|
|
document.getElementById("input").focus(); |
|
|
|
|
|
storesystemPrompt = localStorage.getItem("system_prompt"); |
|
|
if (storesystemPrompt) { |
|
|
document.getElementById("systemPrompt").value = storesystemPrompt; |
|
|
} else { |
|
|
document.getElementById("systemPrompt").value = null; |
|
|
} |
|
|
|
|
|
marked.setOptions({ |
|
|
highlight: function (code) { |
|
|
return hljs.highlightAuto(code).value; |
|
|
}, |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
document.addEventListener("alpine:init", () => { |
|
|
|
|
|
if (!Alpine.store("chat")) { |
|
|
|
|
|
|
|
|
function generateChatId() { |
|
|
return "chat_" + Date.now() + "_" + Math.random().toString(36).substr(2, 9); |
|
|
} |
|
|
|
|
|
function getCurrentModel() { |
|
|
const modelInput = document.getElementById("chat-model"); |
|
|
return modelInput ? modelInput.value : ""; |
|
|
} |
|
|
|
|
|
Alpine.store("chat", { |
|
|
chats: [], |
|
|
activeChatId: null, |
|
|
chatIdCounter: 0, |
|
|
languages: [undefined], |
|
|
activeRequestIds: [], |
|
|
|
|
|
activeChat() { |
|
|
if (!this.activeChatId) return null; |
|
|
return this.chats.find(c => c.id === this.activeChatId) || null; |
|
|
}, |
|
|
|
|
|
getChat(chatId) { |
|
|
return this.chats.find(c => c.id === chatId) || null; |
|
|
}, |
|
|
|
|
|
createChat(model, systemPrompt, mcpMode) { |
|
|
const chatId = generateChatId(); |
|
|
const now = Date.now(); |
|
|
const chat = { |
|
|
id: chatId, |
|
|
name: "New Chat", |
|
|
model: model || getCurrentModel() || "", |
|
|
history: [], |
|
|
systemPrompt: systemPrompt || "", |
|
|
mcpMode: mcpMode || false, |
|
|
tokenUsage: { |
|
|
promptTokens: 0, |
|
|
completionTokens: 0, |
|
|
totalTokens: 0, |
|
|
currentRequest: null |
|
|
}, |
|
|
contextSize: null, |
|
|
createdAt: now, |
|
|
updatedAt: now |
|
|
}; |
|
|
this.chats.push(chat); |
|
|
this.activeChatId = chatId; |
|
|
return chat; |
|
|
}, |
|
|
|
|
|
switchChat(chatId) { |
|
|
if (this.chats.find(c => c.id === chatId)) { |
|
|
this.activeChatId = chatId; |
|
|
return true; |
|
|
} |
|
|
return false; |
|
|
}, |
|
|
|
|
|
deleteChat(chatId) { |
|
|
const index = this.chats.findIndex(c => c.id === chatId); |
|
|
if (index === -1) return false; |
|
|
|
|
|
this.chats.splice(index, 1); |
|
|
|
|
|
if (this.activeChatId === chatId) { |
|
|
if (this.chats.length > 0) { |
|
|
this.activeChatId = this.chats[0].id; |
|
|
} else { |
|
|
this.createChat(); |
|
|
} |
|
|
} |
|
|
return true; |
|
|
}, |
|
|
|
|
|
updateChatName(chatId, name) { |
|
|
const chat = this.getChat(chatId); |
|
|
if (chat) { |
|
|
chat.name = name || "New Chat"; |
|
|
chat.updatedAt = Date.now(); |
|
|
return true; |
|
|
} |
|
|
return false; |
|
|
}, |
|
|
|
|
|
clear() { |
|
|
const chat = this.activeChat(); |
|
|
if (chat) { |
|
|
chat.history.length = 0; |
|
|
chat.tokenUsage = { |
|
|
promptTokens: 0, |
|
|
completionTokens: 0, |
|
|
totalTokens: 0, |
|
|
currentRequest: null |
|
|
}; |
|
|
chat.updatedAt = Date.now(); |
|
|
} |
|
|
}, |
|
|
|
|
|
updateTokenUsage(usage, targetChatId = null) { |
|
|
|
|
|
|
|
|
const chat = targetChatId ? this.getChat(targetChatId) : this.activeChat(); |
|
|
if (!chat) return; |
|
|
|
|
|
if (usage) { |
|
|
const currentRequest = chat.tokenUsage.currentRequest || { |
|
|
promptTokens: 0, |
|
|
completionTokens: 0, |
|
|
totalTokens: 0 |
|
|
}; |
|
|
|
|
|
const isNewUsage = |
|
|
(usage.prompt_tokens !== undefined && usage.prompt_tokens > currentRequest.promptTokens) || |
|
|
(usage.completion_tokens !== undefined && usage.completion_tokens > currentRequest.completionTokens) || |
|
|
(usage.total_tokens !== undefined && usage.total_tokens > currentRequest.totalTokens); |
|
|
|
|
|
if (isNewUsage) { |
|
|
chat.tokenUsage.promptTokens = chat.tokenUsage.promptTokens - currentRequest.promptTokens + (usage.prompt_tokens || 0); |
|
|
chat.tokenUsage.completionTokens = chat.tokenUsage.completionTokens - currentRequest.completionTokens + (usage.completion_tokens || 0); |
|
|
chat.tokenUsage.totalTokens = chat.tokenUsage.totalTokens - currentRequest.totalTokens + (usage.total_tokens || 0); |
|
|
|
|
|
chat.tokenUsage.currentRequest = { |
|
|
promptTokens: usage.prompt_tokens || 0, |
|
|
completionTokens: usage.completion_tokens || 0, |
|
|
totalTokens: usage.total_tokens || 0 |
|
|
}; |
|
|
chat.updatedAt = Date.now(); |
|
|
} |
|
|
} |
|
|
}, |
|
|
|
|
|
getRemainingTokens() { |
|
|
const chat = this.activeChat(); |
|
|
if (!chat || !chat.contextSize) return null; |
|
|
return Math.max(0, chat.contextSize - chat.tokenUsage.totalTokens); |
|
|
}, |
|
|
|
|
|
getContextUsagePercent() { |
|
|
const chat = this.activeChat(); |
|
|
if (!chat || !chat.contextSize) return null; |
|
|
return Math.min(100, (chat.tokenUsage.totalTokens / chat.contextSize) * 100); |
|
|
}, |
|
|
|
|
|
|
|
|
hasActiveRequest(chatId) { |
|
|
if (!chatId) return false; |
|
|
|
|
|
return this.activeRequestIds.includes(chatId); |
|
|
}, |
|
|
|
|
|
|
|
|
updateActiveRequestTracking(chatId, isActive) { |
|
|
if (isActive) { |
|
|
if (!this.activeRequestIds.includes(chatId)) { |
|
|
this.activeRequestIds.push(chatId); |
|
|
} |
|
|
} else { |
|
|
const index = this.activeRequestIds.indexOf(chatId); |
|
|
if (index > -1) { |
|
|
this.activeRequestIds.splice(index, 1); |
|
|
} |
|
|
} |
|
|
}, |
|
|
|
|
|
add(role, content, image, audio, targetChatId = null) { |
|
|
|
|
|
const chat = targetChatId ? this.getChat(targetChatId) : this.activeChat(); |
|
|
if (!chat) return; |
|
|
|
|
|
const N = chat.history.length - 1; |
|
|
if (role === "thinking" || role === "reasoning") { |
|
|
let c = ""; |
|
|
const lines = content.split("\n"); |
|
|
lines.forEach((line) => { |
|
|
c += DOMPurify.sanitize(marked.parse(line)); |
|
|
}); |
|
|
chat.history.push({ role, content, html: c, image, audio }); |
|
|
} |
|
|
else if (chat.history.length && chat.history[N].role === role) { |
|
|
chat.history[N].content += content; |
|
|
chat.history[N].html = DOMPurify.sanitize( |
|
|
marked.parse(chat.history[N].content) |
|
|
); |
|
|
if (image && image.length > 0) { |
|
|
chat.history[N].image = [...(chat.history[N].image || []), ...image]; |
|
|
} |
|
|
if (audio && audio.length > 0) { |
|
|
chat.history[N].audio = [...(chat.history[N].audio || []), ...audio]; |
|
|
} |
|
|
} else { |
|
|
let c = ""; |
|
|
const lines = content.split("\n"); |
|
|
lines.forEach((line) => { |
|
|
c += DOMPurify.sanitize(marked.parse(line)); |
|
|
}); |
|
|
chat.history.push({ |
|
|
role, |
|
|
content, |
|
|
html: c, |
|
|
image: image || [], |
|
|
audio: audio || [] |
|
|
}); |
|
|
|
|
|
if (role === "user" && chat.name === "New Chat" && content.trim()) { |
|
|
const name = content.trim().substring(0, 50); |
|
|
chat.name = name.length < content.trim().length ? name + "..." : name; |
|
|
} |
|
|
} |
|
|
|
|
|
chat.updatedAt = Date.now(); |
|
|
|
|
|
const chatContainer = document.getElementById('chat'); |
|
|
if (chatContainer) { |
|
|
chatContainer.scrollTo({ |
|
|
top: chatContainer.scrollHeight, |
|
|
behavior: 'smooth' |
|
|
}); |
|
|
} |
|
|
if (role === "thinking" || role === "reasoning") { |
|
|
setTimeout(() => { |
|
|
if (typeof window.scrollThinkingBoxToBottom === 'function') { |
|
|
window.scrollThinkingBoxToBottom(); |
|
|
} |
|
|
}, 100); |
|
|
} |
|
|
const parser = new DOMParser(); |
|
|
const html = parser.parseFromString( |
|
|
chat.history[chat.history.length - 1].html, |
|
|
"text/html" |
|
|
); |
|
|
const code = html.querySelectorAll("pre code"); |
|
|
if (!code.length) return; |
|
|
code.forEach((el) => { |
|
|
const language = el.className.split("language-")[1]; |
|
|
if (this.languages.includes(language)) return; |
|
|
const script = document.createElement("script"); |
|
|
script.src = `https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.8.0/build/languages/${language}.min.js`; |
|
|
document.head.appendChild(script); |
|
|
this.languages.push(language); |
|
|
}); |
|
|
}, |
|
|
|
|
|
messages() { |
|
|
const chat = this.activeChat(); |
|
|
if (!chat) return []; |
|
|
return chat.history.map((message) => ({ |
|
|
role: message.role, |
|
|
content: message.content, |
|
|
image: message.image, |
|
|
audio: message.audio, |
|
|
})); |
|
|
}, |
|
|
|
|
|
|
|
|
get activeHistory() { |
|
|
const chat = this.activeChat(); |
|
|
return chat ? chat.history : []; |
|
|
}, |
|
|
}); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() { |
|
|
|
|
|
setTimeout(() => { |
|
|
if (!window.Alpine || !Alpine.store("chat")) { |
|
|
console.error('Alpine store not initialized'); |
|
|
return; |
|
|
} |
|
|
|
|
|
const chatStore = Alpine.store("chat"); |
|
|
|
|
|
|
|
|
const chatData = localStorage.getItem('localai_index_chat_data'); |
|
|
let shouldCreateNewChat = false; |
|
|
let indexChatData = null; |
|
|
|
|
|
if (chatData) { |
|
|
try { |
|
|
indexChatData = JSON.parse(chatData); |
|
|
shouldCreateNewChat = true; |
|
|
} catch (error) { |
|
|
console.error('Error parsing chat data from index:', error); |
|
|
localStorage.removeItem('localai_index_chat_data'); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const storedData = loadChatsFromStorage(); |
|
|
|
|
|
if (storedData && storedData.chats && storedData.chats.length > 0) { |
|
|
|
|
|
chatStore.chats.length = 0; |
|
|
storedData.chats.forEach(chat => { |
|
|
chatStore.chats.push(chat); |
|
|
}); |
|
|
|
|
|
if (!shouldCreateNewChat) { |
|
|
chatStore.activeChatId = storedData.activeChatId || storedData.chats[0].id; |
|
|
|
|
|
|
|
|
if (!chatStore.activeChat()) { |
|
|
chatStore.activeChatId = storedData.chats[0].id; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
if (shouldCreateNewChat) { |
|
|
|
|
|
const currentModel = document.getElementById("chat-model")?.value || ""; |
|
|
|
|
|
const urlParams = new URLSearchParams(window.location.search); |
|
|
const mcpFromUrl = urlParams.get('mcp') === 'true'; |
|
|
const newChat = chatStore.createChat(currentModel, "", mcpFromUrl || indexChatData.mcpMode || false); |
|
|
|
|
|
|
|
|
const contextSizeInput = document.getElementById("chat-model"); |
|
|
if (contextSizeInput && contextSizeInput.dataset.contextSize) { |
|
|
const contextSize = parseInt(contextSizeInput.dataset.contextSize); |
|
|
newChat.contextSize = contextSize; |
|
|
} |
|
|
|
|
|
|
|
|
const input = document.getElementById('input'); |
|
|
if (input && indexChatData.message) { |
|
|
input.value = indexChatData.message; |
|
|
|
|
|
|
|
|
if (indexChatData.imageFiles && indexChatData.imageFiles.length > 0) { |
|
|
indexChatData.imageFiles.forEach(file => { |
|
|
images.push(file.data); |
|
|
}); |
|
|
} |
|
|
|
|
|
if (indexChatData.audioFiles && indexChatData.audioFiles.length > 0) { |
|
|
indexChatData.audioFiles.forEach(file => { |
|
|
audios.push(file.data); |
|
|
}); |
|
|
} |
|
|
|
|
|
if (indexChatData.textFiles && indexChatData.textFiles.length > 0) { |
|
|
indexChatData.textFiles.forEach(file => { |
|
|
fileContents.push({ name: file.name, content: file.data }); |
|
|
currentFileNames.push(file.name); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
localStorage.removeItem('localai_index_chat_data'); |
|
|
|
|
|
|
|
|
saveChatsToStorage(); |
|
|
|
|
|
|
|
|
updateUIForActiveChat(); |
|
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
if (input.value.trim()) { |
|
|
processAndSendMessage(input.value); |
|
|
} |
|
|
}, 500); |
|
|
} else { |
|
|
|
|
|
localStorage.removeItem('localai_index_chat_data'); |
|
|
|
|
|
|
|
|
const urlParams = new URLSearchParams(window.location.search); |
|
|
if (urlParams.get('mcp') === 'true' && newChat) { |
|
|
newChat.mcpMode = true; |
|
|
saveChatsToStorage(); |
|
|
updateUIForActiveChat(); |
|
|
} |
|
|
saveChatsToStorage(); |
|
|
updateUIForActiveChat(); |
|
|
} |
|
|
} else { |
|
|
|
|
|
if (!storedData || !storedData.chats || storedData.chats.length === 0) { |
|
|
const currentModel = document.getElementById("chat-model")?.value || ""; |
|
|
const oldSystemPrompt = localStorage.getItem(SYSTEM_PROMPT_STORAGE_KEY); |
|
|
|
|
|
const urlParams = new URLSearchParams(window.location.search); |
|
|
const mcpFromUrl = urlParams.get('mcp') === 'true'; |
|
|
chatStore.createChat(currentModel, oldSystemPrompt || "", mcpFromUrl); |
|
|
|
|
|
|
|
|
if (oldSystemPrompt) { |
|
|
localStorage.removeItem(SYSTEM_PROMPT_STORAGE_KEY); |
|
|
} |
|
|
} else { |
|
|
|
|
|
const urlModel = document.getElementById("chat-model")?.value || ""; |
|
|
const activeChat = chatStore.activeChat(); |
|
|
const shouldCreateNewChat = sessionStorage.getItem('localai_create_new_chat') === 'true'; |
|
|
|
|
|
|
|
|
if (shouldCreateNewChat) { |
|
|
sessionStorage.removeItem('localai_create_new_chat'); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (urlModel && urlModel.trim() && (shouldCreateNewChat || (activeChat && activeChat.model !== urlModel) || !activeChat)) { |
|
|
|
|
|
const urlParams = new URLSearchParams(window.location.search); |
|
|
const mcpFromUrl = urlParams.get('mcp') === 'true'; |
|
|
const newChat = chatStore.createChat(urlModel, "", mcpFromUrl); |
|
|
|
|
|
|
|
|
const contextSizeInput = document.getElementById("chat-model"); |
|
|
if (contextSizeInput && contextSizeInput.dataset.contextSize) { |
|
|
const contextSize = parseInt(contextSizeInput.dataset.contextSize); |
|
|
if (!isNaN(contextSize)) { |
|
|
newChat.contextSize = contextSize; |
|
|
} |
|
|
} |
|
|
|
|
|
saveChatsToStorage(); |
|
|
updateUIForActiveChat(); |
|
|
} else { |
|
|
|
|
|
const urlParams = new URLSearchParams(window.location.search); |
|
|
if (urlParams.get('mcp') === 'true') { |
|
|
if (activeChat) { |
|
|
activeChat.mcpMode = true; |
|
|
saveChatsToStorage(); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const contextSizeInput = document.getElementById("chat-model"); |
|
|
if (contextSizeInput && contextSizeInput.dataset.contextSize) { |
|
|
const contextSize = parseInt(contextSizeInput.dataset.contextSize); |
|
|
const activeChat = chatStore.activeChat(); |
|
|
if (activeChat && !activeChat.contextSize) { |
|
|
activeChat.contextSize = contextSize; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
updateUIForActiveChat(); |
|
|
} |
|
|
|
|
|
|
|
|
saveChatsToStorage(); |
|
|
}, 300); |
|
|
}); |
|
|
|
|
|
|