📜
chat.js
Back
📝 Javascript ⚡ Executable Ctrl+S: Save • Ctrl+R: Run • Ctrl+F: Find
// chat.js - Clean AI Chat Interface with Active File + File Context (function () { console.log("[chat] Loading AI Chat interface (clean)…"); // --- Utility Functions --- function escapeHtml(text) { const div = document.createElement("div"); div.textContent = text; return div.innerHTML; } function generateSessionId() { return ( "session_" + Date.now() + "_" + Math.random().toString(36).substr(2, 9) ); } // --- Get API configuration from Settings module --- function getApiConfig() { if (window.Settings) { const settings = window.Settings.get(); return { endpoint: window.Settings.getApiEndpoint(), defaultModel: settings.defaultModel, maxTokens: settings.maxTokens, temperature: settings.temperature, chatPrompt: settings.chatPrompt, snippetsPrompt: settings.snippetsPrompt, fullCodePrompt: settings.fullCodePrompt, responseMode: settings.responseMode, currentMode: settings.currentMode, }; } // Fallback if Settings not loaded return { endpoint: "api.php", defaultModel: "grok-code-fast-1", maxTokens: 2000, temperature: 0.7, chatPrompt: "You are a helpful AI assistant for web development.", snippetsPrompt: "You are an expert at writing small, focused code snippets. Provide concise, working code examples.", fullCodePrompt: "You are an expert full-stack developer. Provide complete, production-ready code solutions.", responseMode: "normal", currentMode: "chat", }; } let currentModel = getApiConfig().defaultModel; let sessionId = generateSessionId(); // Listen for settings updates window.addEventListener("settingsUpdated", (e) => { const newSettings = e.detail; currentModel = newSettings.defaultModel; console.log("[chat] Settings updated, new default model:", currentModel); }); // --- API Call Function --- async function callAI(question, systemMessage, conversationHistory) { const config = getApiConfig(); const modelConfig = window.Settings ? window.Settings.getModelConfig(currentModel) : { provider: "unknown", name: currentModel }; try { const response = await fetch(config.endpoint, { method: "POST", headers: { "Content-Type": "application/json", "X-Session-Id": sessionId, "X-Username": "user", // Make dynamic if you add auth }, body: JSON.stringify({ question: question, model: currentModel, system: systemMessage, sessionId: sessionId, maxTokens: config.maxTokens, temperature: config.temperature, }), }); if (!response.ok) { throw new Error("API request failed: " + response.status); } const data = await response.json(); if (data.error) { throw new Error(data.error); } return { answer: data.answer || "(no response)", usage: data.usage, provider: data.provider, model: data.model, }; } catch (error) { console.error("[chat] API Error:", error); throw error; } } // --- File Context Functions --- function getFileContext() { if (!window.FilesManager) { console.warn("[chat] FilesManager not available"); return { active: null, read: [] }; } const files = window.FilesManager.getFiles(); const activeFile = files.find((f) => f.active); const readFiles = files.filter((f) => f.read); return { active: activeFile, read: readFiles, }; } function buildFileContextMessage() { const fileContext = getFileContext(); let contextMessage = ""; if (fileContext.active) { contextMessage += "\n\n## ACTIVE FILE (Primary Context)\n"; contextMessage += "File: " + fileContext.active.name + "\n"; contextMessage += "Path: " + fileContext.active.path + "\n"; contextMessage += "```\n" + fileContext.active.content + "\n```"; } if (fileContext.read.length > 0) { contextMessage += "\n\n## READ FILES (Additional Context)\n"; fileContext.read.forEach(function (file) { contextMessage += "\nFile: " + file.name + "\n"; contextMessage += "Path: " + file.path + "\n"; contextMessage += "```\n" + file.content + "\n```\n"; }); } return contextMessage; } // --- File Context Badge Display --- function renderFileContextBadge() { const fileContext = getFileContext(); if (!fileContext.active && fileContext.read.length === 0) { return `<div style="color: #666; font-size: 12px; margin-bottom: 12px;"> 📎 No files in context </div>`; } let badgeHTML = '<div style="margin-bottom: 12px; padding: 8px 12px; background: rgba(139, 92, 246, 0.1); border: 1px solid #8b5cf6; border-radius: 6px;">'; badgeHTML += '<div style="color: #c4b5fd; font-weight: 700; margin-bottom: 6px; font-size: 12px;">📎 FILE CONTEXT</div>'; if (fileContext.active) { badgeHTML += '<div style="font-size: 12px; margin-bottom: 4px;">'; badgeHTML += '<span style="color: #16a34a; font-weight: 700;">🟢</span> '; badgeHTML += '<span style="color: #e6edf3; font-family: monospace;">' + escapeHtml(fileContext.active.name) + "</span>"; badgeHTML += "</div>"; } if (fileContext.read.length > 0) { fileContext.read.forEach(function (file) { badgeHTML += '<div style="font-size: 11px; margin-left: 8px; color: #9ca3af;">'; badgeHTML += '<span style="color: #3b82f6;">🔵</span> '; badgeHTML += '<span style="font-family: monospace;">' + escapeHtml(file.name) + "</span>"; badgeHTML += "</div>"; }); } badgeHTML += "</div>"; return badgeHTML; } // --- Message Display Functions (no scopes) --- function addUserMessage( messagesContainer, message, messagesPlaceholder, conversationHistory ) { if (messagesPlaceholder && messagesPlaceholder.parentNode) { messagesPlaceholder.remove(); } const wrapper = document.createElement("div"); wrapper.style.cssText = ` margin-bottom: 16px; display: flex; justify-content: flex-end; align-items: flex-start; gap: 8px; `; wrapper.dataset.role = "user"; wrapper.dataset.includedInContext = "true"; const userMsg = document.createElement("div"); userMsg.className = "message-content"; userMsg.style.cssText = ` padding: 12px 16px; background: #1e3a5f; border-radius: 8px; max-width: 80%; color: #e0e0e0; font-size: 14px; line-height: 1.5; `; userMsg.innerHTML = escapeHtml(message); // Controls const controls = document.createElement("div"); controls.style.cssText = ` display: flex; flex-direction: column; gap: 4px; padding-top: 4px; `; const eyeBtn = document.createElement("button"); eyeBtn.className = "msg-eye-btn"; eyeBtn.innerHTML = "👁️"; eyeBtn.title = "Hide from context"; eyeBtn.style.cssText = ` width: 32px; height: 32px; background: #374151; border: 1px solid #4b5563; border-radius: 4px; cursor: pointer; font-size: 16px; transition: all 0.2s; padding: 0; display: flex; align-items: center; justify-content: center; `; const deleteBtn = document.createElement("button"); deleteBtn.innerHTML = "❌"; deleteBtn.title = "Delete message"; deleteBtn.style.cssText = ` width: 32px; height: 32px; background: #374151; border: 1px solid #4b5563; border-radius: 4px; cursor: pointer; font-size: 16px; transition: all 0.2s; padding: 0; display: flex; align-items: center; justify-content: center; `; // Eye button toggle eyeBtn.addEventListener("click", () => { const isHidden = wrapper.dataset.includedInContext === "false"; if (isHidden) { wrapper.dataset.includedInContext = "true"; userMsg.style.opacity = "1"; eyeBtn.innerHTML = "👁️"; eyeBtn.title = "Hide from context"; } else { wrapper.dataset.includedInContext = "false"; userMsg.style.opacity = "0.4"; eyeBtn.innerHTML = "🙈"; eyeBtn.title = "Show in context"; } updateConversationHistory(messagesContainer, conversationHistory); }); // Delete button deleteBtn.addEventListener("click", () => { if (confirm("Delete this message?")) { wrapper.remove(); updateConversationHistory(messagesContainer, conversationHistory); } }); // Hover effects eyeBtn.addEventListener("mouseenter", () => { eyeBtn.style.background = "#4b5563"; }); eyeBtn.addEventListener("mouseleave", () => { eyeBtn.style.background = "#374151"; }); deleteBtn.addEventListener("mouseenter", () => { deleteBtn.style.background = "#ef4444"; deleteBtn.style.borderColor = "#dc2626"; }); deleteBtn.addEventListener("mouseleave", () => { deleteBtn.style.background = "#374151"; deleteBtn.style.borderColor = "#4b5563"; }); controls.appendChild(eyeBtn); controls.appendChild(deleteBtn); wrapper.appendChild(userMsg); wrapper.appendChild(controls); messagesContainer.appendChild(wrapper); messagesContainer.scrollTop = messagesContainer.scrollHeight; } function addAIMessage( messagesContainer, message, conversationHistory, isStreaming ) { const wrapper = document.createElement("div"); wrapper.style.cssText = ` margin-bottom: 16px; display: flex; justify-content: flex-start; align-items: flex-start; gap: 8px; `; wrapper.dataset.role = "assistant"; wrapper.dataset.includedInContext = "true"; // Controls const controls = document.createElement("div"); controls.style.cssText = ` display: flex; flex-direction: column; gap: 4px; padding-top: 4px; `; const eyeBtn = document.createElement("button"); eyeBtn.className = "msg-eye-btn"; eyeBtn.innerHTML = "👁️"; eyeBtn.title = "Hide from context"; eyeBtn.style.cssText = ` width: 32px; height: 32px; background: #374151; border: 1px solid #4b5563; border-radius: 4px; cursor: pointer; font-size: 16px; transition: all 0.2s; padding: 0; display: flex; align-items: center; justify-content: center; `; const deleteBtn = document.createElement("button"); deleteBtn.innerHTML = "❌"; deleteBtn.title = "Delete message"; deleteBtn.style.cssText = ` width: 32px; height: 32px; background: #374151; border: 1px solid #4b5563; border-radius: 4px; cursor: pointer; font-size: 16px; transition: all 0.2s; padding: 0; display: flex; align-items: center; justify-content: center; `; const aiMsg = document.createElement("div"); aiMsg.className = "message-content"; aiMsg.style.cssText = ` padding: 12px 16px; background: #2a2a2a; border-radius: 8px; max-width: 80%; color: #e0e0e0; font-size: 14px; line-height: 1.5; white-space: pre-wrap; word-wrap: break-word; `; if (isStreaming) { aiMsg.innerHTML = '<span style="color:#888;">🤖 Thinking...</span>'; } else { aiMsg.innerHTML = escapeHtml(message || ""); } // Eye button toggle eyeBtn.addEventListener("click", () => { const isHidden = wrapper.dataset.includedInContext === "false"; if (isHidden) { wrapper.dataset.includedInContext = "true"; aiMsg.style.opacity = "1"; eyeBtn.innerHTML = "👁️"; eyeBtn.title = "Hide from context"; } else { wrapper.dataset.includedInContext = "false"; aiMsg.style.opacity = "0.4"; eyeBtn.innerHTML = "🙈"; eyeBtn.title = "Show in context"; } updateConversationHistory(messagesContainer, conversationHistory); }); // Delete button deleteBtn.addEventListener("click", () => { if (confirm("Delete this message?")) { wrapper.remove(); updateConversationHistory(messagesContainer, conversationHistory); } }); // Hover effects eyeBtn.addEventListener("mouseenter", () => { eyeBtn.style.background = "#4b5563"; }); eyeBtn.addEventListener("mouseleave", () => { eyeBtn.style.background = "#374151"; }); deleteBtn.addEventListener("mouseenter", () => { deleteBtn.style.background = "#ef4444"; deleteBtn.style.borderColor = "#dc2626"; }); deleteBtn.addEventListener("mouseleave", () => { deleteBtn.style.background = "#374151"; deleteBtn.style.borderColor = "#4b5563"; }); controls.appendChild(eyeBtn); controls.appendChild(deleteBtn); wrapper.appendChild(controls); wrapper.appendChild(aiMsg); messagesContainer.appendChild(wrapper); messagesContainer.scrollTop = messagesContainer.scrollHeight; return aiMsg; } // --- Update Conversation History from DOM --- function updateConversationHistory(messagesContainer, conversationHistory) { conversationHistory.length = 0; const wrappers = messagesContainer.querySelectorAll("[data-role]"); wrappers.forEach((wrapper) => { if (wrapper.dataset.includedInContext === "true") { const role = wrapper.dataset.role; const content = wrapper .querySelector(".message-content") .textContent.trim(); let cleanContent = content; if (role === "assistant") { cleanContent = content.replace(/^🤖\s+/, ""); } conversationHistory.push({ role: role, content: cleanContent, }); } }); console.log( "[chat] Updated conversation history:", conversationHistory.length, "messages" ); } // --- Tab Switching --- function switchTab(tabName, tabs, contents) { const [chatTab, activeFileTab, filesTab, settingsTab] = tabs; const [chatContent, activeFileContent, filesContent, settingsContent] = contents; tabs.forEach((tab) => { tab.style.background = "#1a1a1a"; tab.style.borderBottomColor = "transparent"; tab.style.color = "#666"; }); contents.forEach((content) => { content.style.display = "none"; }); if (tabName === "chat") { chatTab.style.background = "#2a2a2a"; chatTab.style.borderBottomColor = "#16a34a"; chatTab.style.color = "#fff"; chatContent.style.display = "flex"; } else if (tabName === "activeFile") { activeFileTab.style.background = "#2a2a2a"; activeFileTab.style.borderBottomColor = "#3b82f6"; activeFileTab.style.color = "#fff"; activeFileContent.style.display = "block"; } else if (tabName === "files") { filesTab.style.background = "#2a2a2a"; filesTab.style.borderBottomColor = "#8b5cf6"; filesTab.style.color = "#fff"; filesContent.style.display = "block"; } else if (tabName === "settings") { settingsTab.style.background = "#2a2a2a"; settingsTab.style.borderBottomColor = "#f59e0b"; settingsTab.style.color = "#fff"; settingsContent.style.display = "block"; } } // --- AI Chat Slide Configuration --- const aiChatSlide = { title: "AI Chat", html: ` <div style="display: flex; flex-direction: column; height: 100%;"> <!-- Tab Navigation --> <div style=" display: flex; background: #1a1a1a; border-bottom: 2px solid #2a2a2a; "> <button id="chatTab" class="ai-tab active" style=" flex: 1; padding: 14px 20px; background: #2a2a2a; border: none; border-bottom: 3px solid #16a34a; color: #fff; cursor: pointer; font-size: 14px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; transition: all 0.2s; ">💬 Chat</button> <button id="activeFileTab" class="ai-tab" style=" flex: 1; padding: 14px 20px; background: #1a1a1a; border: none; border-bottom: 3px solid transparent; color: #666; cursor: pointer; font-size: 14px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; transition: all 0.2s; ">📄 Active File</button> <button id="filesTab" class="ai-tab" style=" flex: 1; padding: 14px 20px; background: #1a1a1a; border: none; border-bottom: 3px solid transparent; color: #666; cursor: pointer; font-size: 14px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; transition: all 0.2s; ">📁 Files</button> <button id="settingsTab" class="ai-tab" style=" flex: 1; padding: 14px 20px; background: #1a1a1a; border: none; border-bottom: 3px solid transparent; color: #666; cursor: pointer; font-size: 14px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; transition: all 0.2s; ">⚙️ Settings</button> </div> <!-- Tab Content Container --> <div style="flex: 1; overflow: hidden; display: flex; flex-direction: column;"> <!-- Chat Tab Content --> <div id="chatContent" class="tab-content" style=" flex: 1; display: flex; flex-direction: column; "> <div id="aiChatMessages" style=" flex: 1; overflow-y: auto; padding: 20px; background: #0a0a0a; "> <div id="fileContextBadge"></div> <div id="aiChatMessagesPlaceholder" style="color: #666; text-align: center; padding: 40px;"> 💬 No messages yet. Start a conversation! </div> </div> <div style=" padding: 16px; background: #1a1a1a; border-top: 1px solid #2a2a2a; "> <!-- Message Input --> <div style="display: flex; gap: 8px;"> <textarea id="aiChatInput" placeholder="Type your message..." style=" flex: 1; padding: 12px; background: #2a2a2a; border: 1px solid #3a3a3a; border-radius: 4px; color: #e0e0e0; font-size: 14px; font-family: 'Segoe UI', sans-serif; resize: vertical; min-height: 60px; max-height: 200px; " ></textarea> <button id="aiChatSend" style=" padding: 12px 24px; background: #16a34a; border: 1px solid #15803d; border-radius: 4px; color: #fff; cursor: pointer; font-size: 14px; font-weight: 600; align-self: flex-end; transition: all 0.2s; " >Send</button> </div> </div> </div> <!-- Active File Tab Content --> <div id="activeFileContent" class="tab-content" style=" flex: 1; overflow-y: auto; padding: 20px; background: #0a0a0a; display: none; "> <div id="aiChatActiveFileContainer"></div> </div> <!-- Files Tab Content --> <div id="filesContent" class="tab-content" style=" flex: 1; overflow-y: auto; padding: 20px; background: #0a0a0a; display: none; "> <div id="aiChatFilesContainer"></div> </div> <!-- Settings Tab Content --> <div id="settingsContent" class="tab-content" style=" flex: 1; overflow-y: auto; padding: 20px; background: #0a0a0a; display: none; "> <div style="padding: 40px; text-align: center; color: #666;"> Click the Settings tab to open settings... </div> </div> </div> </div> `, onRender(el) { console.log("[chat] Rendering AI chat interface with tabs"); const chatTab = el.querySelector("#chatTab"); const activeFileTab = el.querySelector("#activeFileTab"); const filesTab = el.querySelector("#filesTab"); const settingsTab = el.querySelector("#settingsTab"); const chatContent = el.querySelector("#chatContent"); const activeFileContent = el.querySelector("#activeFileContent"); const filesContent = el.querySelector("#filesContent"); const settingsContent = el.querySelector("#settingsContent"); const messagesContainer = el.querySelector("#aiChatMessages"); const activeFileContainer = el.querySelector( "#aiChatActiveFileContainer" ); const filesContainer = el.querySelector("#aiChatFilesContainer"); const messagesPlaceholder = el.querySelector( "#aiChatMessagesPlaceholder" ); const fileContextBadge = el.querySelector("#fileContextBadge"); const input = el.querySelector("#aiChatInput"); const sendBtn = el.querySelector("#aiChatSend"); let conversationHistory = []; let isProcessing = false; // File context badge const updateFileContextBadge = function () { if (fileContextBadge) { fileContextBadge.innerHTML = renderFileContextBadge(); } }; // Initial file context updateFileContextBadge(); // Tab switching const tabs = [chatTab, activeFileTab, filesTab, settingsTab]; const contents = [ chatContent, activeFileContent, filesContent, settingsContent, ]; chatTab.addEventListener("click", () => switchTab("chat", tabs, contents) ); activeFileTab.addEventListener("click", () => switchTab("activeFile", tabs, contents) ); filesTab.addEventListener("click", () => switchTab("files", tabs, contents) ); settingsTab.addEventListener("click", () => { // Open Settings overlay if (window.Settings && window.Settings.open) { window.Settings.open(); } else { alert( "Settings module not loaded. Make sure settings.js is included." ); } }); // Tab hover effects tabs.forEach((tab) => { tab.addEventListener("mouseenter", () => { if (tab.style.background === "rgb(26, 26, 26)") { tab.style.background = "#242424"; } }); tab.addEventListener("mouseleave", () => { if (tab.style.background === "rgb(36, 36, 36)") { tab.style.background = "#1a1a1a"; } }); }); // Function to update active file display const updateActiveFile = () => { if (activeFileContainer && window.ActiveFileDisplay) { window.ActiveFileDisplay.render(activeFileContainer); } updateFileContextBadge(); }; // Function to render files list const updateFilesList = () => { if (filesContainer && window.FilesManager) { window.FilesManager.render(filesContainer, updateActiveFile); } }; // Initial renders updateActiveFile(); updateFilesList(); // Listen for file changes window.addEventListener("activeFilesUpdated", () => { updateActiveFile(); updateFilesList(); }); // Send message handler const sendMessage = async () => { const message = input.value.trim(); if (!message || isProcessing) return; isProcessing = true; input.value = ""; input.disabled = true; sendBtn.disabled = true; sendBtn.textContent = "⏳ Sending..."; console.log("[chat] User message:", message); console.log("[chat] Model:", currentModel); // Add user message addUserMessage( messagesContainer, message, messagesPlaceholder, conversationHistory ); // Create AI message placeholder const aiMsgElement = addAIMessage( messagesContainer, "", conversationHistory, true ); try { const config = getApiConfig(); // Build system message let systemMessage = ""; // Mode-based prompt if (config.currentMode === "chat") { systemMessage = config.chatPrompt; } else if (config.currentMode === "snippets") { systemMessage = config.snippetsPrompt; } else if (config.currentMode === "fullcode") { systemMessage = config.fullCodePrompt; } // Response mode (raw vs normal) if (config.responseMode === "raw") { systemMessage += "\n\nIMPORTANT: Return ONLY the requested code with NO explanations, comments, or markdown formatting. Just pure code."; } const fileContextMessage = buildFileContextMessage(); if (fileContextMessage) { systemMessage += fileContextMessage; } // Call API const result = await callAI( message, systemMessage, conversationHistory ); const cleanedAnswer = result.answer || ""; aiMsgElement.innerHTML = escapeHtml(cleanedAnswer); // Usage info if (result.usage) { const usageDiv = document.createElement("div"); usageDiv.style.cssText = "margin-top: 8px; padding: 8px; background: rgba(0,0,0,0.3); border-radius: 4px; font-size: 11px; color: #888;"; usageDiv.textContent = `📊 Tokens: ${ result.usage.prompt_tokens || 0 } in, ${result.usage.completion_tokens || 0} out | Model: ${ result.model }`; aiMsgElement.appendChild(usageDiv); } console.log("[chat] AI response received:", result); } catch (error) { aiMsgElement.innerHTML = '<span style="color: #ef4444;">❌ Error: ' + escapeHtml(error.message) + "</span>"; console.error("[chat] Error:", error); } finally { isProcessing = false; input.disabled = false; sendBtn.disabled = false; sendBtn.textContent = "Send"; input.focus(); } }; if (sendBtn) { sendBtn.addEventListener("click", sendMessage); sendBtn.addEventListener("mouseenter", () => { sendBtn.style.background = "#15803d"; }); sendBtn.addEventListener("mouseleave", () => { sendBtn.style.background = "#16a34a"; }); } if (input) { input.addEventListener("keydown", (e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendMessage(); } }); input.focus(); } }, }; // --- Expose AI Chat API --- window.AIChat = { open: () => { if (window.AppOverlay) { window.AppOverlay.open([aiChatSlide]); } else { console.error("[chat] AppOverlay not available"); } }, slide: aiChatSlide, getFileContext: getFileContext, buildFileContextMessage: buildFileContextMessage, }; // --- Register as AppItems component --- if (window.AppItems) { window.AppItems.push({ title: "AI Chat", html: aiChatSlide.html, onRender: aiChatSlide.onRender, }); console.log("[chat] Registered as AppItems component"); } console.log("[chat] AI Chat interface loaded (clean)"); })();