diff --git a/chatv2.js b/chatv2.js index 661686a..2a48ca4 100644 --- a/chatv2.js +++ b/chatv2.js @@ -1,17 +1,17 @@ const BACKENDS = { - plato : { + plato: { prod: '/api/plato', - dev : 'http://192.168.1.74:1234/v1', + dev: 'http://192.168.1.74:8080/v1', name: 'Plato (192.168.1.74)' }, - stoic : { + stoic: { prod: '/api/stoic', - dev : 'http://192.168.1.159:1234/v1', + dev: 'http://192.168.1.159:1234/v1', name: 'Stoic (192.168.1.159)' }, ollama: { prod: '/api/ollama', - dev : 'http://192.168.1.159:8081/v1', + dev: 'http://192.168.1.159:8081/v1', name: 'Ollama (192.168.1.159:8081)' } } @@ -25,12 +25,13 @@ let currentStreamController = null let isStreaming = false let chatHistory = JSON.parse(localStorage.getItem('chatHistory')) || [] -chatHistory = chatHistory.map(message => ({ - role : message.role || 'user', - content : message.content || '', - markdown : message.markdown || false, - messageId: message.messageId || `msg-${ Date.now() }` -})) +// ✅ FIX: Generate unique IDs and validate message structure +chatHistory = chatHistory.map((message, index) => ({ + role: message.role || 'user', + content: message.content || '', + markdown: message.markdown || false, + messageId: message.messageId || `msg-${Date.now()}-${index}` +})).filter(msg => msg.content && typeof msg.content === 'string') const welcomeMessage = `# Welcome to LM Studio Chat! @@ -38,7 +39,7 @@ I now support **full conversation history**, **real-time streaming responses** a ## Features: 1. **Full Chat History** - Complete conversation context is now sent to the AI -2. **Smart History Management** - Automatically keeps the last ${ MAX_CHAT_HISTORY } messages +2. **Smart History Management** - Automatically keeps the last ${MAX_CHAT_HISTORY} messages 3. **Streaming Mode** (enabled by default) - Watch responses appear word-by-word 4. **Markdown Rendering** - Proper formatting for code, lists, tables, and more 5. **Readable Dark Text** - No more eye strain from light gray text @@ -51,60 +52,102 @@ I now support **full conversation history**, **real-time streaming responses** a > *Tip: You can toggle streaming and markdown using the checkboxes below.*` +// ✅ NEW: Robust parser for backend:model format +function splitBackendModel(value) { + if (!value || value === ':') { + return { backendKey: '', model: '' }; + } + + const colonIndex = value.indexOf(':'); + if (colonIndex === -1) { + return { backendKey: value, model: '' }; + } + + return { + backendKey: value.substring(0, colonIndex), + model: value.substring(colonIndex + 1) + }; +} function getApiUrl() { - const [backendKey, model] = currentBackendModel.split(':') - const backend = BACKENDS[backendKey] - return IS_PRODUCTION ? backend.prod : backend.dev + const { backendKey, model } = splitBackendModel(currentBackendModel); + const effectiveBackendKey = backendKey || 'plato'; + const backend = BACKENDS[effectiveBackendKey]; + + if (!backend) { + throw new Error(`Invalid backend: '${effectiveBackendKey}'`); + } + + return IS_PRODUCTION ? backend.prod : backend.dev; } function updateBackendModelDisplay() { - const [backendKey, model] = currentBackendModel.split(':') - const backend = BACKENDS[backendKey] + const { backendKey, model } = splitBackendModel(currentBackendModel); + const backend = BACKENDS[backendKey]; + if (!backend) return; + const displayText = IS_PRODUCTION - ? `${ backend.prod } ? ${ backend.name }` - : backend.dev - document.getElementById('backendDisplay').textContent = `${ displayText } - Model: ${ model }` + ? `${backend.prod} - ${backend.name}` + : backend.dev; + document.getElementById('backendDisplay').textContent = `${displayText} - Model: ${model || 'None'}`; } async function fetchBackendModels() { try { - const response = await fetch(`${ getApiUrl() }/models`) - if (response.ok) { - const data = await response.json() - availableBackendModels = data.data.map(model => ({ - backend: currentBackendModel.split(':')[0], - model: model.id - })) - populateBackendModelSelector() + // ✅ FIX: Ensure valid backend before fetching + if (!currentBackendModel || currentBackendModel === ':') { + currentBackendModel = 'plato'; + } - if (!currentBackendModel && availableBackendModels.length > 0) { - currentBackendModel = `${ availableBackendModels[0].backend }:${ availableBackendModels[0].model }` - document.getElementById('backendModelSelector').value = currentBackendModel + const apiUrl = getApiUrl(); + console.log(`Fetching models from: ${apiUrl}`); + + const response = await fetch(`${apiUrl}/models`); + + if (response.ok) { + const data = await response.json(); + + const { backendKey } = splitBackendModel(currentBackendModel); + const effectiveBackendKey = backendKey || 'plato'; + + availableBackendModels = data.data.map(model => ({ + backend: effectiveBackendKey, + model: model.id + })); + + populateBackendModelSelector(); + + // ✅ FIX: Only auto-select if no model is set + const hasModel = currentBackendModel && currentBackendModel.includes(':') && currentBackendModel.split(':')[1]; + if (!hasModel && availableBackendModels.length > 0) { + currentBackendModel = `${availableBackendModels[0].backend}:${availableBackendModels[0].model}`; + document.getElementById('backendModelSelector').value = currentBackendModel; } + + updateBackendModelDisplay(); } else { - console.error('Failed to fetch models:', response.statusText) - document.getElementById('backendModelSelector').innerHTML = '' + console.error('Failed to fetch models:', response.statusText); + document.getElementById('backendModelSelector').innerHTML = ''; } } catch (error) { - console.error('Error fetching models:', error) - document.getElementById('backendModelSelector').innerHTML = '' + console.error('Error fetching models:', error); + document.getElementById('backendModelSelector').innerHTML = ''; } } function populateBackendModelSelector() { - const selector = document.getElementById('backendModelSelector') + const selector = document.getElementById('backendModelSelector'); if (availableBackendModels.length === 0) { - selector.innerHTML = '' - return + selector.innerHTML = ''; + return; } selector.innerHTML = availableBackendModels.map(backendModel => - `` - ).join('') + `` + ).join(''); if (currentBackendModel) { - selector.value = currentBackendModel + selector.value = currentBackendModel; } } @@ -112,472 +155,500 @@ function getApiMessages() { return chatHistory.slice(-MAX_CHAT_HISTORY).map(({ role, content }) => ({ role, content - })) + })); } function trimChatHistory() { if (chatHistory.length > MAX_CHAT_HISTORY) { - chatHistory = chatHistory.slice(-MAX_CHAT_HISTORY) - localStorage.setItem('chatHistory', JSON.stringify(chatHistory)) + chatHistory = chatHistory.slice(-MAX_CHAT_HISTORY); + localStorage.setItem('chatHistory', JSON.stringify(chatHistory)); } } document.addEventListener('DOMContentLoaded', () => { - const backendModelSelector = document.getElementById('backendModelSelector') + const backendModelSelector = document.getElementById('backendModelSelector'); backendModelSelector.addEventListener('change', (e) => { - currentBackendModel = e.target.value - updateBackendModelDisplay() - fetchBackendModels() - }) + currentBackendModel = e.target.value; + updateBackendModelDisplay(); + fetchBackendModels(); + }); - fetchBackendModels() -}) + fetchBackendModels(); +}); -const chatLog = document.getElementById('chatLog') -const userInput = document.getElementById('userInput') -const sendBtn = document.getElementById('sendBtn') -const streamToggle = document.getElementById('streamToggle') -const markdownToggle = document.getElementById('markdownToggle') -const modelNameSpan = document.getElementById('modelName') +const chatLog = document.getElementById('chatLog'); +const userInput = document.getElementById('userInput'); +const sendBtn = document.getElementById('sendBtn'); +const streamToggle = document.getElementById('streamToggle'); +const markdownToggle = document.getElementById('markdownToggle'); +const modelNameSpan = document.getElementById('modelName'); marked.setOptions({ - gfm : true, - breaks : true, + gfm: true, + breaks: true, highlight: function(code, lang) { if (window.hljs) { - const language = hljs.getLanguage(lang) ? lang : 'plaintext' - return hljs.highlight(code, { language }).value + const language = hljs.getLanguage(lang) ? lang : 'plaintext'; + return hljs.highlight(code, { language }).value; } - return code + return code; } -}) +}); function autoResize(textarea) { - textarea.style.height = 'auto' - textarea.style.height = Math.min(textarea.scrollHeight, 300) + 'px' + textarea.style.height = 'auto'; + textarea.style.height = Math.min(textarea.scrollHeight, 300) + 'px'; } function addMessage(role, content, markdown = false, messageId = null) { - const messageDiv = document.createElement('div') - messageDiv.className = `message message-${ role }` + const messageDiv = document.createElement('div'); + messageDiv.className = `message message-${role}`; if (messageId) { - messageDiv.id = messageId + messageDiv.id = messageId; } - const headerDiv = document.createElement('div') - headerDiv.className = `message-header ${ role }` + const headerDiv = document.createElement('div'); + headerDiv.className = `message-header ${role}`; - const avatar = document.createElement('div') - avatar.className = `avatar ${ role }-avatar` - avatar.textContent = role === 'user' ? '?' : '?' + const avatar = document.createElement('div'); + avatar.className = `avatar ${role}-avatar`; + avatar.textContent = role === 'user' ? '?' : '?'; - const name = document.createElement('span') - name.textContent = role === 'user' ? 'You' : 'Assistant' + const name = document.createElement('span'); + name.textContent = role === 'user' ? 'You' : 'Assistant'; - headerDiv.appendChild(avatar) - headerDiv.appendChild(name) + headerDiv.appendChild(avatar); + headerDiv.appendChild(name); - const contentDiv = document.createElement('div') + const contentDiv = document.createElement('div'); if (markdown && role === 'assistant' && markdownToggle.checked) { - contentDiv.className = 'markdown-content' - const processedContent = parseThinkingTags(content) - contentDiv.innerHTML = marked.parse(processedContent) + contentDiv.className = 'markdown-content'; + const processedContent = parseThinkingTags(content); + contentDiv.innerHTML = marked.parse(processedContent); if (window.hljs) { setTimeout(() => { contentDiv.querySelectorAll('pre code').forEach((block) => { - hljs.highlightElement(block) - }) - }, 0) + hljs.highlightElement(block); + }); + }, 0); } } else { - const processedContent = parseThinkingTags(content) - contentDiv.innerHTML = processedContent - contentDiv.style.whiteSpace = 'pre-wrap' - contentDiv.style.padding = '8px 0' - contentDiv.style.color = '#2d3339' + const processedContent = parseThinkingTags(content); + contentDiv.innerHTML = processedContent; + contentDiv.style.whiteSpace = 'pre-wrap'; + contentDiv.style.padding = '8px 0'; + contentDiv.style.color = '#2d3339'; } - messageDiv.appendChild(headerDiv) - messageDiv.appendChild(contentDiv) - chatLog.appendChild(messageDiv) - chatLog.scrollTop = chatLog.scrollHeight + messageDiv.appendChild(headerDiv); + messageDiv.appendChild(contentDiv); + chatLog.appendChild(messageDiv); + chatLog.scrollTop = chatLog.scrollHeight; - chatHistory.push({ role, content, markdown, messageId }) - trimChatHistory() - localStorage.setItem('chatHistory', JSON.stringify(chatHistory)) + chatHistory.push({ role, content, markdown, messageId }); + trimChatHistory(); + localStorage.setItem('chatHistory', JSON.stringify(chatHistory)); - return contentDiv + return contentDiv; } function parseThinkingTags(content) { - let result = content - let thinkingCounter = 0 + let result = content; + let thinkingCounter = 0; - const completeThinkRegex = /\[THINK\]([\s\S]*?)\[\/THINK\]/gi + const completeThinkRegex = /\[THINK\]([\s\S]*?)\[\/THINK\]/gi; result = result.replace(completeThinkRegex, (match, thinkContent) => { - thinkingCounter++ - const id = `thinking-${ Date.now() }-${ thinkingCounter }` + thinkingCounter++; + const id = `thinking-${Date.now()}-${thinkingCounter}`; return `
-
- ? +
+ ? ? Thinking...
-
${ thinkContent.trim() }
-
` - }) +
${thinkContent.trim()}
+
`; + }); - const incompleteThinkRegex = /\[THINK\]([\s\S]*?)$/gi + const incompleteThinkRegex = /\[THINK\]([\s\S]*?)$/gi; result = result.replace(incompleteThinkRegex, (match, thinkContent) => { - thinkingCounter++ - const id = `thinking-${ Date.now() }-${ thinkingCounter }` + thinkingCounter++; + const id = `thinking-${Date.now()}-${thinkingCounter}`; return `
-
- ? +
+ ? ? Thinking...
-
${ thinkContent.trim() }
-
` - }) +
${thinkContent.trim()}
+
`; + }); - return result + return result; } window.toggleThinking = function(id) { - const content = document.getElementById(id) - const toggle = document.getElementById(id + '-toggle') + const content = document.getElementById(id); + const toggle = document.getElementById(id + '-toggle'); if (content && toggle) { - content.classList.toggle('hidden') - toggle.classList.toggle('collapsed') + content.classList.toggle('hidden'); + toggle.classList.toggle('collapsed'); } -} +}; function updateMessageContent(contentDiv, newContent, markdown = false, role) { - const processedContent = parseThinkingTags(newContent) + const processedContent = parseThinkingTags(newContent); if (markdown && role === 'assistant' && markdownToggle.checked) { - contentDiv.innerHTML = marked.parse(processedContent) + contentDiv.innerHTML = marked.parse(processedContent); if (window.hljs) { setTimeout(() => { contentDiv.querySelectorAll('pre code').forEach((block) => { - hljs.highlightElement(block) - }) - }, 0) + hljs.highlightElement(block); + }); + }, 0); } } else { - contentDiv.textContent = newContent + contentDiv.innerHTML = processedContent; } } function showTypingIndicator() { - const typingDiv = document.createElement('div') - typingDiv.id = 'typingIndicator' - typingDiv.className = 'message message-assistant' + const typingDiv = document.createElement('div'); + typingDiv.id = 'typingIndicator'; + typingDiv.className = 'message message-assistant'; - const headerDiv = document.createElement('div') - headerDiv.className = 'message-header assistant' + const headerDiv = document.createElement('div'); + headerDiv.className = 'message-header assistant'; - const avatar = document.createElement('div') - avatar.className = 'avatar assistant-avatar' - avatar.textContent = '?' + const avatar = document.createElement('div'); + avatar.className = 'avatar assistant-avatar'; + avatar.textContent = '?'; - const name = document.createElement('span') - name.textContent = 'Assistant' + const name = document.createElement('span'); + name.textContent = 'Assistant'; - headerDiv.appendChild(abatar) - headerDiv.appendChild(name) + headerDiv.appendChild(avatar); // ✅ FIX: Typo "abatar" + headerDiv.appendChild(name); - const dotsDiv = document.createElement('div') - dotsDiv.className = 'typing-indicator' + const dotsDiv = document.createElement('div'); + dotsDiv.className = 'typing-indicator'; dotsDiv.innerHTML = ` -
-
-
- Thinking... - ` +
+
+
+ Thinking... + `; - typingDiv.appendChild(headerDiv) - typingDiv.appendChild(dotsDiv) - chatLog.appendChild(typingDiv) - chatLog.scrollTop = chatLog.scrollHeight + typingDiv.appendChild(headerDiv); + typingDiv.appendChild(dotsDiv); + chatLog.appendChild(typingDiv); + chatLog.scrollTop = chatLog.scrollHeight; - return typingDiv + return typingDiv; } function removeTypingIndicator(typingDiv) { if (typingDiv && typingDiv.parentNode) { - typingDiv.parentNode.removeChild(typingDiv) + typingDiv.parentNode.removeChild(typingDiv); } } async function handleStreamingResponse(userMessage) { - const messageId = 'msg-' + Date.now() - let accumulatedContent = '' - let contentDiv = null + const messageId = 'msg-' + Date.now(); + let accumulatedContent = ''; + let contentDiv = null; - const messageDiv = document.createElement('div') - messageDiv.className = 'message message-assistant' - messageDiv.id = messageId + const messageDiv = document.createElement('div'); + messageDiv.className = 'message message-assistant'; + messageDiv.id = messageId; - const headerDiv = document.createElement('div') - headerDiv.className = 'message-header assistant' + const headerDiv = document.createElement('div'); + headerDiv.className = 'message-header assistant'; - const avatar = document.createElement('div') - avatar.className = 'avatar assistant-avatar' - avatar.textContent = '?' + const avatar = document.createElement('div'); + avatar.className = 'avatar assistant-avatar'; + avatar.textContent = '?'; - const name = document.createElement('span') - name.textContent = 'Assistant' + const name = document.createElement('span'); + name.textContent = 'Assistant'; - headerDiv.appendChild(avatar) - headerDiv.appendChild(name) + headerDiv.appendChild(avatar); + headerDiv.appendChild(name); - contentDiv = document.createElement('div') - contentDiv.className = 'markdown-content' + contentDiv = document.createElement('div'); + contentDiv.className = 'markdown-content'; - messageDiv.appendChild(headerDiv) - messageDiv.appendChild(contentDiv) - chatLog.appendChild(messageDiv) + messageDiv.appendChild(headerDiv); + messageDiv.appendChild(contentDiv); + chatLog.appendChild(messageDiv); - const cursorSpan = document.createElement('span') - cursorSpan.className = 'streaming-cursor' - contentDiv.appendChild(cursorSpan) + const cursorSpan = document.createElement('span'); + cursorSpan.className = 'streaming-cursor'; + contentDiv.appendChild(cursorSpan); + + // ✅ FIX: Create AbortController BEFORE fetch + currentStreamController = new AbortController(); + isStreaming = true; try { - if (!currentModel && availableModels.length > 0) { - currentModel = availableModels[0].id - document.getElementById('modelSelector').value = currentModel - } + const { backendKey, model } = splitBackendModel(currentBackendModel); + const selectedModel = model || availableBackendModels[0]?.model || 'local-model'; const requestBody = { - model : currentModel || 'local-model', - messages : getApiMessages(), - stream : true, + model: selectedModel, + messages: getApiMessages(), + stream: true, temperature: 0.7, - max_tokens : 2000 - } + max_tokens: 2000 + }; - const response = await fetch(`${ getApiUrl() }/chat/completions`, { - method : 'POST', + const response = await fetch(`${getApiUrl()}/chat/completions`, { + method: 'POST', headers: { - 'Content-Type' : 'application/json', - 'Authorization': `Bearer ${ API_KEY }` + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${API_KEY}` }, - body : JSON.stringify(requestBody) - }) + body: JSON.stringify(requestBody), + signal: currentStreamController.signal // ✅ Now works correctly + }); if (!response.ok) { - throw new Error(`API Error: ${ response.status } ${ response.statusText }`) + throw new Error(`API Error: ${response.status} ${response.statusText}`); } - const reader = response.body.getReader() - const decoder = new TextDecoder() + const reader = response.body.getReader(); + const decoder = new TextDecoder(); - isStreaming = true - currentStreamController = new AbortController() - - let updateScheduled = false - let lastUpdate = Date.now() - const MIN_UPDATE_INTERVAL = 100 + let updateScheduled = false; + let lastUpdate = Date.now(); + const MIN_UPDATE_INTERVAL = 100; function scheduleUpdate() { - if (updateScheduled) return + if (updateScheduled) return; - const now = Date.now() - const timeSinceLastUpdate = now - lastUpdate + const now = Date.now(); + const timeSinceLastUpdate = now - lastUpdate; if (timeSinceLastUpdate >= MIN_UPDATE_INTERVAL) { - updateScheduled = true + updateScheduled = true; requestAnimationFrame(() => { - updateMessageContent(contentDiv, accumulatedContent, markdownToggle.checked, 'assistant') + updateMessageContent(contentDiv, accumulatedContent, markdownToggle.checked, 'assistant'); if (cursorSpan.parentNode) { - contentDiv.appendChild(cursorSpan) + contentDiv.appendChild(cursorSpan); } - chatLog.scrollTop = chatLog.scrollHeight - lastUpdate = Date.now() - updateScheduled = false - }) + chatLog.scrollTop = chatLog.scrollHeight; + lastUpdate = Date.now(); + updateScheduled = false; + }); } else { - updateScheduled = true + updateScheduled = true; setTimeout(() => { requestAnimationFrame(() => { - updateMessageContent(contentDiv, accumulatedContent, markdownToggle.checked, 'assistant') + updateMessageContent(contentDiv, accumulatedContent, markdownToggle.checked, 'assistant'); if (cursorSpan.parentNode) { - contentDiv.appendChild(cursorSpan) + contentDiv.appendChild(cursorSpan); } - chatLog.scrollTop = chatLog.scrollHeight - lastUpdate = Date.now() - updateScheduled = false - }) - }, MIN_UPDATE_INTERVAL - timeSinceLastUpdate) + chatLog.scrollTop = chatLog.scrollHeight; + lastUpdate = Date.now(); + updateScheduled = false; + }); + }, MIN_UPDATE_INTERVAL - timeSinceLastUpdate); } } while (true) { - const { done, value } = await reader.read() - if (done) break + const { done, value } = await reader.read(); + if (done) break; - const chunk = decoder.decode(value) - const lines = chunk.split('\n').filter(line => line.trim() !== '') + const chunk = decoder.decode(value); + const lines = chunk.split('\n').filter(line => line.trim() !== ''); for (const line of lines) { if (line.startsWith('data: ')) { - const data = line.slice(6) + const data = line.slice(6); if (data === '[DONE]') { - isStreaming = false - currentStreamController = null - updateMessageContent(contentDiv, accumulatedContent, markdownToggle.checked, 'assistant') + isStreaming = false; + currentStreamController = null; + updateMessageContent(contentDiv, accumulatedContent, markdownToggle.checked, 'assistant'); if (cursorSpan.parentNode) { - cursorSpan.parentNode.removeChild(cursorSpan) + cursorSpan.parentNode.removeChild(cursorSpan); } - chatLog.scrollTop = chatLog.scrollHeight - return + chatLog.scrollTop = chatLog.scrollHeight; + return; } try { - const parsed = JSON.parse(data) + const parsed = JSON.parse(data); if (parsed.choices && parsed.choices[0].delta.content) { - accumulatedContent += parsed.choices[0].delta.content - scheduleUpdate() + accumulatedContent += parsed.choices[0].delta.content; + scheduleUpdate(); } } catch (e) { - console.log('Error parsing stream data:', e) + console.log('Error parsing stream data:', e); } } } } - updateMessageContent(contentDiv, accumulatedContent, markdownToggle.checked, 'assistant') - chatLog.scrollTop = chatLog.scrollHeight + updateMessageContent(contentDiv, accumulatedContent, markdownToggle.checked, 'assistant'); + chatLog.scrollTop = chatLog.scrollHeight; } catch (error) { - console.error('Streaming error:', error) + console.error('Streaming error:', error); if (error.name !== 'AbortError') { - updateMessageContent(contentDiv, `Error: ${ error.message }`, false, 'assistant') + updateMessageContent(contentDiv, `Error: ${error.message}`, false, 'assistant'); } } finally { - isStreaming = false - currentStreamController = null - sendBtn.disabled = false + isStreaming = false; + currentStreamController = null; + sendBtn.disabled = false; - if (cursorSpan.parentNode) { - cursorSpan.parentNode.removeChild(cursorSpan) + if (cursorSpan && cursorSpan.parentNode) { + cursorSpan.parentNode.removeChild(cursorSpan); } } } async function handleNonStreamingResponse(userMessage) { + let typingIndicator = null; try { - const typingIndicator = showTypingIndicator() + typingIndicator = showTypingIndicator(); - if (!currentModel && availableModels.length > 0) { - currentModel = availableModels[0].id - document.getElementById('modelSelector').value = currentModel - } + const { backendKey, model } = splitBackendModel(currentBackendModel); + const selectedModel = model || availableBackendModels[0]?.model || 'local-model'; const requestBody = { - model : currentModel || 'local-model', - messages : getApiMessages(), - stream : false, + model: selectedModel, + messages: getApiMessages(), + stream: false, temperature: 0.7, - max_tokens : 2000 - } + max_tokens: 2000 + }; - const response = await fetch(`${ getApiUrl() }/chat/completions`, { - method : 'POST', + const response = await fetch(`${getApiUrl()}/chat/completions`, { + method: 'POST', headers: { - 'Content-Type' : 'application/json', - 'Authorization': `Bearer ${ API_KEY }` + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${API_KEY}` }, - body : JSON.stringify(requestBody) - }) + body: JSON.stringify(requestBody) + }); if (!response.ok) { - throw new Error(`API Error: ${ response.status } ${ response.statusText }`) + throw new Error(`API Error: ${response.status} ${response.statusText}`); } - const data = await response.json() - removeTypingIndicator(typingIndicator) + const data = await response.json(); + removeTypingIndicator(typingIndicator); if (data.choices && data.choices.length > 0) { - const assistantReply = data.choices[0].message.content - addMessage('assistant', assistantReply, markdownToggle.checked) + const assistantReply = data.choices[0].message.content; + addMessage('assistant', assistantReply, markdownToggle.checked); } else { - addMessage('assistant', 'No response generated.', false) + addMessage('assistant', 'No response generated.', false); } } catch (error) { - console.error('Error:', error) - removeTypingIndicator(typingIndicator) - addMessage('assistant', `Error: ${ error.message }`, false) + console.error('Error:', error); + if (typingIndicator) { + removeTypingIndicator(typingIndicator); + } + addMessage('assistant', `Error: ${error.message}`, false); } finally { - sendBtn.disabled = false - userInput.focus() + sendBtn.disabled = false; + userInput.focus(); } } function stopStream() { if (currentStreamController) { - currentStreamController.abort() - isStreaming = false - currentStreamController = null - sendBtn.disabled = false - sendBtn.textContent = 'Send' + currentStreamController.abort(); + isStreaming = false; + currentStreamController = null; + sendBtn.disabled = false; + sendBtn.textContent = 'Send'; } } async function sendMessage() { - const userMessage = userInput.value.trim() - if (!userMessage || isStreaming) return + const userMessage = userInput.value.trim(); + if (!userMessage || isStreaming) return; - addMessage('user', userMessage) - - userInput.value = '' - userInput.style.height = 'auto' - sendBtn.disabled = true - - if (streamToggle.checked) { - sendBtn.textContent = 'Stop' - await handleStreamingResponse(userMessage) - } else { - await handleNonStreamingResponse(userMessage) + // ✅ FIX: Validate backend selection + if (!currentBackendModel) { + addMessage('assistant', 'Please select a backend model first.', false); + return; } - sendBtn.textContent = 'Send' + addMessage('user', userMessage, false); + + userInput.value = ''; + userInput.style.height = 'auto'; + sendBtn.disabled = true; + + if (streamToggle.checked) { + sendBtn.textContent = 'Stop'; + await handleStreamingResponse(userMessage); + } else { + await handleNonStreamingResponse(userMessage); + } + + sendBtn.textContent = 'Send'; } sendBtn.addEventListener('click', () => { if (isStreaming) { - stopStream() + stopStream(); } else { - sendMessage() + sendMessage(); } -}) +}); userInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault() + e.preventDefault(); if (!isStreaming) { - sendMessage() + sendMessage(); } } -}) +}); +// ✅ Updated window.onload with better initialization window.onload = () => { - userInput.focus() - fetchModels() + userInput.focus(); - if (chatHistory.length === 0) { - document.getElementById('resetBtn').addEventListener('click', resetChat) + // Set default backend BEFORE fetching + if (!currentBackendModel) { + currentBackendModel = 'plato'; } -} + + fetchBackendModels().then(() => { + // Load chat history after models are fetched + if (chatLog) { + if (chatHistory.length === 0) { + //addMessage('assistant', welcomeMessage, true); + } else { + chatHistory.forEach(msg => { + if (typeof msg.content === 'string' && msg.content.trim()) { + addMessage(msg.role, msg.content, msg.markdown, msg.messageId); + } + }); + } + } + }); + + document.getElementById('resetBtn')?.addEventListener('click', resetChat); +}; function resetChat() { - chatHistory = [] - chatLog.innerHTML = welcomeMessage + chatHistory = []; + localStorage.setItem('chatHistory', JSON.stringify(chatHistory)); - localStorage.setItem('chatHistory', JSON.stringify(chatHistory)) -} + if (chatLog) { + chatLog.innerHTML = ''; + addMessage('assistant', welcomeMessage, true); + } +} \ No newline at end of file diff --git a/index.html b/index.html index e653d32..e6a8dd0 100644 --- a/index.html +++ b/index.html @@ -23,33 +23,56 @@ - -
- -
+
+ +
+
?
Assistant

Welcome to LM Studio Chat!

+

I now support full conversation history, real-time streaming responses and dark, readable text formatting.

+

Features:

+
    +
  1. Full Chat History - Complete conversation context is now sent to the AI
  2. +
  3. Smart History Management - Automatically keeps the last 50 messages
  4. +
  5. Streaming Mode (enabled by default) - Watch responses appear word-by-word
  6. +
  7. Markdown Rendering - Proper formatting for code, lists, tables, and more
  8. +
  9. Readable Dark Text - No more eye strain from light gray text
  10. +
+

Try it out:

+
    +
  • Ask follow-up questions that reference earlier messages
  • +
  • Have a multi-turn conversation with full context
  • +
  • Ask "what did I just ask?" to test history retention
  • +
  • Watch the streaming response in real-time!
  • +
+
+

Tip: You can toggle streaming and markdown using the checkboxes below.

+
+
+
+ +
-
-
-
- - -
-
Model: unknown
+
+
+
+ +
- - +
Model: unknown
+ +
+
diff --git a/style.css b/style.css index f3ca099..827c085 100644 --- a/style.css +++ b/style.css @@ -9,7 +9,6 @@ body { #chatContainer { display: flex; flex-direction: column; - height: 95vh; background: white; border-radius: 10px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);