// 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) ); } let currentChatMode = localStorage.getItem('chat_currentMode') || 'chat'; function saveChatMode(mode) { currentChatMode = mode; localStorage.setItem('chat_currentMode', mode); console.log('[chat] Mode changed to:', mode); } // --- 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, responseMode: settings.responseMode, }; } // Fallback if Settings not loaded return { endpoint: "api.php", defaultModel: "grok-code-fast-1", maxTokens: 2000, temperature: 0.7, responseMode: "normal", }; } 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 `
📎 No files in context
`; } let badgeHTML = '
'; badgeHTML += '
📎 FILE CONTEXT
'; if (fileContext.active) { badgeHTML += '
'; badgeHTML += '🟢 '; badgeHTML += '' + escapeHtml(fileContext.active.name) + ""; badgeHTML += "
"; } if (fileContext.read.length > 0) { fileContext.read.forEach(function (file) { badgeHTML += '
'; badgeHTML += '🔵 '; badgeHTML += '' + escapeHtml(file.name) + ""; badgeHTML += "
"; }); } badgeHTML += "
"; 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(); setTimeout(() => { updateConversationHistory(messagesContainer, conversationHistory); }, 0); } }); // 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 = '🤖 Thinking...'; } 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(); setTimeout(() => { updateConversationHistory(messagesContainer, conversationHistory); }, 0); } }); // 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) => { // Completely ignore excluded messages if (wrapper.classList.contains("excluded")) return; if (wrapper.dataset.includedInContext !== "true") return; 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: `
💬 No messages yet. Start a conversation!
`, onRender(el) { console.log("[chat] Rendering AI chat interface with tabs"); // --- TAB ELEMENTS --- 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"); // --- CHAT ELEMENTS --- const messagesContainer = el.querySelector("#aiChatMessages"); const messagesPlaceholder = el.querySelector("#aiChatMessagesPlaceholder"); const fileContextBadge = el.querySelector("#fileContextBadge"); // --- INPUT AREA --- const inputArea = el.querySelector("#aiChatInputArea"); const input = el.querySelector("#aiChatInput"); const sendBtn = el.querySelector("#aiChatSend"); const openBtn = el.querySelector("#aiOpenInputBtn"); // --- FILE AREAS --- const activeFileContainer = el.querySelector("#aiChatActiveFileContainer"); const filesContainer = el.querySelector("#aiChatFilesContainer"); // --- CHAT STATE --- let conversationHistory = []; let isProcessing = false; // ---------------------------------------------------------------------- // FILE CONTEXT BADGE // ---------------------------------------------------------------------- function updateFileContextBadge() { if (fileContextBadge) { fileContextBadge.innerHTML = renderFileContextBadge(); } } updateFileContextBadge(); // ---------------------------------------------------------------------- // TAB SWITCH HANDLER // ---------------------------------------------------------------------- function switchTab(tabName) { const tabs = { chat: chatContent, activeFile: activeFileContent, files: filesContent, settings: settingsContent, }; chatContent.style.display = "none"; activeFileContent.style.display = "none"; filesContent.style.display = "none"; settingsContent.style.display = "none"; const target = tabs[tabName]; target.style.display = tabName === "chat" ? "flex" : "block"; } chatTab.onclick = () => switchTab("chat"); activeFileTab.onclick = () => switchTab("activeFile"); filesTab.onclick = () => switchTab("files"); settingsTab.onclick = () => { if (window.Settings && window.Settings.open) { window.Settings.open(); } else { alert("Settings module not loaded."); } }; // ---------------------------------------------------------------------- // TOGGLE INPUT AREA (Ask AI button) // ---------------------------------------------------------------------- openBtn.addEventListener("click", () => { const isHidden = inputArea.style.display === "none" || inputArea.style.display === ""; if (isHidden) { inputArea.style.display = "block"; openBtn.textContent = "▼ Hide Input"; input.focus(); } else { inputArea.style.display = "none"; openBtn.textContent = "🤖 Ask AI"; } }); // ---------------------------------------------------------------------- // MODE SELECTOR // ---------------------------------------------------------------------- const modeSelector = el.querySelector("#aiModeSelector"); if (modeSelector) { modeSelector.value = currentChatMode; // Set initial value from localStorage modeSelector.addEventListener("change", () => { saveChatMode(modeSelector.value); console.log("✅ Mode selector changed to:", modeSelector.value); }); } // Hover UI [chatTab, activeFileTab, filesTab, settingsTab].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"; } }); }); // ---------------------------------------------------------------------- // ACTIVE FILE RENDER // ---------------------------------------------------------------------- function updateActiveFile() { if (activeFileContainer && window.ActiveFileDisplay) { window.ActiveFileDisplay.render(activeFileContainer); } updateFileContextBadge(); } // ---------------------------------------------------------------------- // FILE LIST RENDER // ---------------------------------------------------------------------- function updateFilesList() { if (filesContainer && window.FilesManager) { window.FilesManager.render(filesContainer, updateActiveFile); } } updateActiveFile(); updateFilesList(); 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.innerHTML = 'Thinking...'; // Add USER message addUserMessage(messagesContainer, message, messagesPlaceholder, conversationHistory); // Add AI placeholder message with spinner const aiMsgElement = addAIMessage(messagesContainer, "", conversationHistory, true); aiMsgElement.innerHTML = 'Thinking...'; try { const config = getApiConfig(); //-------------------------------------------------- // SYSTEM MESSAGE (based on Settings currentMode) //-------------------------------------------------- //-------------------------------------------------- // LOAD SYSTEM PROMPT FROM FILE BASED ON MODE //-------------------------------------------------- // LOAD SYSTEM PROMPT FROM FILE BASED ON MODE //-------------------------------------------------- let systemMessage = ""; const modeToFile = { chat: "prompts/chat.txt", snippets: "prompts/snippets.txt", fullcode: "prompts/fullcode.txt", document: "prompts/document.txt", edit: "prompts/edit.txt", architecture: "prompts/architecture.txt", scopes: "prompts/scope_mode.txt", }; const promptFile = modeToFile[currentChatMode] || modeToFile.chat; console.log("🔍 Loading prompt for mode:", currentChatMode, "->", promptFile); try { // Try without leading slash first let promptResponse = await fetch(`${promptFile}?v=${Date.now()}`); console.log("🔍 First fetch attempt:", promptResponse.status, promptResponse.statusText); // If that fails, try with leading slash if (!promptResponse.ok) { console.log("⚠️ First attempt failed, trying with leading slash..."); promptResponse = await fetch(`/${promptFile}?v=${Date.now()}`); console.log("🔍 Second fetch attempt:", promptResponse.status, promptResponse.statusText); } if (promptResponse.ok) { systemMessage = await promptResponse.text(); console.log(`✅ Loaded prompt from ${promptFile} (${systemMessage.length} chars)`); console.log("📄 Preview:", systemMessage.substring(0, 100) + "..."); } else { throw new Error(`HTTP ${promptResponse.status}: ${promptResponse.statusText}`); } } catch (err) { console.error("❌ Failed to load mode prompt:", err); console.error(" Attempted path:", promptFile); console.error(" Current mode:", currentChatMode); systemMessage = "You are a helpful AI assistant."; // Fallback } console.log("🔍 System message ready, length:", systemMessage.length); if (config.responseMode === "raw") { systemMessage += "\n\nReturn ONLY code. No markdown. No comments. No explanation."; } //-------------------------------------------------- // File context injection //-------------------------------------------------- const context = buildFileContextMessage(); if (context) systemMessage += context; //-------------------------------------------------- // API CALL //-------------------------------------------------- const result = await callAI(message, systemMessage, conversationHistory); aiMsgElement.innerHTML = escapeHtml(result.answer || ""); // Token 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); } } catch (error) { aiMsgElement.innerHTML = `❌ Error: ${escapeHtml( error.message )}`; } // Reset UI isProcessing = false; input.disabled = false; sendBtn.disabled = false; sendBtn.textContent = "Send"; input.focus(); }; // Attach handlers sendBtn.addEventListener("click", sendMessage); sendBtn.addEventListener("mouseenter", () => { if (!isProcessing) sendBtn.style.background = "#15803d"; }); sendBtn.addEventListener("mouseleave", () => { if (!isProcessing) sendBtn.style.background = "#16a34a"; }); 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)"); })();