Первая версия

This commit is contained in:
2026-02-23 09:08:15 +03:00
commit 75fbb53390
23 changed files with 2498 additions and 0 deletions
+227
View File
@@ -0,0 +1,227 @@
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);
}
}
}