📜
chat_copy6.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: ` <style> :host { display: block; height: 100%; } /* Modern sleek look */ #aiChatWrapper { display: flex; flex-direction: column; height: 100%; background: #0a0a0a; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; } /* Tabs */ .tab-bar { display: flex; background: #1a1a1a; border-bottom: 2px solid #2a2a2a; } .ai-tab { flex: 1; padding: 16px; background: #1a1a1a; border: none; color: #888; font-weight: 700; font-size: 14px; text-transform: uppercase; letter-spacing: 0.5px; cursor: pointer; transition: all 0.2s; } .ai-tab.active { background: #2a2a2a; color: white; border-bottom: 3px solid #16a34a; } .ai-tab:hover:not(.active) { background: #242424; color: #aaa; } /* Messages area */ #aiChatMessages { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 16px; } .user-message { align-self: flex-end; max-width: 80%; padding: 14px 18px; background: linear-gradient(135deg, rgba(59, 130, 246, 0.2), rgba(37, 99, 235, 0.15)); border-left: 4px solid #3b82f6; border-radius: 12px; color: #e2e8f0; box-shadow: 0 2px 8px rgba(59, 130, 246, 0.15); } .ai-message { align-self: flex-start; max-width: 85%; padding: 14px 18px; background: linear-gradient(135deg, rgba(30, 41, 59, 0.7), rgba(15, 23, 42, 0.7)); border-left: 4px solid #16a34a; border-radius: 12px; color: #e2e8f0; white-space: pre-wrap; box-shadow: 0 2px 10px rgba(0,0,0,0.3); } /* Thinking dots */ .thinking-indicator { display: inline-flex; align-items: center; gap: 6px; padding: 10px 14px; background: rgba(59, 130, 246, 0.15); border-radius: 12px; font-size: 13px; color: #3b82f6; font-weight: 600; } .thinking-dot { width: 8px; height: 8px; background: #3b82f6; border-radius: 50%; animation: pulse 1.4s ease-in-out infinite; } .thinking-dot:nth-child(2) { animation-delay: 0.2s; } .thinking-dot:nth-child(3) { animation-delay: 0.4s; } @keyframes pulse { 0%,100% { transform: scale(0.8); opacity: 0.5; } 50% { transform: scale(1.2); opacity: 1; } } /* Input area */ .input-container { padding: 20px; background: linear-gradient(180deg, #111 0%, #0a0a0a 100%); border-top: 1px solid #2a2a2a; box-shadow: 0 -6px 20px rgba(0,0,0,0.4); } .input-row { display: flex; gap: 14px; align-items: flex-end; } #aiChatInput { flex: 1; padding: 16px 18px; background: rgba(30, 41, 59, 0.6); border: 1px solid rgba(148, 163, 184, 0.2); border-radius: 16px; color: #e2e8f0; font-size: 15px; resize: none; min-height: 60px; max-height: 180px; transition: all 0.3s ease; box-shadow: 0 4px 12px rgba(0,0,0,0.3); } #aiChatInput:focus { outline: none; border-color: #3b82f6; background: rgba(30, 41, 59, 0.9); box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.2), 0 6px 20px rgba(0,0,0,0.4); } #aiChatInput::placeholder { color: #64748b; } #aiChatSend { padding: 0 32px; height: 60px; background: linear-gradient(135deg, #16a34a, #15803d); border: none; border-radius: 16px; color: white; font-size: 15px; font-weight: 700; cursor: pointer; box-shadow: 0 4px 15px rgba(22, 163, 74, 0.4); transition: all 0.2s; } #aiChatSend:hover { transform: translateY(-2px); box-shadow: 0 8px 20px rgba(22, 163, 74, 0.5); } #aiChatSend:disabled { opacity: 0.6; transform: none; cursor: not-allowed; } </style> <div id="aiChatWrapper"> <div class="tab-bar"> <button id="chatTab" class="ai-tab active">Chat</button> <button id="activeFileTab" class="ai-tab">Active File</button> <button id="filesTab" class="ai-tab">Files</button> <button id="settingsTab" class="ai-tab">Settings</button> </div> <div id="chatContent" style="flex:1; display:flex; flex-direction:column;"> <div id="aiChatMessages"> <div id="aiChatMessagesPlaceholder" style="color:#666; text-align:center; padding:80px;"> No messages yet. Ask me anything! </div> </div> <div class="input-container"> <div class="input-row"> <textarea id="aiChatInput" placeholder="Ask me anything... (Shift+Enter for new line)"></textarea> <button id="aiChatSend">Send</button> </div> </div> </div> <!-- Hidden tabs --> <div id="activeFileContent" style="display:none; flex:1; overflow:auto; padding:20px; background:#0a0a0a;"> <div id="aiChatActiveFileContainer"></div> </div> <div id="filesContent" style="display:none; flex:1; overflow:auto; padding:20px; background:#0a0a0a;"> <div id="aiChatFilesContainer"></div> </div> <div id="settingsContent" style="display:none; flex:1; overflow:auto; padding:20px; background:#0a0a0a; color:#666; text-align:center; padding-top:100px;"> Settings panel </div> </div> `, onRender(el) { console.log("[AI Chat] Modern interface loaded"); // Elements const messagesDiv = el.querySelector("#aiChatMessages"); const placeholder = el.querySelector("#aiChatMessagesPlaceholder"); const input = el.querySelector("#aiChatInput"); const sendBtn = el.querySelector("#aiChatSend"); const chatContent = el.querySelector("#chatContent"); const activeFileContent = el.querySelector("#activeFileContent"); const filesContent = el.querySelector("#filesContent"); const settingsContent = el.querySelector("#settingsContent"); let conversationHistory = []; let isProcessing = false; // Auto-scroll to bottom const scrollToBottom = () => { messagesDiv.scrollTop = messagesDiv.scrollHeight; }; // Add user message const addUserMessage = (text) => { placeholder.style.display = "none"; const div = document.createElement("div"); div.className = "user-message"; div.textContent = text; messagesDiv.appendChild(div); scrollToBottom(); conversationHistory.push({ role: "user", content: text }); }; // Add AI message (with optional thinking dots) const addAIMessage = (text = "", showThinking = false) => { const div = document.createElement("div"); div.className = "ai-message"; if (showThinking) { div.innerHTML = ` <div class="thinking-indicator"> <span>Thinking</span> <div class="thinking-dot"></div> <div class="thinking-dot"></div> <div class="thinking-dot"></div> </div>`; } else { div.textContent = text; } messagesDiv.appendChild(div); scrollToBottom(); return div; }; // Tab switching const switchTo = (tab) => { [chatContent, activeFileContent, filesContent, settingsContent].forEach(t => t.style.display = "none"); document.querySelectorAll(".ai-tab").forEach(b => { b.classList.remove("active"); }); if (tab === "chat") { chatContent.style.display = "flex"; el.querySelector("#chatTab").classList.add("active"); } else if (tab === "activeFile") { activeFileContent.style.display = "block"; el.querySelector("#activeFileTab").classList.add("active"); if (window.ActiveFileDisplay) window.ActiveFileDisplay.render(el.querySelector("#aiChatActiveFileContainer")); } else if (tab === "files") { filesContent.style.display = "block"; el.querySelector("#filesTab").classList.add("active"); if (window.FilesManager) window.FilesManager.render(el.querySelector("#aiChatFilesContainer")); } else if (tab === "settings") { settingsContent.style.display = "block"; el.querySelector("#settingsTab").classList.add("active"); if (window.Settings?.open) window.Settings.open(); } }; el.querySelector("#chatTab").onclick = () => switchTo("chat"); el.querySelector("#activeFileTab").onclick = () => switchTo("activeFile"); el.querySelector("#filesTab").onclick = () => switchTo("files"); el.querySelector("#settingsTab").onclick = () => switchTo("settings"); // Send message const sendMessage = async () => { const text = input.value.trim(); if (!text || isProcessing) return; addUserMessage(text); input.value = ""; input.style.height = "60px"; const aiBubble = addAIMessage("", true); isProcessing = true; sendBtn.disabled = true; sendBtn.textContent = "Thinking..."; try { const config = getApiConfig() || {}; let systemPrompt = config.chatPrompt || "You are a helpful coding assistant."; if (config.responseMode === "raw") { systemPrompt += "\n\nReturn ONLY code, no explanations or markdown."; } const fileContext = typeof buildFileContextMessage === "function" ? buildFileContextMessage() : ""; if (fileContext) systemPrompt += "\n\n" + fileContext; const result = await callAI(text, systemPrompt, conversationHistory); // Replace thinking with real answer aiBubble.innerHTML = escapeHtml(result.answer || "No response"); if (result.usage) { const usage = document.createElement("div"); usage.style.cssText = "margin-top:10px; font-size:11px; color:#666;"; usage.textContent = `Tokens: ${result.usage.prompt_tokens} in / ${result.usage.completion_tokens} out • ${result.model}`; aiBubble.appendChild(usage); } conversationHistory.push({ role: "assistant", content: result.answer }); } catch (err) { aiBubble.innerHTML = `<span style="color:#ef4444;">Error: ${escapeHtml(err.message)}</span>`; console.error(err); } finally { isProcessing = false; sendBtn.disabled = false; sendBtn.textContent = "Send"; input.focus(); scrollToBottom(); } }; sendBtn.onclick = sendMessage; input.addEventListener("keydown", (e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendMessage(); } }); // Auto-resize textarea input.addEventListener("input", () => { input.style.height = "60px"; input.style.height = input.scrollHeight + "px"; }); 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)"); })();