228 lines
7.9 KiB
JavaScript
228 lines
7.9 KiB
JavaScript
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);
|
|
}
|
|
}
|
|
}
|