// 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)");
})();