diff --git a/chatv2.js b/chatv2.js index 96f51bc..32e223c 100644 --- a/chatv2.js +++ b/chatv2.js @@ -1,35 +1,32 @@ -// Configuration const BACKENDS = { plato : { prod: '/api/plato', - dev : 'http://192.168.1.74:1234/v1', // Removed trailing space + dev : 'http://192.168.1.74:1234/v1', name: 'Plato (192.168.1.74)' }, stoic : { prod: '/api/stoic', - dev : 'http://192.168.1.159:1234/v1', // Removed trailing space + 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', // Removed trailing space + dev : 'http://192.168.1.159:8081/v1', name: 'Ollama (192.168.1.159:8081)' } } const IS_PRODUCTION = window.location.hostname === 'jarvis-lan.appmodel.nl' const API_KEY = 'not-needed' -const MAX_CHAT_HISTORY = 50 // Maximum messages to keep in history +const MAX_CHAT_HISTORY = 50 -// Global state let currentBackend = 'plato' let currentModel = null let availableModels = [] let currentStreamController = null let isStreaming = false -let chatHistory = JSON.parse(localStorage.getItem('chatHistory')) || [] // Stores conversation history +let chatHistory = JSON.parse(localStorage.getItem('chatHistory')) || [] -// Ensure chatHistory is an array of objects with the correct structure chatHistory = chatHistory.map(message => ({ role : message.role || 'user', content : message.content || '', @@ -37,13 +34,11 @@ chatHistory = chatHistory.map(message => ({ messageId: message.messageId || `msg-${ Date.now() }` })) -// Get current API URL based on selected backend function getApiUrl() { const backend = BACKENDS[currentBackend] return IS_PRODUCTION ? backend.prod : backend.dev } -// Update backend display function updateBackendDisplay() { const backend = BACKENDS[currentBackend] const displayText = IS_PRODUCTION @@ -52,7 +47,6 @@ function updateBackendDisplay() { document.getElementById('backendDisplay').textContent = displayText } -// Fetch available models from backend async function fetchModels() { try { const response = await fetch(`${ getApiUrl() }/models`) @@ -61,7 +55,6 @@ async function fetchModels() { availableModels = data.data || [] populateModelSelector() - // Auto-select first model if none selected if (!currentModel && availableModels.length > 0) { currentModel = availableModels[0].id document.getElementById('modelSelector').value = currentModel @@ -76,7 +69,6 @@ async function fetchModels() { } } -// Populate model selector dropdown function populateModelSelector() { const selector = document.getElementById('modelSelector') if (availableModels.length === 0) { @@ -93,26 +85,20 @@ function populateModelSelector() { } } -// Get formatted messages for API (excludes UI-specific properties) function getApiMessages() { - // Get last N messages and format for API return chatHistory.slice(-MAX_CHAT_HISTORY).map(({ role, content }) => ({ role, content })) } -// Trim chat history if it exceeds maximum function trimChatHistory() { if (chatHistory.length > MAX_CHAT_HISTORY) { chatHistory = chatHistory.slice(-MAX_CHAT_HISTORY) - console.log(`Chat history trimmed to ${ MAX_CHAT_HISTORY } messages`) - // Save chat history to localStorage localStorage.setItem('chatHistory', JSON.stringify(chatHistory)) } } -// Handle backend and model selection changes document.addEventListener('DOMContentLoaded', () => { const backendSelector = document.getElementById('backendSelector') const modelSelector = document.getElementById('modelSelector') @@ -124,17 +110,14 @@ document.addEventListener('DOMContentLoaded', () => { backendSelector.addEventListener('change', (e) => { currentBackend = e.target.value updateBackendDisplay() - console.log('Backend switched to:', currentBackend, '?', getApiUrl()) - fetchModels() // Reload models for new backend + fetchModels() }) modelSelector.addEventListener('change', (e) => { currentModel = e.target.value - console.log('Model selected:', currentModel) }) }) -// DOM Elements const chatLog = document.getElementById('chatLog') const userInput = document.getElementById('userInput') const sendBtn = document.getElementById('sendBtn') @@ -142,7 +125,6 @@ const streamToggle = document.getElementById('streamToggle') const markdownToggle = document.getElementById('markdownToggle') const modelNameSpan = document.getElementById('modelName') -// Initialize marked with options marked.setOptions({ gfm : true, breaks : true, @@ -155,13 +137,11 @@ marked.setOptions({ } }) -// Auto-resize textarea function autoResize(textarea) { textarea.style.height = 'auto' textarea.style.height = Math.min(textarea.scrollHeight, 300) + 'px' } -// Add message to chat function addMessage(role, content, markdown = false, messageId = null) { const messageDiv = document.createElement('div') messageDiv.className = `message message-${ role }` @@ -186,10 +166,8 @@ function addMessage(role, content, markdown = false, messageId = null) { if (markdown && role === 'assistant' && markdownToggle.checked) { contentDiv.className = 'markdown-content' - // Parse thinking tags before markdown const processedContent = parseThinkingTags(content) contentDiv.innerHTML = marked.parse(processedContent) - // Apply syntax highlighting if hljs is available if (window.hljs) { setTimeout(() => { contentDiv.querySelectorAll('pre code').forEach((block) => { @@ -198,7 +176,6 @@ function addMessage(role, content, markdown = false, messageId = null) { }, 0) } } else { - // Parse thinking tags for non-markdown too const processedContent = parseThinkingTags(content) contentDiv.innerHTML = processedContent contentDiv.style.whiteSpace = 'pre-wrap' @@ -211,24 +188,17 @@ function addMessage(role, content, markdown = false, messageId = null) { chatLog.appendChild(messageDiv) chatLog.scrollTop = chatLog.scrollHeight - // Store message in history (excluding UI-specific properties) chatHistory.push({ role, content, markdown, messageId }) - - // Trim history if it gets too long trimChatHistory() - - // Save chat history to localStorage localStorage.setItem('chatHistory', JSON.stringify(chatHistory)) return contentDiv } -// Parse [THINK] tags and create collapsible sections function parseThinkingTags(content) { let result = content let thinkingCounter = 0 - // Handle complete [THINK]...[/THINK] blocks const completeThinkRegex = /\[THINK\]([\s\S]*?)\[\/THINK\]/gi result = result.replace(completeThinkRegex, (match, thinkContent) => { thinkingCounter++ @@ -242,7 +212,6 @@ function parseThinkingTags(content) { ` }) - // Handle incomplete [THINK] blocks (still streaming) const incompleteThinkRegex = /\[THINK\]([\s\S]*?)$/gi result = result.replace(incompleteThinkRegex, (match, thinkContent) => { thinkingCounter++ @@ -259,7 +228,6 @@ function parseThinkingTags(content) { return result } -// Toggle thinking section visibility window.toggleThinking = function(id) { const content = document.getElementById(id) const toggle = document.getElementById(id + '-toggle') @@ -270,14 +238,11 @@ window.toggleThinking = function(id) { } } -// Update message content (for streaming) function updateMessageContent(contentDiv, newContent, markdown = false, role) { - // Parse thinking tags first const processedContent = parseThinkingTags(newContent) if (markdown && role === 'assistant' && markdownToggle.checked) { contentDiv.innerHTML = marked.parse(processedContent) - // Apply syntax highlighting if hljs is available if (window.hljs) { setTimeout(() => { contentDiv.querySelectorAll('pre code').forEach((block) => { @@ -290,7 +255,6 @@ function updateMessageContent(contentDiv, newContent, markdown = false, role) { } } -// Show typing indicator function showTypingIndicator() { const typingDiv = document.createElement('div') typingDiv.id = 'typingIndicator' @@ -306,7 +270,7 @@ function showTypingIndicator() { const name = document.createElement('span') name.textContent = 'Assistant' - headerDiv.appendChild(avatar) + headerDiv.appendChild(abatar) headerDiv.appendChild(name) const dotsDiv = document.createElement('div') @@ -326,20 +290,17 @@ function showTypingIndicator() { return typingDiv } -// Remove typing indicator function removeTypingIndicator(typingDiv) { if (typingDiv && typingDiv.parentNode) { typingDiv.parentNode.removeChild(typingDiv) } } -// Handle streaming response async function handleStreamingResponse(userMessage) { const messageId = 'msg-' + Date.now() let accumulatedContent = '' let contentDiv = null - // Create initial empty message const messageDiv = document.createElement('div') messageDiv.className = 'message message-assistant' messageDiv.id = messageId @@ -364,13 +325,11 @@ async function handleStreamingResponse(userMessage) { messageDiv.appendChild(contentDiv) chatLog.appendChild(messageDiv) - // Add streaming cursor const cursorSpan = document.createElement('span') cursorSpan.className = 'streaming-cursor' contentDiv.appendChild(cursorSpan) try { - // Auto-select first model if none selected if (!currentModel && availableModels.length > 0) { currentModel = availableModels[0].id document.getElementById('modelSelector').value = currentModel @@ -378,7 +337,7 @@ async function handleStreamingResponse(userMessage) { const requestBody = { model : currentModel || 'local-model', - messages : getApiMessages(), // Send full chat history + messages : getApiMessages(), stream : true, temperature: 0.7, max_tokens : 2000 @@ -403,10 +362,9 @@ async function handleStreamingResponse(userMessage) { isStreaming = true currentStreamController = new AbortController() - // Debounce DOM updates to reduce stutter let updateScheduled = false let lastUpdate = Date.now() - const MIN_UPDATE_INTERVAL = 100 // ms, update at most every 100ms + const MIN_UPDATE_INTERVAL = 100 function scheduleUpdate() { if (updateScheduled) return @@ -415,7 +373,6 @@ async function handleStreamingResponse(userMessage) { const timeSinceLastUpdate = now - lastUpdate if (timeSinceLastUpdate >= MIN_UPDATE_INTERVAL) { - // Update immediately if enough time has passed updateScheduled = true requestAnimationFrame(() => { updateMessageContent(contentDiv, accumulatedContent, markdownToggle.checked, 'assistant') @@ -427,7 +384,6 @@ async function handleStreamingResponse(userMessage) { updateScheduled = false }) } else { - // Schedule update after remaining time updateScheduled = true setTimeout(() => { requestAnimationFrame(() => { @@ -457,7 +413,6 @@ async function handleStreamingResponse(userMessage) { if (data === '[DONE]') { isStreaming = false currentStreamController = null - // Final update and remove cursor updateMessageContent(contentDiv, accumulatedContent, markdownToggle.checked, 'assistant') if (cursorSpan.parentNode) { cursorSpan.parentNode.removeChild(cursorSpan) @@ -479,7 +434,6 @@ async function handleStreamingResponse(userMessage) { } } - // Final update updateMessageContent(contentDiv, accumulatedContent, markdownToggle.checked, 'assistant') chatLog.scrollTop = chatLog.scrollHeight @@ -493,66 +447,60 @@ async function handleStreamingResponse(userMessage) { currentStreamController = null sendBtn.disabled = false - // Remove cursor if still exists if (cursorSpan.parentNode) { cursorSpan.parentNode.removeChild(cursorSpan) } } } -// Handle non-streaming response async function handleNonStreamingResponse(userMessage) { const typingIndicator = showTypingIndicator() - try { - // Auto-select first model if none selected - if (!currentModel && availableModels.length > 0) { - currentModel = availableModels[0].id - document.getElementById('modelSelector').value = currentModel - } - - const requestBody = { - model : currentModel || 'local-model', - messages : getApiMessages(), // Send full chat history - stream : false, - temperature: 0.7, - max_tokens : 2000 - } - - const response = await fetch(`${ getApiUrl() }/chat/completions`, { - method : 'POST', - headers: { - 'Content-Type' : 'application/json', - 'Authorization': `Bearer ${ API_KEY }` - }, - body : JSON.stringify(requestBody) - }) - - if (!response.ok) { - throw new Error(`API Error: ${ response.status } ${ response.statusText }`) - } - - 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) - } else { - addMessage('assistant', 'No response generated.', false) - } - - } catch (error) { - console.error('Error:', error) - removeTypingIndicator(typingIndicator) - addMessage('assistant', `Error: ${ error.message }`, false) - } finally { - sendBtn.disabled = false - userInput.focus() + if (!currentModel && availableModels.length > 0) { + currentModel = availableModels[0].id + document.getElementById('modelSelector').value = currentModel } + + const requestBody = { + model : currentModel || 'local-model', + messages : getApiMessages(), + stream : false, + temperature: 0.7, + max_tokens : 2000 + } + + const response = await fetch(`${ getApiUrl() }/chat/completions`, { + method : 'POST', + headers: { + 'Content-Type' : 'application/json', + 'Authorization': `Bearer ${ API_KEY }` + }, + body : JSON.stringify(requestBody) + }) + + if (!response.ok) { + throw new Error(`API Error: ${ response.status } ${ response.statusText }`) + } + + 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) + } else { + addMessage('assistant', 'No response generated.', false) + } + +} catch (error) { + console.error('Error:', error) + removeTypingIndicator(typingIndicator) + addMessage('assistant', `Error: ${ error.message }`, false) +} finally { + sendBtn.disabled = false + userInput.focus() } -// Stop current stream function stopStream() { if (currentStreamController) { currentStreamController.abort() @@ -563,20 +511,16 @@ function stopStream() { } } -// Send message async function sendMessage() { const userMessage = userInput.value.trim() if (!userMessage || isStreaming) return - // Add user message to UI and history addMessage('user', userMessage) - // Clear input field userInput.value = '' userInput.style.height = 'auto' sendBtn.disabled = true - // Update button text based on streaming state if (streamToggle.checked) { sendBtn.textContent = 'Stop' await handleStreamingResponse(userMessage) @@ -584,11 +528,9 @@ async function sendMessage() { await handleNonStreamingResponse(userMessage) } - // Reset button text sendBtn.textContent = 'Send' } -// Event Listeners sendBtn.addEventListener('click', () => { if (isStreaming) { stopStream() @@ -606,49 +548,18 @@ userInput.addEventListener('keydown', (e) => { } }) -// Initialize on load window.onload = () => { userInput.focus() fetchModels() - // Load chat history from localStorage if (chatHistory.length === 0) { - - // Add event listener for reset button document.getElementById('resetBtn').addEventListener('click', resetChat) } } -// Reset chat history function resetChat() { - // Clear chat history chatHistory = [] - - // Clear chat log chatLog.innerHTML = '' - // Add welcome message - const welcomeMessage = `# 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. **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 - -## 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.*` - - addMessage('assistant', welcomeMessage, true) - - // Save chat history to localStorage localStorage.setItem('chatHistory', JSON.stringify(chatHistory)) }