|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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() {
|
|
|
|
|
|
this.loadSettings();
|
|
|
|
|
|
|
|
|
document.querySelectorAll(".modal").forEach(m => m.classList.remove("active"));
|
|
|
|
|
|
|
|
|
Chat.init();
|
|
|
|
|
|
|
|
|
this.setupEventListeners();
|
|
|
|
|
|
|
|
|
if (!FileSystem.isSupported()) {
|
|
|
Utils.toast.warning("File System Access API not supported. Please use Chrome or Edge browser.");
|
|
|
}
|
|
|
|
|
|
|
|
|
this.setupKeyboardShortcuts();
|
|
|
|
|
|
|
|
|
this.setupDragAndDrop();
|
|
|
|
|
|
|
|
|
this.applyTheme(this.state.theme);
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
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");
|
|
|
});
|
|
|
|
|
|
|
|
|
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");
|
|
|
});
|
|
|
|
|
|
|
|
|
overlay.addEventListener("click", () => {
|
|
|
sidebarLeft.classList.remove("visible");
|
|
|
sidebarRight.classList.remove("visible");
|
|
|
overlay.classList.remove("active");
|
|
|
btnToggleLeft.classList.remove("active");
|
|
|
btnToggleRight.classList.remove("active");
|
|
|
});
|
|
|
|
|
|
|
|
|
if (!isMobile()) {
|
|
|
btnToggleLeft.classList.add("active");
|
|
|
btnToggleRight.classList.add("active");
|
|
|
}
|
|
|
|
|
|
|
|
|
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 => {
|
|
|
|
|
|
if (e.ctrlKey && e.key === "o") {
|
|
|
e.preventDefault();
|
|
|
this.openFolder();
|
|
|
}
|
|
|
|
|
|
if (e.ctrlKey && e.key === "k") {
|
|
|
e.preventDefault();
|
|
|
this.elements.searchFiles.focus();
|
|
|
}
|
|
|
|
|
|
if (e.key === "Escape") {
|
|
|
document.querySelectorAll(".modal.active").forEach(modal => {
|
|
|
Utils.modal.close(modal.id);
|
|
|
});
|
|
|
}
|
|
|
|
|
|
if (e.ctrlKey && e.key === "Enter" && document.activeElement === document.getElementById("chat-input")) {
|
|
|
e.preventDefault();
|
|
|
Chat.sendMessage();
|
|
|
}
|
|
|
|
|
|
if ((e.key === "ArrowUp" || e.key === "ArrowDown") && document.activeElement.closest(".file-tree")) {
|
|
|
e.preventDefault();
|
|
|
this.navigateFileTree(e.key === "ArrowUp" ? -1 : 1);
|
|
|
}
|
|
|
|
|
|
if (e.key === "Enter" && document.activeElement.classList.contains("tree-item")) {
|
|
|
e.preventDefault();
|
|
|
const path = document.activeElement.dataset.path;
|
|
|
if (path) this.previewFile(path);
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
setupEventListeners() {
|
|
|
|
|
|
this.elements.btnOpenFolder.addEventListener("click", () => this.openFolder());
|
|
|
this.elements.btnCloseFolder.addEventListener("click", () => this.closeFolder());
|
|
|
|
|
|
|
|
|
this.elements.btnSettings.addEventListener("click", () => {
|
|
|
this.populateSettings();
|
|
|
Utils.modal.open("modal-settings");
|
|
|
});
|
|
|
|
|
|
this.elements.btnSaveSettings.addEventListener("click", () => {
|
|
|
this.saveSettings();
|
|
|
Utils.modal.close("modal-settings");
|
|
|
});
|
|
|
|
|
|
|
|
|
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();
|
|
|
}
|
|
|
});
|
|
|
|
|
|
|
|
|
this.setupSidebarToggles();
|
|
|
|
|
|
|
|
|
this.elements.searchFiles.addEventListener(
|
|
|
"input",
|
|
|
Utils.debounce(e => this.searchFiles(e.target.value), 300)
|
|
|
);
|
|
|
|
|
|
|
|
|
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 ? "π" : "ποΈ";
|
|
|
});
|
|
|
}
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
if (window.Chat) {
|
|
|
window.Chat.maxHistoryMessages = this.state.maxHistoryMessages;
|
|
|
}
|
|
|
|
|
|
|
|
|
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...");
|
|
|
|
|
|
|
|
|
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";
|
|
|
|
|
|
|
|
|
Chat.updateContextInfo();
|
|
|
|
|
|
|
|
|
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() {
|
|
|
|
|
|
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();
|
|
|
|
|
|
|
|
|
this.elements.folderInfo.style.display = "none";
|
|
|
this.elements.btnOpenFolder.style.display = "block";
|
|
|
this.elements.btnCloseFolder.style.display = "none";
|
|
|
this.elements.fileTree.innerHTML = `
|
|
|
<div class="empty-state">
|
|
|
<p>π No folder opened</p>
|
|
|
<p class="hint">Click "Open Folder" to start</p>
|
|
|
</div>
|
|
|
`;
|
|
|
this.elements.filePreview.innerHTML = `
|
|
|
<div class="empty-state">
|
|
|
<p>π No file selected</p>
|
|
|
<p class="hint">Click a file in the tree to preview</p>
|
|
|
</div>
|
|
|
`;
|
|
|
|
|
|
|
|
|
Chat.updateContextInfo();
|
|
|
|
|
|
Utils.toast.info("Folder closed");
|
|
|
}
|
|
|
|
|
|
renderFileTree() {
|
|
|
const tree = FileSystem.buildTree();
|
|
|
this.elements.fileTree.innerHTML = "";
|
|
|
|
|
|
|
|
|
const shouldCollapseByDefault = FileSystem.files.length > 100;
|
|
|
|
|
|
const renderNode = (node, parentEl, level = 0) => {
|
|
|
if (node.type === "directory") {
|
|
|
const folderEl = document.createElement("div");
|
|
|
folderEl.className = "tree-folder";
|
|
|
|
|
|
const isCollapsed = shouldCollapseByDefault && level > 0;
|
|
|
|
|
|
const folderHeader = document.createElement("div");
|
|
|
folderHeader.className = "tree-item tree-folder-header";
|
|
|
folderHeader.style.paddingLeft = `${level * 1.5}rem`;
|
|
|
folderHeader.innerHTML = `
|
|
|
<span class="icon">${isCollapsed ? "π" : "π"}</span>
|
|
|
<span>${node.name}</span>
|
|
|
`;
|
|
|
|
|
|
|
|
|
folderHeader.addEventListener("click", () => {
|
|
|
const isCurrentlyCollapsed = folderHeader.querySelector(".icon").textContent === "π";
|
|
|
folderHeader.querySelector(".icon").textContent = isCurrentlyCollapsed ? "π" : "π";
|
|
|
childrenContainer.style.display = isCurrentlyCollapsed ? "block" : "none";
|
|
|
});
|
|
|
|
|
|
folderEl.appendChild(folderHeader);
|
|
|
|
|
|
const childrenContainer = document.createElement("div");
|
|
|
childrenContainer.className = "tree-children";
|
|
|
childrenContainer.style.display = isCollapsed ? "none" : "block";
|
|
|
|
|
|
if (node.children) {
|
|
|
node.children.forEach(child => renderNode(child, childrenContainer, level + 1));
|
|
|
}
|
|
|
|
|
|
folderEl.appendChild(childrenContainer);
|
|
|
parentEl.appendChild(folderEl);
|
|
|
} else {
|
|
|
const fileEl = document.createElement("div");
|
|
|
fileEl.className = "tree-item";
|
|
|
fileEl.style.paddingLeft = `${level * 1.5}rem`;
|
|
|
fileEl.dataset.path = node.path;
|
|
|
fileEl.innerHTML = `
|
|
|
<span class="icon">π</span>
|
|
|
<span>${node.name}</span>
|
|
|
`;
|
|
|
|
|
|
fileEl.addEventListener("click", e => {
|
|
|
|
|
|
document.querySelectorAll(".tree-item").forEach(el => el.classList.remove("active"));
|
|
|
fileEl.classList.add("active");
|
|
|
this.previewFile(node.path);
|
|
|
|
|
|
|
|
|
if (window.innerWidth <= 900) {
|
|
|
const sidebarLeft = document.getElementById("sidebar-left");
|
|
|
const overlay = document.getElementById("sidebar-overlay");
|
|
|
sidebarLeft.classList.remove("visible");
|
|
|
overlay.classList.remove("active");
|
|
|
document.getElementById("btn-toggle-left").classList.remove("active");
|
|
|
}
|
|
|
});
|
|
|
|
|
|
parentEl.appendChild(fileEl);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
if (tree.children) {
|
|
|
tree.children.forEach(child => renderNode(child, this.elements.fileTree));
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async previewFile(path) {
|
|
|
try {
|
|
|
const fileEntry = FileSystem.getFileByPath(path);
|
|
|
if (!fileEntry) return;
|
|
|
|
|
|
|
|
|
document.querySelectorAll(".tree-item").forEach(el => el.classList.remove("active"));
|
|
|
|
|
|
const activeItem = document.querySelector(`.tree-item[data-path="${CSS.escape(fileEntry.path)}"]`);
|
|
|
if (activeItem) activeItem.classList.add("active");
|
|
|
|
|
|
this.state.currentFile = fileEntry;
|
|
|
|
|
|
|
|
|
if (fileEntry.size > 100 * 1024) {
|
|
|
this.elements.filePreview.innerHTML = '<div class="empty-state"><p>π Loading file...</p></div>';
|
|
|
}
|
|
|
|
|
|
|
|
|
const content = await FileSystem.readFile(fileEntry);
|
|
|
|
|
|
|
|
|
const language = fileEntry.language;
|
|
|
const escapedContent = Utils.escapeHtml(content);
|
|
|
const escapedPath = Utils.escapeHtml(fileEntry.path);
|
|
|
const escapedName = Utils.escapeHtml(fileEntry.name);
|
|
|
|
|
|
|
|
|
const canShowArtifact = ["html", "htm"].includes(fileEntry.extension);
|
|
|
|
|
|
this.elements.filePreview.innerHTML = `
|
|
|
<div class="preview-header">
|
|
|
<span class="preview-filename">${escapedPath}</span>
|
|
|
<div class="preview-actions">
|
|
|
<button class="btn-icon" onclick="app.copyFileContent()" title="Copy">π</button>
|
|
|
<button class="btn-icon" onclick="app.viewFileFullscreen()" title="View Fullscreen">π</button>
|
|
|
${canShowArtifact ? '<button class="btn-icon" onclick="app.viewAsArtifact()" title="Preview as Artifact">β¨</button>' : ""}
|
|
|
</div>
|
|
|
</div>
|
|
|
<pre><code class="language-${language}">${escapedContent}</code></pre>
|
|
|
`;
|
|
|
|
|
|
|
|
|
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 = `
|
|
|
<div class="empty-state">
|
|
|
<p>No files found</p>
|
|
|
</div>
|
|
|
`;
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
results.forEach(file => {
|
|
|
const fileEl = document.createElement("div");
|
|
|
fileEl.className = "tree-item";
|
|
|
fileEl.dataset.path = file.path;
|
|
|
fileEl.innerHTML = `
|
|
|
<span class="icon">π</span>
|
|
|
<span>${file.path}</span>
|
|
|
`;
|
|
|
fileEl.addEventListener("click", e => {
|
|
|
document.querySelectorAll(".tree-item").forEach(el => el.classList.remove("active"));
|
|
|
fileEl.classList.add("active");
|
|
|
this.previewFile(file.path);
|
|
|
|
|
|
|
|
|
if (window.innerWidth <= 900) {
|
|
|
const sidebarLeft = document.getElementById("sidebar-left");
|
|
|
const overlay = document.getElementById("sidebar-overlay");
|
|
|
sidebarLeft.classList.remove("visible");
|
|
|
overlay.classList.remove("active");
|
|
|
document.getElementById("btn-toggle-left").classList.remove("active");
|
|
|
}
|
|
|
});
|
|
|
this.elements.fileTree.appendChild(fileEl);
|
|
|
});
|
|
|
}
|
|
|
|
|
|
async loadHistory() {
|
|
|
const chats = await DB.getChats(20);
|
|
|
const historyList = document.getElementById("history-list");
|
|
|
|
|
|
if (chats.length === 0) {
|
|
|
historyList.innerHTML = `<p class="empty-state">No chat history yet</p>`;
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
|
|
|
let html = `<p style="padding: 0.75rem 1rem; background: var(--bg-tertiary); color: var(--text-secondary); font-size: 0.85rem; margin: 0; border-bottom: 1px solid var(--border-color);">
|
|
|
π Read-only history β Start a new chat to continue coding!
|
|
|
</p>`;
|
|
|
|
|
|
html += chats
|
|
|
.map(chat => {
|
|
|
const date = new Date(chat.timestamp);
|
|
|
|
|
|
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 `
|
|
|
<div class="history-item" style="padding: 1rem; border-bottom: 1px solid var(--border-color);">
|
|
|
<div style="font-size: 0.8rem; color: var(--text-secondary); margin-bottom: 0.5rem;">
|
|
|
${escapedDate} β’ ${escapedModel} β’ ${escapedFolderName}
|
|
|
</div>
|
|
|
<div style="margin-bottom: 0.5rem;">
|
|
|
<strong>You:</strong> ${escapedUserMsg}
|
|
|
</div>
|
|
|
<div>
|
|
|
<strong>Assistant:</strong> ${escapedAssistantMsg}
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
})
|
|
|
.join("");
|
|
|
|
|
|
historyList.innerHTML = html;
|
|
|
}
|
|
|
|
|
|
|
|
|
navigateFileTree(direction) {
|
|
|
const items = Array.from(document.querySelectorAll(".tree-item[data-path]"));
|
|
|
if (items.length === 0) return;
|
|
|
|
|
|
const currentIndex = items.findIndex(el => el.classList.contains("active"));
|
|
|
let newIndex = currentIndex + direction;
|
|
|
|
|
|
|
|
|
if (newIndex < 0) newIndex = items.length - 1;
|
|
|
if (newIndex >= items.length) newIndex = 0;
|
|
|
|
|
|
|
|
|
items.forEach(el => el.classList.remove("active"));
|
|
|
items[newIndex].classList.add("active");
|
|
|
items[newIndex].focus();
|
|
|
items[newIndex].scrollIntoView({ block: "nearest" });
|
|
|
}
|
|
|
|
|
|
|
|
|
async viewFileFullscreen() {
|
|
|
if (!this.state.currentFile) return;
|
|
|
|
|
|
try {
|
|
|
const content = await FileSystem.readFile(this.state.currentFile);
|
|
|
const language = this.state.currentFile.language;
|
|
|
const escapedContent = Utils.escapeHtml(content);
|
|
|
|
|
|
|
|
|
document.getElementById("viewer-filename").textContent = `π ${this.state.currentFile.name}`;
|
|
|
document.getElementById("viewer-file-info").textContent =
|
|
|
`${this.state.currentFile.extension} β’ ${Utils.formatFileSize(this.state.currentFile.size)}`;
|
|
|
|
|
|
const viewerContent = document.getElementById("viewer-content");
|
|
|
viewerContent.innerHTML = `<pre><code class="language-${language}">${escapedContent}</code></pre>`;
|
|
|
|
|
|
|
|
|
if (this.state.syntaxHighlighting && window.Prism) {
|
|
|
const codeBlock = viewerContent.querySelector("code");
|
|
|
Prism.highlightElement(codeBlock);
|
|
|
}
|
|
|
|
|
|
|
|
|
const artifactBtn = document.getElementById("btn-viewer-artifact");
|
|
|
const canShowArtifact = ["html", "htm"].includes(this.state.currentFile.extension);
|
|
|
artifactBtn.style.display = canShowArtifact ? "block" : "none";
|
|
|
|
|
|
|
|
|
document.getElementById("btn-viewer-copy").onclick = () => this.copyFileContent();
|
|
|
document.getElementById("btn-viewer-artifact").onclick = () => {
|
|
|
Utils.modal.close("modal-code-viewer");
|
|
|
this.viewAsArtifact();
|
|
|
};
|
|
|
|
|
|
|
|
|
Utils.modal.open("modal-code-viewer");
|
|
|
} catch (err) {
|
|
|
console.error("Failed to view file:", err);
|
|
|
Utils.toast.error("Failed to open file viewer");
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
async viewAsArtifact() {
|
|
|
if (!this.state.currentFile) return;
|
|
|
|
|
|
try {
|
|
|
const content = await FileSystem.readFile(this.state.currentFile);
|
|
|
|
|
|
|
|
|
document.getElementById("artifact-filename").textContent = this.state.currentFile.name;
|
|
|
|
|
|
|
|
|
const processedContent = await this.resolveArtifactDependencies(content);
|
|
|
|
|
|
|
|
|
const iframe = document.getElementById("artifact-iframe");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
iframe.setAttribute("sandbox", "allow-scripts allow-forms");
|
|
|
|
|
|
iframe.srcdoc = processedContent;
|
|
|
|
|
|
|
|
|
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();
|
|
|
};
|
|
|
|
|
|
|
|
|
Utils.modal.open("modal-artifact");
|
|
|
} catch (err) {
|
|
|
console.error("Failed to view artifact:", err);
|
|
|
Utils.toast.error("Failed to preview artifact");
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
async resolveArtifactDependencies(htmlContent) {
|
|
|
|
|
|
const currentPath = this.state.currentFile.path;
|
|
|
const dirPath = currentPath.substring(0, currentPath.lastIndexOf("/"));
|
|
|
|
|
|
|
|
|
let processed = htmlContent;
|
|
|
|
|
|
|
|
|
const cssRegex = /<link[^>]+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);
|
|
|
|
|
|
processed = processed.replace(match[0], `<style>${cssContent}</style>`);
|
|
|
} catch (err) {
|
|
|
console.warn(`Could not load CSS: ${fullPath}`);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
const jsRegex = /<script[^>]+src=["'](?!http|https|\/\/)([^"']+)["'][^>]*><\/script>/gi;
|
|
|
const jsMatches = [...htmlContent.matchAll(jsRegex)];
|
|
|
|
|
|
for (const match of jsMatches) {
|
|
|
const relativePath = match[1];
|
|
|
const fullPath = dirPath ? `${dirPath}/${relativePath}` : relativePath;
|
|
|
const jsFile = FileSystem.getFileByPath(fullPath);
|
|
|
|
|
|
if (jsFile) {
|
|
|
try {
|
|
|
const jsContent = await FileSystem.readFile(jsFile);
|
|
|
|
|
|
processed = processed.replace(match[0], `<script>${jsContent}</script>`);
|
|
|
} catch (err) {
|
|
|
console.warn(`Could not load JS: ${fullPath}`);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return processed;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
const app = new App();
|
|
|
|
|
|
|
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
|
app.init();
|
|
|
});
|
|
|
|
|
|
|
|
|
window.app = app;
|
|
|
|
|
|
export default app;
|
|
|
|