// 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,
responseMode: settings.responseMode,
currentMode: settings.currentMode, // still needed for choosing the prompt file
};
}
// Fallback if Settings not loaded
return {
endpoint: "api.php",
defaultModel: "grok-code-fast-1",
maxTokens: 2000,
temperature: 0.7,
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>
#aiModeSelector {
width: 180px;
padding: 6px 8px;
background: #111;
border: 1px solid #333;
border-radius: 6px;
color: white;
font-size: 13px;
}
/* Spinner animation */
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.thinking-spinner {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid #333;
border-top-color: #16a34a;
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin-right: 8px;
vertical-align: middle;
}
</style>
<div style="display: flex; flex-direction: column; height: 100%; position: relative;">
<!-- TOP TABS -->
<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;
">💬 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;
">📄 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;
">📁 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;
">⚙️ Settings</button>
</div>
<!-- MAIN CONTENT AREA -->
<div style="flex: 1; position: relative; display: flex; flex-direction: column; overflow: hidden; min-height: 0;">
<!-- CHAT AREA -->
<div id="chatContent" class="tab-content" style="
flex: 1;
display: flex;
flex-direction: column;
background: #0a0a0a;
overflow: hidden;
min-height: 0;
">
<div style="flex: 1; overflow-y: auto; padding: 20px;">
<div id="fileContextBadge"></div>
<div id="aiChatMessages">
<div id="aiChatMessagesPlaceholder" style="color: #666; text-align: center; padding: 40px;">
💬 No messages yet. Start a conversation!
</div>
</div>
</div>
<!-- INPUT AREA (part of chat, not overlay) -->
<div id="aiChatInputArea" style="
background: #1a1a1a;
border-top: 2px solid #2a2a2a;
padding: 12px;
display: none;
">
<textarea id="aiChatInput" placeholder="Type your message..."
style="
width: 100%;
padding: 12px;
background: #2a2a2a;
border: 1px solid #3a3a3a;
border-radius: 6px;
color: #e0e0e0;
font-size: 14px;
min-height: 70px;
resize: vertical;
"
></textarea>
<div style="margin-top: 10px; display: flex; justify-content: space-between; align-items: center; gap: 8px;">
<select id="aiModeSelector">
<option value="chat">💬 Chat</option>
<option value="scopes">🧩 Scope Mode</option>
<option value="edit">✏️ Edit Mode</option>
<option value="document">📘 Document Mode</option>
<option value="architecture">🏗️ Architecture</option>
</select>
<button id="aiChatSend" style="
padding: 10px 16px;
background: #16a34a;
color: white;
border: 1px solid #15803d;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
">Send</button>
</div>
</div>
<!-- ASK AI BUTTON (always visible) -->
<div style="
padding: 12px;
background: #1a1a1a;
border-top: 2px solid #2a2a2a;
display: flex;
justify-content: center;
">
<button id="aiOpenInputBtn" style="
padding: 12px 24px;
background: #2563eb;
border: 1px solid #1e40af;
border-radius: 6px;
color: white;
font-size: 15px;
font-weight: 700;
cursor: pointer;
">🤖 Ask AI</button>
</div>
</div>
<!-- ACTIVE FILE AREA -->
<div id="activeFileContent" class="tab-content" style="
flex: 1; overflow-y: auto; padding: 20px; background: #0a0a0a; display: none;
">
<div id="aiChatActiveFileContainer"></div>
</div>
<!-- FILES LIST -->
<div id="filesContent" class="tab-content" style="
flex: 1; overflow-y: auto; padding: 20px; background: #0a0a0a; display: none;
">
<div id="aiChatFilesContainer"></div>
</div>
<!-- SETTINGS -->
<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");
// --- TAB ELEMENTS ---
const chatTab = el.querySelector("#chatTab");
const activeFileTab = el.querySelector("#activeFileTab");
const filesTab = el.querySelector("#filesTab");
const settingsTab = el.querySelector("#settingsTab");
const chatContent = el.querySelector("#chatContent");
const activeFileContent = el.querySelector("#activeFileContent");
const filesContent = el.querySelector("#filesContent");
const settingsContent = el.querySelector("#settingsContent");
// --- CHAT ELEMENTS ---
const messagesContainer = el.querySelector("#aiChatMessages");
const messagesPlaceholder = el.querySelector("#aiChatMessagesPlaceholder");
const fileContextBadge = el.querySelector("#fileContextBadge");
// --- INPUT AREA ---
const inputArea = el.querySelector("#aiChatInputArea");
const input = el.querySelector("#aiChatInput");
const sendBtn = el.querySelector("#aiChatSend");
const openBtn = el.querySelector("#aiOpenInputBtn");
// --- FILE AREAS ---
const activeFileContainer = el.querySelector("#aiChatActiveFileContainer");
const filesContainer = el.querySelector("#aiChatFilesContainer");
// --- CHAT STATE ---
let conversationHistory = [];
let isProcessing = false;
// ----------------------------------------------------------------------
// FILE CONTEXT BADGE
// ----------------------------------------------------------------------
function updateFileContextBadge() {
if (fileContextBadge) {
fileContextBadge.innerHTML = renderFileContextBadge();
}
}
updateFileContextBadge();
// ----------------------------------------------------------------------
// TAB SWITCH HANDLER
// ----------------------------------------------------------------------
function switchTab(tabName) {
const tabs = {
chat: chatContent,
activeFile: activeFileContent,
files: filesContent,
settings: settingsContent,
};
chatContent.style.display = "none";
activeFileContent.style.display = "none";
filesContent.style.display = "none";
settingsContent.style.display = "none";
const target = tabs[tabName];
target.style.display = tabName === "chat" ? "flex" : "block";
}
chatTab.onclick = () => switchTab("chat");
activeFileTab.onclick = () => switchTab("activeFile");
filesTab.onclick = () => switchTab("files");
settingsTab.onclick = () => {
if (window.Settings && window.Settings.open) {
window.Settings.open();
} else {
alert("Settings module not loaded.");
}
};
// ----------------------------------------------------------------------
// TOGGLE INPUT AREA (Ask AI button)
// ----------------------------------------------------------------------
openBtn.addEventListener("click", () => {
const isHidden = inputArea.style.display === "none" || inputArea.style.display === "";
if (isHidden) {
inputArea.style.display = "block";
openBtn.textContent = "▼ Hide Input";
input.focus();
} else {
inputArea.style.display = "none";
openBtn.textContent = "🤖 Ask AI";
}
});
// Hover UI
[chatTab, activeFileTab, filesTab, settingsTab].forEach((tab) => {
tab.addEventListener("mouseenter", () => {
if (tab.style.background === "rgb(26, 26, 26)") {
tab.style.background = "#242424";
}
});
tab.addEventListener("mouseleave", () => {
if (tab.style.background === "rgb(36, 36, 36)") {
tab.style.background = "#1a1a1a";
}
});
});
// ----------------------------------------------------------------------
// ACTIVE FILE RENDER
// ----------------------------------------------------------------------
function updateActiveFile() {
if (activeFileContainer && window.ActiveFileDisplay) {
window.ActiveFileDisplay.render(activeFileContainer);
}
updateFileContextBadge();
}
// ----------------------------------------------------------------------
// FILE LIST RENDER
// ----------------------------------------------------------------------
function updateFilesList() {
if (filesContainer && window.FilesManager) {
window.FilesManager.render(filesContainer, updateActiveFile);
}
}
updateActiveFile();
updateFilesList();
window.addEventListener("activeFilesUpdated", () => {
updateActiveFile();
updateFilesList();
});
// ----------------------------------------------------------------------
// SEND MESSAGE HANDLER
// ----------------------------------------------------------------------
const sendMessage = async () => {
const message = input.value.trim();
if (!message || isProcessing) return;
isProcessing = true;
input.value = "";
input.disabled = true;
sendBtn.disabled = true;
sendBtn.innerHTML = '<span class="thinking-spinner"></span>Thinking...';
// Add USER message
addUserMessage(messagesContainer, message, messagesPlaceholder, conversationHistory);
// Add AI placeholder message with spinner
const aiMsgElement = addAIMessage(messagesContainer, "", conversationHistory, true);
aiMsgElement.innerHTML = '<span class="thinking-spinner"></span><span style="color:#888;">Thinking...</span>';
try {
const config = getApiConfig();
//--------------------------------------------------
// SYSTEM MESSAGE (based on Settings currentMode)
//--------------------------------------------------
//--------------------------------------------------
// LOAD SYSTEM PROMPT FROM FILE BASED ON MODE
//--------------------------------------------------
// LOAD SYSTEM PROMPT FROM FILE BASED ON MODE
//--------------------------------------------------
let systemMessage = "";
const modeToFile = {
chat: "prompts/chat.txt",
snippets: "prompts/snippets.txt",
fullcode: "prompts/fullcode.txt",
document: "prompts/document.txt",
edit: "prompts/edit.txt",
architecture: "prompts/architecture.txt",
scopes: "prompts/scope_mode.txt",
};
const promptFile = modeToFile[config.currentMode] || modeToFile.chat;
try {
alert("🔍 Current mode:", config.currentMode);
alert("🔍 Prompt file:", promptFile);
alert("🔍 Full URL:", window.location.origin + "/" + promptFile);
// Try different path variations
let promptResponse = await fetch(`${promptFile}?v=${Date.now()}`);
// If that fails, try with leading slash
if (!promptResponse.ok) {
promptResponse = await fetch(`/${promptFile}?v=${Date.now()}`);
}
if (promptResponse.ok) {
systemMessage = await promptResponse.text();
console.log(`✅ Loaded prompt from ${promptFile}`, systemMessage.substring(0, 100) + "...");
} else {
throw new Error(`HTTP ${promptResponse.status}: ${promptResponse.statusText}`);
}
} catch (err) {
console.error("❌ Failed to load mode prompt:", err);
console.error(" Attempted path:", promptFile);
systemMessage = "You are a helpful AI assistant."; // Fallback
}
if (config.responseMode === "raw") {
systemMessage +=
"\n\nReturn ONLY code. No markdown. No comments. No explanation.";
}
//--------------------------------------------------
// File context injection
//--------------------------------------------------
const context = buildFileContextMessage();
if (context) systemMessage += context;
//--------------------------------------------------
// API CALL
//--------------------------------------------------
const result = await callAI(message, systemMessage, conversationHistory);
aiMsgElement.innerHTML = escapeHtml(result.answer || "");
// Token info
if (result.usage) {
const usageDiv = document.createElement("div");
usageDiv.style.cssText =
"margin-top:8px; padding:8px; background:rgba(0,0,0,0.3); border-radius:4px; font-size:11px; color:#888;";
usageDiv.textContent = `📊 Tokens: ${
result.usage.prompt_tokens || 0
} in, ${result.usage.completion_tokens || 0} out | Model: ${result.model}`;
aiMsgElement.appendChild(usageDiv);
}
} catch (error) {
aiMsgElement.innerHTML = `<span style="color:#ef4444;">❌ Error: ${escapeHtml(
error.message
)}</span>`;
}
// Reset UI
isProcessing = false;
input.disabled = false;
sendBtn.disabled = false;
sendBtn.textContent = "Send";
input.focus();
};
// Attach handlers
sendBtn.addEventListener("click", sendMessage);
sendBtn.addEventListener("mouseenter", () => {
if (!isProcessing) sendBtn.style.background = "#15803d";
});
sendBtn.addEventListener("mouseleave", () => {
if (!isProcessing) sendBtn.style.background = "#16a34a";
});
input.addEventListener("keydown", (e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
input.focus();
},
};
// --- Expose AI Chat API ---
window.AIChat = {
open: () => {
if (window.AppOverlay) {
window.AppOverlay.open([aiChatSlide]);
} else {
console.error("[chat] AppOverlay not available");
}
},
slide: aiChatSlide,
getFileContext: getFileContext,
buildFileContextMessage: buildFileContextMessage,
};
// --- Register as AppItems component ---
if (window.AppItems) {
window.AppItems.push({
title: "AI Chat",
html: aiChatSlide.html,
onRender: aiChatSlide.onRender,
});
console.log("[chat] Registered as AppItems component");
}
console.log("[chat] AI Chat interface loaded (clean)");
})();