/* ELYSIA CODE COMPANION v1.2.2 - Main Application Entry point and UI orchestration */ import Utils from "./utils.js"; import DB from "./db.js"; import FileSystem from "./filesystem.js"; import Chat from "./chat.js"; class App { constructor() { this.state = { apiKey: null, model: "x-ai/grok-3-fast", maxFiles: 100, autoPreview: true, syntaxHighlighting: true, maxResponseTokens: 4000, maxHistoryMessages: 20, theme: "dark", currentFile: null }; this.elements = { btnOpenFolder: document.getElementById("btn-open-folder"), btnCloseFolder: document.getElementById("btn-close-folder"), btnSettings: document.getElementById("btn-settings"), btnHistory: document.getElementById("btn-history"), btnSaveSettings: document.getElementById("btn-save-settings"), btnClearHistory: document.getElementById("btn-clear-history"), fileTree: document.getElementById("file-tree"), filePreview: document.getElementById("file-preview"), folderInfo: document.getElementById("folder-info"), folderName: document.getElementById("folder-name"), folderStats: document.getElementById("folder-stats"), searchFiles: document.getElementById("search-files"), apiKeyInput: document.getElementById("api-key"), modelSelect: document.getElementById("model-select"), maxFilesInput: document.getElementById("max-files"), autoPreviewCheckbox: document.getElementById("auto-preview"), syntaxHighlightingCheckbox: document.getElementById("syntax-highlighting"), maxResponseTokensInput: document.getElementById("max-response-tokens"), maxHistoryMessagesInput: document.getElementById("max-history-messages"), themeSelect: document.getElementById("theme-select") }; } async init() { // Load settings this.loadSettings(); // Ensure all modals are closed on startup (safety check) document.querySelectorAll(".modal").forEach(m => m.classList.remove("active")); // Initialize chat Chat.init(); // Setup event listeners this.setupEventListeners(); // Check File System Access API support if (!FileSystem.isSupported()) { Utils.toast.warning("File System Access API not supported. Please use Chrome or Edge browser."); } // Setup keyboard shortcuts this.setupKeyboardShortcuts(); // Setup drag & drop for folder this.setupDragAndDrop(); // Apply theme this.applyTheme(this.state.theme); // App initialized (production: silent mode) // console.log("💎 Elysia Code Companion initialized"); } setupDragAndDrop() { const dropZone = document.body; dropZone.addEventListener("dragover", e => { e.preventDefault(); e.stopPropagation(); dropZone.classList.add("drag-over"); }); dropZone.addEventListener("dragleave", e => { e.preventDefault(); e.stopPropagation(); dropZone.classList.remove("drag-over"); }); dropZone.addEventListener("drop", async e => { e.preventDefault(); e.stopPropagation(); dropZone.classList.remove("drag-over"); const items = e.dataTransfer.items; // Check if folder was dropped if (items && items.length > 0) { const item = items[0]; if (item.kind === "file") { const entry = item.webkitGetAsEntry?.(); if (entry && entry.isDirectory) { Utils.toast.info( "Drag & drop folder detected. Please use 'Open Folder' button for full access." ); } else { Utils.toast.warning("Please drag a folder, not individual files."); } } } }); } setupSidebarToggles() { const btnToggleLeft = document.getElementById("btn-toggle-left"); const btnToggleRight = document.getElementById("btn-toggle-right"); const sidebarLeft = document.getElementById("sidebar-left"); const sidebarRight = document.getElementById("sidebar-right"); const overlay = document.getElementById("sidebar-overlay"); const isMobile = () => window.innerWidth <= 900; // Toggle left sidebar btnToggleLeft.addEventListener("click", () => { if (isMobile()) { sidebarLeft.classList.toggle("visible"); sidebarRight.classList.remove("visible"); overlay.classList.toggle("active", sidebarLeft.classList.contains("visible")); } else { sidebarLeft.classList.toggle("hidden"); } btnToggleLeft.classList.toggle( "active", isMobile() ? sidebarLeft.classList.contains("visible") : !sidebarLeft.classList.contains("hidden") ); btnToggleRight.classList.remove("active"); }); // Toggle right sidebar btnToggleRight.addEventListener("click", () => { if (isMobile()) { sidebarRight.classList.toggle("visible"); sidebarLeft.classList.remove("visible"); overlay.classList.toggle("active", sidebarRight.classList.contains("visible")); } else { sidebarRight.classList.toggle("hidden"); } btnToggleRight.classList.toggle( "active", isMobile() ? sidebarRight.classList.contains("visible") : !sidebarRight.classList.contains("hidden") ); btnToggleLeft.classList.remove("active"); }); // Close sidebars when clicking overlay overlay.addEventListener("click", () => { sidebarLeft.classList.remove("visible"); sidebarRight.classList.remove("visible"); overlay.classList.remove("active"); btnToggleLeft.classList.remove("active"); btnToggleRight.classList.remove("active"); }); // Set initial active state for desktop (sidebars visible by default) if (!isMobile()) { btnToggleLeft.classList.add("active"); btnToggleRight.classList.add("active"); } // Handle resize window.addEventListener("resize", () => { if (!isMobile()) { sidebarLeft.classList.remove("visible"); sidebarRight.classList.remove("visible"); overlay.classList.remove("active"); } }); } applyTheme(theme) { document.body.setAttribute("data-theme", theme); this.state.theme = theme; Utils.storage.set("theme", theme); } setupKeyboardShortcuts() { document.addEventListener("keydown", e => { // Ctrl+O: Open folder if (e.ctrlKey && e.key === "o") { e.preventDefault(); this.openFolder(); } // Ctrl+K: Focus search if (e.ctrlKey && e.key === "k") { e.preventDefault(); this.elements.searchFiles.focus(); } // Escape: Close modals if (e.key === "Escape") { document.querySelectorAll(".modal.active").forEach(modal => { Utils.modal.close(modal.id); }); } // Ctrl+Enter: Send message (when chat input focused) if (e.ctrlKey && e.key === "Enter" && document.activeElement === document.getElementById("chat-input")) { e.preventDefault(); Chat.sendMessage(); } // Arrow Up/Down: Navigate file tree (when file tree focused) if ((e.key === "ArrowUp" || e.key === "ArrowDown") && document.activeElement.closest(".file-tree")) { e.preventDefault(); this.navigateFileTree(e.key === "ArrowUp" ? -1 : 1); } // Enter: Open selected file in tree if (e.key === "Enter" && document.activeElement.classList.contains("tree-item")) { e.preventDefault(); const path = document.activeElement.dataset.path; if (path) this.previewFile(path); } }); } setupEventListeners() { // Folder actions this.elements.btnOpenFolder.addEventListener("click", () => this.openFolder()); this.elements.btnCloseFolder.addEventListener("click", () => this.closeFolder()); // Settings this.elements.btnSettings.addEventListener("click", () => { this.populateSettings(); Utils.modal.open("modal-settings"); }); this.elements.btnSaveSettings.addEventListener("click", () => { this.saveSettings(); Utils.modal.close("modal-settings"); }); // History this.elements.btnHistory.addEventListener("click", () => { this.loadHistory(); Utils.modal.open("modal-history"); }); this.elements.btnClearHistory.addEventListener("click", async () => { if (confirm("Are you sure you want to clear all chat history?")) { await DB.clearChats(); this.loadHistory(); } }); // Sidebar toggles this.setupSidebarToggles(); // File search this.elements.searchFiles.addEventListener( "input", Utils.debounce(e => this.searchFiles(e.target.value), 300) ); // API Key visibility toggle const btnToggleApiKey = document.getElementById("btn-toggle-api-key"); if (btnToggleApiKey) { btnToggleApiKey.addEventListener("click", () => { const apiKeyInput = this.elements.apiKeyInput; const isPassword = apiKeyInput.type === "password"; apiKeyInput.type = isPassword ? "text" : "password"; btnToggleApiKey.textContent = isPassword ? "🙈" : "👁️"; }); } // About modal const btnAbout = document.getElementById("btn-about"); if (btnAbout) { btnAbout.addEventListener("click", e => { e.preventDefault(); Utils.modal.open("modal-about"); }); } } loadSettings() { this.state.apiKey = Utils.storage.get("apiKey"); this.state.model = Utils.storage.get("model", "x-ai/grok-4.1-fast"); this.state.maxFiles = Utils.storage.get("maxFiles", 100); this.state.autoPreview = Utils.storage.get("autoPreview", true); this.state.syntaxHighlighting = Utils.storage.get("syntaxHighlighting", true); this.state.maxResponseTokens = Utils.storage.get("maxResponseTokens", 4000); this.state.maxHistoryMessages = Utils.storage.get("maxHistoryMessages", 20); this.state.theme = Utils.storage.get("theme", "dark"); } populateSettings() { this.elements.apiKeyInput.value = this.state.apiKey || ""; this.elements.modelSelect.value = this.state.model; this.elements.maxFilesInput.value = this.state.maxFiles; this.elements.autoPreviewCheckbox.checked = this.state.autoPreview; this.elements.syntaxHighlightingCheckbox.checked = this.state.syntaxHighlighting; this.elements.maxResponseTokensInput.value = this.state.maxResponseTokens; this.elements.maxHistoryMessagesInput.value = this.state.maxHistoryMessages; this.elements.themeSelect.value = this.state.theme; } saveSettings() { this.state.apiKey = this.elements.apiKeyInput.value; this.state.model = this.elements.modelSelect.value; this.state.maxFiles = parseInt(this.elements.maxFilesInput.value); this.state.autoPreview = this.elements.autoPreviewCheckbox.checked; this.state.syntaxHighlighting = this.elements.syntaxHighlightingCheckbox.checked; this.state.maxResponseTokens = parseInt(this.elements.maxResponseTokensInput.value); this.state.maxHistoryMessages = parseInt(this.elements.maxHistoryMessagesInput.value); this.state.theme = this.elements.themeSelect.value; Utils.storage.set("apiKey", this.state.apiKey); Utils.storage.set("model", this.state.model); Utils.storage.set("maxFiles", this.state.maxFiles); Utils.storage.set("autoPreview", this.state.autoPreview); Utils.storage.set("syntaxHighlighting", this.state.syntaxHighlighting); Utils.storage.set("maxResponseTokens", this.state.maxResponseTokens); Utils.storage.set("maxHistoryMessages", this.state.maxHistoryMessages); // Update Chat module with new history limit if (window.Chat) { window.Chat.maxHistoryMessages = this.state.maxHistoryMessages; } // Apply theme this.applyTheme(this.state.theme); Utils.toast.success("Settings saved!"); } async openFolder() { Utils.loading.show("Opening folder picker..."); try { const result = await FileSystem.openFolder(); if (!result) { Utils.loading.hide(); return; } Utils.loading.update("Loading files..."); // Update UI this.elements.folderInfo.style.display = "block"; this.elements.folderName.textContent = result.name; this.elements.folderStats.textContent = `${result.files.length} files`; this.elements.btnOpenFolder.style.display = "none"; this.elements.btnCloseFolder.style.display = "block"; // Update chat context Chat.updateContextInfo(); // Render file tree Utils.loading.update("Rendering file tree..."); this.renderFileTree(); Utils.loading.hide(); } catch (err) { Utils.loading.hide(); console.error("Failed to open folder:", err); Utils.toast.error("Failed to open folder: " + err.message); } } closeFolder() { // Confirm if there's chat history in current session if (Chat && Chat.messageContainer) { const messageCount = Chat.messageContainer.querySelectorAll(".message.user").length; if (messageCount > 0) { if (!confirm(`You have ${messageCount} message(s) in this session. Close folder and lose context?`)) { return; } } } FileSystem.close(); // Update UI this.elements.folderInfo.style.display = "none"; this.elements.btnOpenFolder.style.display = "block"; this.elements.btnCloseFolder.style.display = "none"; this.elements.fileTree.innerHTML = `
📂 No folder opened
Click "Open Folder" to start
📄 No file selected
Click a file in the tree to preview
📄 Loading file...
${escapedContent}
`;
// Apply syntax highlighting
if (this.state.syntaxHighlighting && window.Prism) {
const codeBlock = this.elements.filePreview.querySelector("code");
Prism.highlightElement(codeBlock);
}
} catch (err) {
console.error("Failed to preview file:", err);
Utils.toast.error("Failed to preview file");
}
}
async copyFileContent() {
if (!this.state.currentFile) return;
try {
const content = await FileSystem.readFile(this.state.currentFile);
await Utils.copyToClipboard(content);
} catch (err) {
console.error("Failed to copy:", err);
Utils.toast.error("Failed to copy file content");
}
}
searchFiles(query) {
if (!query) {
this.renderFileTree();
return;
}
const results = FileSystem.searchFiles(query);
this.elements.fileTree.innerHTML = "";
if (results.length === 0) {
this.elements.fileTree.innerHTML = `
No files found
No chat history yet
`; return; } // Add read-only notice at the top let html = `📖 Read-only history — Start a new chat to continue coding!
`; html += chats .map(chat => { const date = new Date(chat.timestamp); // Escape ALL user-controlled content to prevent XSS const escapedModel = Utils.escapeHtml(chat.model || "unknown"); const escapedUserMsg = Utils.escapeHtml(Utils.truncate(chat.userMessage, 100)); const escapedAssistantMsg = Utils.escapeHtml(Utils.truncate(chat.assistantMessage, 150)); const escapedDate = Utils.escapeHtml(date.toLocaleString()); const escapedFolderName = Utils.escapeHtml(chat.folderName || "Unknown"); return `${escapedContent}`;
// Apply syntax highlighting
if (this.state.syntaxHighlighting && window.Prism) {
const codeBlock = viewerContent.querySelector("code");
Prism.highlightElement(codeBlock);
}
// Show/hide artifact button
const artifactBtn = document.getElementById("btn-viewer-artifact");
const canShowArtifact = ["html", "htm"].includes(this.state.currentFile.extension);
artifactBtn.style.display = canShowArtifact ? "block" : "none";
// Setup buttons
document.getElementById("btn-viewer-copy").onclick = () => this.copyFileContent();
document.getElementById("btn-viewer-artifact").onclick = () => {
Utils.modal.close("modal-code-viewer");
this.viewAsArtifact();
};
// Open modal
Utils.modal.open("modal-code-viewer");
} catch (err) {
console.error("Failed to view file:", err);
Utils.toast.error("Failed to open file viewer");
}
}
// Artifact Preview (HTML Live Preview)
async viewAsArtifact() {
if (!this.state.currentFile) return;
try {
const content = await FileSystem.readFile(this.state.currentFile);
// Update modal
document.getElementById("artifact-filename").textContent = this.state.currentFile.name;
// Resolve dependencies (CSS/JS in same folder)
const processedContent = await this.resolveArtifactDependencies(content);
// Load into iframe
const iframe = document.getElementById("artifact-iframe");
// Security: sandbox restricts capabilities
// allow-scripts: needed for JS in artifacts
// allow-forms: needed for form submission preview
// NOTE: allow-same-origin removed for security (prevents parent access)
iframe.setAttribute("sandbox", "allow-scripts allow-forms");
iframe.srcdoc = processedContent;
// Setup buttons
document.getElementById("btn-artifact-refresh").onclick = () => {
iframe.srcdoc = processedContent;
Utils.toast.success("Artifact refreshed");
};
document.getElementById("btn-artifact-code").onclick = () => {
Utils.modal.close("modal-artifact");
this.viewFileFullscreen();
};
// Open modal
Utils.modal.open("modal-artifact");
} catch (err) {
console.error("Failed to view artifact:", err);
Utils.toast.error("Failed to preview artifact");
}
}
// Resolve CSS/JS dependencies in HTML
async resolveArtifactDependencies(htmlContent) {
// Get current file's directory
const currentPath = this.state.currentFile.path;
const dirPath = currentPath.substring(0, currentPath.lastIndexOf("/"));
// Find all local references (href/src with relative paths)
let processed = htmlContent;
// Match CSS links
const cssRegex = /]+href=["'](?!http|https|\/\/)([^"']+)["'][^>]*>/gi;
const cssMatches = [...htmlContent.matchAll(cssRegex)];
for (const match of cssMatches) {
const relativePath = match[1];
const fullPath = dirPath ? `${dirPath}/${relativePath}` : relativePath;
const cssFile = FileSystem.getFileByPath(fullPath);
if (cssFile) {
try {
const cssContent = await FileSystem.readFile(cssFile);
// Replace link tag with inline style
processed = processed.replace(match[0], ``);
} catch (err) {
console.warn(`Could not load CSS: ${fullPath}`);
}
}
}
// Match JS scripts
const jsRegex = /`);
} catch (err) {
console.warn(`Could not load JS: ${fullPath}`);
}
}
}
return processed;
}
}
// Create global app instance
const app = new App();
// Initialize on DOM ready
document.addEventListener("DOMContentLoaded", () => {
app.init();
});
// Export for window access (for inline onclick handlers)
window.app = app;
export default app;