export class AppView { constructor(reviewStore) { this.reviewStore = reviewStore; this.el = { layout: document.getElementById("layout-root"), splitterLeft: document.getElementById("splitter-left"), splitterRight: document.getElementById("splitter-right"), pickFallback: document.getElementById("pick-project-fallback"), projectName: document.getElementById("project-name"), indexStatus: document.getElementById("index-status"), treeInfo: document.getElementById("tree-info"), treeRoot: document.getElementById("tree-root"), fileTabs: document.getElementById("file-tabs"), newTextTabBtn: document.getElementById("new-text-tab"), mdToggleBtn: document.getElementById("md-toggle-mode"), fileEditor: document.getElementById("file-editor"), mdPreview: document.getElementById("md-preview"), editorInfo: document.getElementById("editor-info"), saveFileBtn: document.getElementById("save-file"), closeFileBtn: document.getElementById("close-file"), diffView: document.getElementById("diff-view"), changeList: document.getElementById("change-list"), toolbar: document.getElementById("review-toolbar"), applyAccepted: document.getElementById("apply-accepted"), newChatSessionBtn: document.getElementById("new-chat-session"), chatLog: document.getElementById("chat-log"), chatForm: document.getElementById("chat-form"), chatInput: document.getElementById("chat-input") }; } setIndexStatus(text) { if (this.el.indexStatus) this.el.indexStatus.textContent = text; } setProjectName(name) { if (this.el.projectName) this.el.projectName.textContent = name; } setTreeStats(totalFiles, totalBytes) { const kb = Math.round((totalBytes || 0) / 1024); this.el.treeInfo.textContent = `Файлов: ${totalFiles || 0} • ${kb} KB`; } setApplyEnabled(enabled) { this.el.applyAccepted.disabled = !enabled; } setEditorEnabled(enabled) { this.el.fileEditor.readOnly = !enabled; } bindEditorInput(onInput) { this.el.fileEditor.oninput = () => onInput(this.el.fileEditor.value); } bindMarkdownToggle(onToggle) { this.el.mdToggleBtn.onclick = onToggle; } bindEditorActions(onSave, onClose) { this.el.saveFileBtn.onclick = onSave; this.el.closeFileBtn.onclick = onClose; } bindNewTextTab(onCreate) { this.el.newTextTabBtn.onclick = onCreate; } bindNewChatSession(onNewSession) { this.el.newChatSessionBtn.onclick = onNewSession; } clearChat() { this.el.chatLog.innerHTML = ""; } setEditorActionsState({ hasFile, isDirty, infoText }) { this.el.saveFileBtn.disabled = !hasFile || !isDirty; this.el.closeFileBtn.disabled = !hasFile; this.el.editorInfo.textContent = infoText || "Файл не выбран"; } setMarkdownToggleVisible(visible) { this.el.mdToggleBtn.classList.toggle("hidden", !visible); if (!visible) { this.el.mdPreview.classList.add("hidden"); this.el.fileEditor.classList.remove("hidden"); } } setMarkdownMode(mode) { const isPreview = mode === "preview"; this.el.mdToggleBtn.classList.toggle("active", isPreview); this.el.mdToggleBtn.textContent = isPreview ? "✏️" : "👁"; this.el.mdToggleBtn.title = isPreview ? "Перейти в редактирование" : "Перейти в просмотр"; this.el.mdPreview.classList.toggle("hidden", !isPreview); this.el.fileEditor.classList.toggle("hidden", isPreview); } renderMarkdown(html) { this.el.mdPreview.innerHTML = html; } appendChat(role, text) { if (!["user", "assistant"].includes(role)) return; const div = document.createElement("div"); div.className = "chat-entry"; div.textContent = `[${role}] ${text}`; this.el.chatLog.appendChild(div); this.el.chatLog.scrollTop = this.el.chatLog.scrollHeight; } renderTree(rootNode, selectedPath, onSelect) { this.el.treeRoot.innerHTML = ""; if (!rootNode) { this.el.treeRoot.textContent = "Директория не выбрана"; return; } if (!rootNode.children?.length) { this.el.treeRoot.textContent = "В выбранной директории нет файлов"; return; } const renderNode = (node, depth) => { const line = document.createElement("div"); line.className = "tree-item"; line.style.paddingLeft = `${depth * 14}px`; const marker = node.type === "dir" ? "📁" : "📄"; const suffix = node.type === "file" && node.supported === false ? " (skip)" : ""; line.textContent = `${marker} ${node.name}${suffix}`; if (node.path === selectedPath) line.style.fontWeight = "700"; if (node.type === "file" && node.supported !== false) line.onclick = () => onSelect(node.path); this.el.treeRoot.appendChild(line); if (node.children) node.children.forEach((child) => renderNode(child, depth + 1)); }; renderNode(rootNode, 0); } renderFileTabs(openPaths, activePath, dirtyPaths, onTabClick, onCloseTab, onRenameTab) { this.el.fileTabs.innerHTML = ""; for (const path of openPaths) { const tab = document.createElement("div"); tab.className = `tab-item ${path === activePath ? "active" : ""} ${dirtyPaths.has(path) ? "dirty" : ""}`; tab.title = path; const openBtn = document.createElement("button"); openBtn.className = "tab-main"; openBtn.textContent = this.#formatTabLabel(path); openBtn.title = path; openBtn.onclick = () => onTabClick(path); openBtn.ondblclick = () => onRenameTab(path); const closeBtn = document.createElement("button"); closeBtn.className = "tab-close"; closeBtn.type = "button"; closeBtn.textContent = "x"; closeBtn.onclick = (event) => { event.stopPropagation(); onCloseTab(path); }; tab.append(openBtn, closeBtn); this.el.fileTabs.appendChild(tab); } } #formatTabLabel(path) { const normalized = (path || "").replaceAll("\\", "/"); const baseName = normalized.includes("/") ? normalized.split("/").pop() : normalized; if (baseName.length <= 32) return baseName; return `${baseName.slice(0, 29)}...`; } renderFile(content) { this.el.fileEditor.value = content || ""; } renderChanges(changes, activePath, onPick) { this.el.changeList.innerHTML = ""; if (!changes.length) { this.el.toolbar.classList.add("hidden"); this.el.diffView.innerHTML = ""; return; } this.el.toolbar.classList.remove("hidden"); for (const change of changes) { const review = this.reviewStore.get(change.path); const btn = document.createElement("button"); btn.className = `change-btn ${change.path === activePath ? "active" : ""}`; btn.textContent = `${change.op} ${change.path} [${review?.status || "pending"}]`; btn.onclick = () => onPick(change.path); this.el.changeList.appendChild(btn); } } renderDiff(change, onToggleLine) { this.el.diffView.innerHTML = ""; if (!change) return; const review = this.reviewStore.get(change.path); for (const op of change.diffOps) { const row = document.createElement("div"); row.className = `diff-line ${op.kind}`; const marker = document.createElement("span"); if (op.kind === "equal") marker.textContent = " "; else { const cb = document.createElement("input"); cb.type = "checkbox"; cb.checked = review?.stagedSelection?.has(op.id) || false; cb.onchange = () => onToggleLine(change.path, op.id); marker.appendChild(cb); } const text = document.createElement("span"); if (op.kind === "add") text.textContent = `+ ${op.newLine}`; else if (op.kind === "remove") text.textContent = `- ${op.oldLine}`; else text.textContent = ` ${op.oldLine}`; row.append(marker, text); this.el.diffView.appendChild(row); } } }