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