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