πŸ“œ
chat_copy5.js
← Back
πŸ“ Javascript ⚑ Executable Ctrl+S: Save β€’ Ctrl+R: Run β€’ Ctrl+F: Find
// chat.js - AI Chat Interface with Scope Targeting and File Context (function () { console.log("[chat] Loading AI Chat interface..."); // --- 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) ); } // Remove triple backticks and normalize code blocks function renderAnswer(raw) { if (!raw) return ""; // -------------------------------------------------- // 1) Strip markdown fences ONLY // -------------------------------------------------- raw = raw.replace(/```[a-zA-Z0-9-]*\s*\n?/g, ""); raw = raw.replace(/```/g, ""); // -------------------------------------------------- // 2) PROTECT COMMENT BLOCKS (NON-DESTRUCTIVE) // We replace them with tokens so nothing later // can accidentally mangle them. // -------------------------------------------------- const htmlMap = []; const cssMap = []; const jsMap = []; // HTML comments: <!-- ... --> raw = raw.replace(/<!--[\s\S]*?-->/g, function(match) { const token = "Β§HTML" + htmlMap.length + "Β§"; htmlMap.push(match); return token; }); // CSS comments: /* ... */ raw = raw.replace(/\/\*[\s\S]*?\*\//g, function(match) { const token = "Β§CSS" + cssMap.length + "Β§"; cssMap.push(match); return token; }); // JS single-line comments: // ... raw = raw.replace(/\/\/[^\n]*/g, function(match) { const token = "Β§JS" + jsMap.length + "Β§"; jsMap.push(match); return token; }); // πŸ‘‰ IMPORTANT: // We are NOT doing any other transformations here. // No scope marker changes, no attribute changes, nothing. // -------------------------------------------------- // 3) RESTORE COMMENTS EXACTLY AS THEY WERE // -------------------------------------------------- raw = raw.replace(/Β§HTML(\d+)Β§/g, function(_, i) { return htmlMap[Number(i)]; }); raw = raw.replace(/Β§CSS(\d+)Β§/g, function(_, i) { return cssMap[Number(i)]; }); raw = raw.replace(/Β§JS(\d+)Β§/g, function(_, i) { return jsMap[Number(i)]; }); return raw.trim(); } // --- 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", // You can make this dynamic based on your auth system }, 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; } // --- Scope Management Functions --- function getAvailableScopes(activeFile) { if (!activeFile || !window.StorageEditorScopes) { return []; } const parsed = window.StorageEditorScopes.parseScopes(activeFile.content); const scopeNames = new Set(); // Extract base scope name (first part before underscore or hyphen) parsed.scopes.forEach((scope) => { const baseName = scope.name.split(/[_-]/)[0]; scopeNames.add(baseName); }); return Array.from(scopeNames).sort(); } function updateScopeSelector(scopeSelector, currentValue) { const files = window.FilesManager ? window.FilesManager.getFiles() : []; const activeFile = files.find((f) => f.active); const scopes = getAvailableScopes(activeFile); // Clear existing options except first two while (scopeSelector.options.length > 2) { scopeSelector.remove(2); } // Add scope options scopes.forEach((scope) => { const option = document.createElement("option"); option.value = scope; option.textContent = `πŸ“¦ ${scope}`; scopeSelector.appendChild(option); }); // Restore selection if it still exists if ( currentValue && (currentValue === "" || currentValue === "__new__" || scopes.includes(currentValue)) ) { scopeSelector.value = currentValue; } else { scopeSelector.value = ""; } return scopeSelector.value; } function updateScopeIndicator(scopeIndicator, currentScope) { if (currentScope === "") { scopeIndicator.textContent = "NO SCOPE"; scopeIndicator.style.background = "#374151"; scopeIndicator.style.color = "#9ca3af"; } else if (currentScope === "__new__") { scopeIndicator.textContent = "NEW SCOPE"; scopeIndicator.style.background = "#7c3aed"; scopeIndicator.style.color = "#fff"; } else { scopeIndicator.textContent = currentScope.toUpperCase(); scopeIndicator.style.background = "#16a34a"; scopeIndicator.style.color = "#fff"; } } // --- Message Display Functions --- function addUserMessage( messagesContainer, message, currentScope, 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; `; let scopeBadge = ""; if (currentScope) { const badgeColor = currentScope === "__new__" ? "#7c3aed" : "#16a34a"; const badgeText = currentScope === "__new__" ? "NEW SCOPE" : currentScope; scopeBadge = ` <div style=" display: inline-block; background: ${badgeColor}; color: #fff; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 700; margin-bottom: 6px; font-family: monospace; ">🎯 ${badgeText}</div><br> `; } userMsg.innerHTML = scopeBadge + 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, currentScope, 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 { const scopeInfo = currentScope ? ` <span style="color:#888;font-size:12px;">(scope: ${currentScope})</span>` : ""; aiMsg.innerHTML = escapeHtml(message || "") + scopeInfo; } // 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(); // Clean up the content (remove emoji prefix and scope info) let cleanContent = content; if (role === "assistant") { cleanContent = content .replace(/^πŸ€–\s+/, "") .replace(/\s+working on scope:.*$/, ""); } conversationHistory.push({ role: role, content: cleanContent, }); } }); console.log( "[chat] Updated conversation history:", conversationHistory.length, "messages" ); } // --- 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; } // --- 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; "> <!-- Scope Selector --> <div style=" display: flex; align-items: center; gap: 12px; margin-bottom: 12px; padding-bottom: 12px; border-bottom: 1px solid #2a2a2a; "> <label style=" color: #888; font-size: 13px; font-weight: 600; white-space: nowrap; ">🎯 Target Scope:</label> <select id="scopeSelector" style=" flex: 1; padding: 8px 12px; background: #2a2a2a; border: 1px solid #3a3a3a; border-radius: 4px; color: #e0e0e0; font-size: 13px; font-family: monospace; cursor: pointer; outline: none; "> <option value="">❌ None (General Chat)</option> <option value="__new__">✨ Create New Scope</option> </select> <div id="scopeIndicator" style=" padding: 6px 12px; background: #374151; border-radius: 4px; color: #9ca3af; font-size: 11px; font-weight: 600; white-space: nowrap; ">NO SCOPE</div> </div> <!-- 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"); const scopeSelector = el.querySelector("#scopeSelector"); const scopeIndicator = el.querySelector("#scopeIndicator"); let currentScope = ""; let conversationHistory = []; let isProcessing = false; // Function to update file context badge const updateFileContextBadge = function () { if (fileContextBadge) { fileContextBadge.innerHTML = renderFileContextBadge(); } }; // Initial scope selector population and file context display currentScope = updateScopeSelector(scopeSelector, currentScope); updateScopeIndicator(scopeIndicator, currentScope); updateFileContextBadge(); // Scope selector change handler scopeSelector.addEventListener("change", () => { const value = scopeSelector.value; if (value === "__new__") { const newScopeName = prompt("Enter new scope name (base name only):"); if (newScopeName && newScopeName.trim()) { const baseName = newScopeName .trim() .toLowerCase() .replace(/[^a-z0-9-]/g, ""); if (baseName) { currentScope = baseName; updateScopeIndicator(scopeIndicator, currentScope); // Add it to the selector const option = document.createElement("option"); option.value = baseName; option.textContent = `πŸ“¦ ${baseName}`; scopeSelector.appendChild(option); scopeSelector.value = baseName; } else { scopeSelector.value = ""; currentScope = ""; updateScopeIndicator(scopeIndicator, currentScope); } } else { scopeSelector.value = ""; currentScope = ""; updateScopeIndicator(scopeIndicator, currentScope); } } else { currentScope = value; updateScopeIndicator(scopeIndicator, currentScope); } }); // 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", () => { switchTab("settings", tabs, contents); // Render Settings UI inside the settings tab if (window.Settings && window.Settings.slide) { settingsContent.innerHTML = window.Settings.slide.html; // Ensure the onRender runs with settingsContent as root try { window.Settings.slide.onRender(settingsContent); } catch (e) { console.error("[chat] Error rendering settings UI:", e); settingsContent.innerHTML = '<div style="color:#f44;padding:20px">❌ Failed to load Settings UI.</div>'; } } else { settingsContent.innerHTML = '<div style="color:#f44;padding:20px">❌ Settings module not found.</div>'; } }); // 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); } // Update scope selector when file changes currentScope = updateScopeSelector(scopeSelector, currentScope); updateScopeIndicator(scopeIndicator, currentScope); // Update file context badge 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] Target scope:", currentScope || "none"); console.log("[chat] Model:", currentModel); // Add user message addUserMessage( messagesContainer, message, currentScope, messagesPlaceholder, conversationHistory ); // Create AI message placeholder const aiMsgElement = addAIMessage( messagesContainer, "", currentScope, conversationHistory, true ); try { // Get config from Settings module const config = getApiConfig(); // Build system message with file context let systemMessage = ""; // Select prompt based on current mode from settings if (config.currentMode === "chat") { systemMessage = config.chatPrompt; } else if (config.currentMode === "snippets") { systemMessage = config.snippetsPrompt; } else if (config.currentMode === "fullcode") { systemMessage = config.fullCodePrompt; } // Add response mode instruction 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; } if (currentScope) { systemMessage += "\n\nYou are working on scope: " + currentScope; } // Call API const result = await callAI( message, systemMessage, conversationHistory ); // --- FINAL ANSWER RENDER AFTER API RETURNS --- const cleanedAnswer = renderAnswer(result.answer); const mode = config.currentMode || "chat"; /* const renderScopeBlock = window.StorageEditorScopes && window.StorageEditorScopes.renderScopeBlock ? window.StorageEditorScopes.renderScopeBlock : null;*/ const renderScopeBlock = window.StorageEditorScopes && window.StorageEditorScopes.renderScopeBlock ? window.StorageEditorScopes.renderScopeBlock : null; if (mode === "snippets" && renderScopeBlock) { // Parse the answer to extract all scopes const parsed = window.StorageEditorScopes.parseScopes(cleanedAnswer); if (parsed.scopes && parsed.scopes.length > 0) { let html = ''; const lines = cleanedAnswer.split('\n'); parsed.scopes.forEach((scope, idx) => { const scopeContent = lines.slice(scope.startLine + 1, scope.endLine).join('\n'); html += renderScopeBlock( { data: { language: scope.language, name: scope.name, header: scope.header, container: scope.container, attributes: scope.attributes }, startLine: scope.startLine, endLine: scope.endLine, content: scopeContent, }, "ai-snippet-" + Date.now() + "-" + idx, true // isInChat flag ); }); aiMsgElement.innerHTML = html; // Attach button event listeners window.StorageEditorScopes.renderAnswer(cleanedAnswer, aiMsgElement); } else { // Fallback if no scopes parsed aiMsgElement.innerHTML = renderScopeBlock( { data: { language: "javascript", name: currentScope || "snippet", }, startLine: 0, endLine: cleanedAnswer.split("\n").length - 1, content: cleanedAnswer, }, "ai-snippet-" + Date.now() ); } } else { // Normal Chat Mode const scopeInfo = currentScope ? ` <span style="color:#888;font-size:12px;">(scope: ${currentScope})</span>` : ""; aiMsgElement.innerHTML = escapeHtml(cleanedAnswer) + scopeInfo; } // Show usage info if available 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"); })();