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 `
- `
- })
+
${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 `
- `
- })
+
${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 @@
-
-
-
-
+
+
+
+
Welcome to LM Studio Chat!
+
I now support full conversation history, real-time streaming responses and dark, readable text formatting.
+
Features:
+
+ - Full Chat History - Complete conversation context is now sent to the AI
+ - Smart History Management - Automatically keeps the last 50 messages
+ - Streaming Mode (enabled by default) - Watch responses appear word-by-word
+ - Markdown Rendering - Proper formatting for code, lists, tables, and more
+ - Readable Dark Text - No more eye strain from light gray text
+
+
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.
+
+
+
+
+