CodeScribe / static /script.js
Rahul-Samedavar's picture
allset
3e802a5
document.addEventListener("DOMContentLoaded", () => {
// --- Constants ---
const GITHUB_TOKEN_KEY = "github_access_token";
// --- DOM Element Cache ---
const docForm = document.getElementById("doc-form");
const submitBtn = document.getElementById("submit-btn");
const authSection = document.getElementById("auth-section");
const mainContent = document.getElementById("main-content");
const githubLoginBtn = document.getElementById("github-login-btn");
const selectZipBtn = document.getElementById("select-zip-btn");
const selectGithubBtn = document.getElementById("select-github-btn");
const zipInputs = document.getElementById("zip-inputs");
const githubInputs = document.getElementById("github-inputs");
const zipFileInput = document.getElementById("zip-file");
const repoSelect = document.getElementById("repo-select");
const baseBranchSelect = document.getElementById("base-branch-select");
const newBranchInput = document.getElementById("new-branch-input");
const branchNameError = document.getElementById("branch-name-error");
const fileTreeContainer = document.getElementById("file-tree-container");
const fileTree = document.getElementById("file-tree");
const liveProgressView = document.getElementById("live-progress-view");
const resultSection = document.getElementById("result-section");
const resultLink = document.getElementById("result-link");
const logOutput = document.getElementById("log-output");
// --- Helper Functions ---
const showView = (viewId) => {
[authSection, mainContent, liveProgressView, resultSection].forEach((el) =>
el.classList.add("hidden")
);
document.getElementById(viewId).classList.remove("hidden");
};
const sanitizeForId = (str) =>
`subtask-${str.replace(/[^a-zA-Z0-9-]/g, "-")}`;
const resetProgressView = () => {
document
.querySelectorAll(".phase-item")
.forEach((item) => (item.dataset.status = "pending"));
document
.querySelectorAll(".subtask-list")
.forEach((list) => (list.innerHTML = ""));
logOutput.textContent = "";
};
const createTreeHtml = (nodes, pathPrefix = "") => {
let html = "<ul>";
nodes.forEach((node) => {
const fullPath = pathPrefix ? `${pathPrefix}/${node.name}` : node.name;
const isDir = !!node.children;
html += `<li><input type="checkbox" name="exclude_paths" value="${fullPath}" id="cb-${fullPath}"> <label for="cb-${fullPath}"><strong>${
node.name
}${isDir ? "/" : ""}</strong></label>`;
if (isDir) html += createTreeHtml(node.children, fullPath);
html += "</li>";
});
html += "</ul>";
return html;
};
// --- Core Logic (unchanged app behavior) ---
const checkBranchName = async () => {
const repoFullName = repoSelect.value;
const branchName = newBranchInput.value.trim();
const token = localStorage.getItem(GITHUB_TOKEN_KEY);
branchNameError.textContent = "";
branchNameError.style.display = "none";
if (
!repoFullName ||
!branchName ||
!token ||
!selectGithubBtn.classList.contains("active")
) {
submitBtn.disabled = false;
return;
}
submitBtn.disabled = true;
submitBtn
.querySelector(".btn-inner span:last-child")
?.replaceWith(document.createTextNode("Checking branch..."));
try {
const response = await fetch(
`/api/github/branch-exists?repo_full_name=${encodeURIComponent(
repoFullName
)}&branch_name=${encodeURIComponent(branchName)}`,
{
headers: { Authorization: `Bearer ${token}` },
}
);
if (!response.ok) {
const errData = await response.json();
throw new Error(errData.detail || `Server error: ${response.status}`);
}
const data = await response.json();
if (data.exists) {
branchNameError.textContent = `Branch '${branchName}' already exists. Please choose another name.`;
branchNameError.style.display = "block";
submitBtn.disabled = true;
} else {
submitBtn.disabled = false;
}
} catch (error) {
console.error("Error checking branch name:", error);
branchNameError.textContent = `Could not verify branch name. ${error.message}`;
branchNameError.style.display = "block";
submitBtn.disabled = false; // Allow submission, server will catch it if it's a real issue.
} finally {
// Restore button text
const inner = submitBtn.querySelector(".btn-inner");
if (inner) {
inner.innerHTML =
'<span class="loader" aria-hidden="true"></span><span>Generate Documentation</span>';
}
}
};
const handleAuth = () => {
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get("token");
const error = urlParams.get("error");
if (error) alert(`Authentication failed: ${error}`);
if (token && token !== "None")
localStorage.setItem(GITHUB_TOKEN_KEY, token);
window.history.replaceState({}, document.title, "/");
if (localStorage.getItem(GITHUB_TOKEN_KEY)) {
// --- Logged In User Flow ---
showView("main-content");
fetchGithubRepos();
switchMode("github");
} else {
// --- Logged Out User Flow (FIXED) ---
// 1. Always show the main form content, not the separate auth screen.
// This makes the form, including ZIP upload, always accessible.
showView("main-content");
// 2. The auth section with the login button should be visible.
// We manually un-hide it because showView is designed to show only one view.
authSection.classList.remove("hidden");
// 3. Default to ZIP mode and disable the GitHub tab.
switchMode("zip");
selectGithubBtn.disabled = true;
}
};
const fetchGithubRepos = async () => {
const token = localStorage.getItem(GITHUB_TOKEN_KEY);
if (!token) return;
try {
const response = await fetch("/api/github/repos", {
headers: { Authorization: `Bearer ${token}` },
});
if (response.status === 401) {
localStorage.removeItem(GITHUB_TOKEN_KEY);
alert("GitHub session expired. Please log in again.");
handleAuth();
return;
}
if (!response.ok) throw new Error("Failed to fetch repos");
const repos = await response.json();
repoSelect.innerHTML =
'<option value="">-- Select a repository --</option>';
repos.forEach((repo) => {
const option = document.createElement("option");
option.value = repo.full_name;
option.textContent = repo.full_name;
option.dataset.defaultBranch = repo.default_branch;
repoSelect.appendChild(option);
});
} catch (error) {
console.error(error);
}
};
const fetchRepoBranches = async (repoFullName, defaultBranch) => {
const token = localStorage.getItem(GITHUB_TOKEN_KEY);
if (!token || !repoFullName) return;
baseBranchSelect.innerHTML = "<option>Loading branches...</option>";
baseBranchSelect.disabled = true;
try {
const response = await fetch(
`/api/github/branches?repo_full_name=${repoFullName}`,
{
headers: { Authorization: `Bearer ${token}` },
}
);
if (!response.ok) throw new Error("Failed to fetch branches");
const branches = await response.json();
baseBranchSelect.innerHTML = "";
branches.forEach((branchName) => {
const option = document.createElement("option");
option.value = branchName;
option.textContent = branchName;
if (branchName === defaultBranch) option.selected = true;
baseBranchSelect.appendChild(option);
});
} catch (error) {
console.error(error);
baseBranchSelect.innerHTML = `<option>Error loading branches</option>`;
} finally {
baseBranchSelect.disabled = false;
}
};
const fetchAndBuildTree = async (repoFullName, branch) => {
fileTreeContainer.classList.remove("hidden");
fileTree.innerHTML = "<em>Loading repository file tree...</em>";
const token = localStorage.getItem(GITHUB_TOKEN_KEY);
if (!token || !repoFullName || !branch) return;
try {
const response = await fetch(
`/api/github/tree?repo_full_name=${repoFullName}&branch=${branch}`,
{
headers: { Authorization: `Bearer ${token}` },
}
);
if (!response.ok)
throw new Error(
`Failed to fetch file tree (status: ${response.status})`
);
const treeData = await response.json();
fileTree.innerHTML = createTreeHtml(treeData);
} catch (error) {
console.error(error);
fileTree.innerHTML = `<em style="color: #ef4444;">${error.message}</em>`;
}
};
// --- In script.js ---
const switchMode = (mode) => {
fileTreeContainer.classList.add("hidden");
if (mode === "github") {
selectGithubBtn.classList.add("active");
selectZipBtn.classList.remove("active");
githubInputs.classList.remove("hidden");
zipInputs.classList.add("hidden");
// GitHub Mode: ZIP is not required, GitHub fields are.
zipFileInput.required = false;
repoSelect.required = true;
baseBranchSelect.required = true;
newBranchInput.required = true;
if (repoSelect.value)
fetchAndBuildTree(repoSelect.value, baseBranchSelect.value);
} else { // This is ZIP mode
selectZipBtn.classList.add("active");
selectGithubBtn.classList.remove("active");
zipInputs.classList.remove("hidden");
githubInputs.classList.add("hidden");
// ZIP Mode: ZIP is required, GitHub fields are not.
zipFileInput.required = true;
repoSelect.required = false;
baseBranchSelect.required = false; // <-- FIX: Added this line
newBranchInput.required = false; // <-- FIX: Added this line
}
};
const handleFormSubmit = (e) => {
e.preventDefault();
resetProgressView();
showView("live-progress-view");
submitBtn.disabled = true;
const formData = new FormData(docForm);
const endpoint = selectGithubBtn.classList.contains("active")
? "/process-github"
: "/process-zip";
const headers = new Headers();
if (endpoint === "/process-github") {
headers.append(
"Authorization",
`Bearer ${localStorage.getItem(GITHUB_TOKEN_KEY)}`
);
}
const options = { method: "POST", body: formData, headers: headers };
fetch(endpoint, options)
.then((response) => {
if (!response.ok) {
return response.json().then((errData) => {
throw new Error(
`Server error: ${response.status} - ${
errData.detail || "Unknown error"
}`
);
});
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
function push() {
reader
.read()
.then(({ done, value }) => {
if (done) {
if (buffer) {
try {
const json = JSON.parse(buffer);
handleStreamEvent(json.type, json.payload);
} catch (e) {
console.error(
"Error parsing final buffer chunk:",
buffer,
e
);
}
}
return;
}
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop();
for (const line of lines) {
if (line.trim() === "") continue;
try {
const json = JSON.parse(line);
handleStreamEvent(json.type, json.payload);
} catch (e) {
console.error("Failed to parse JSON line:", line, e);
}
}
push();
})
.catch((err) => {
console.error("Stream reading error:", err);
handleStreamEvent("error", `Stream error: ${err.message}`);
});
}
push();
})
.catch((err) => handleStreamEvent("error", `${err.message}`));
};
const handleStreamEvent = (type, payload) => {
switch (type) {
case "phase": {
const phaseEl = document.getElementById(`phase-${payload.id}`);
if (phaseEl) phaseEl.dataset.status = payload.status;
break;
}
case "subtask": {
const subtaskId = sanitizeForId(payload.id);
let subtaskEl = document.getElementById(subtaskId);
const listEl = document.getElementById(payload.listId);
if (!subtaskEl && listEl) {
subtaskEl = document.createElement("li");
subtaskEl.id = subtaskId;
subtaskEl.textContent = payload.name;
listEl.appendChild(subtaskEl);
}
if (subtaskEl) subtaskEl.dataset.status = payload.status;
break;
}
case "log": {
logOutput.textContent += payload.message.replace(/\\n/g, "\n") + "\n";
logOutput.scrollTop = logOutput.scrollHeight;
break;
}
case "error": {
document
.querySelectorAll('.phase-item[data-status="in-progress"]')
.forEach((el) => (el.dataset.status = "error"));
logOutput.textContent += `\n\n--- ERROR ---\n${payload}\n`;
submitBtn.disabled = false;
break;
}
case "done": {
showView("result-section");
resultLink.innerHTML = `<p>${payload.message}</p>`;
if (payload.type === "zip") {
resultLink.innerHTML += `<a href="/download/${payload.download_path}" class="button-link" download>Download ZIP</a>`;
} else if (payload.url) {
const linkText = payload.url.includes("/pull/new/")
? "Create Pull Request"
: "View Repository";
resultLink.innerHTML += `<a href="${payload.url}" target="_blank" rel="noopener noreferrer" class="button-link">${linkText}</a>`;
}
submitBtn.disabled = false;
break;
}
}
};
// --- Events ---
githubLoginBtn.addEventListener(
"click",
() => (window.location.href = "/login/github")
);
selectZipBtn.addEventListener("click", () => switchMode("zip"));
selectGithubBtn.addEventListener("click", () => switchMode("github"));
docForm.addEventListener("submit", handleFormSubmit);
repoSelect.addEventListener("change", async (e) => {
const selectedOption = e.target.options[e.target.selectedIndex];
const repoFullName = selectedOption.value;
const defaultBranch = selectedOption.dataset.defaultBranch || "";
if (repoFullName) {
await fetchRepoBranches(repoFullName, defaultBranch);
fetchAndBuildTree(repoFullName, baseBranchSelect.value);
checkBranchName();
} else {
baseBranchSelect.innerHTML = "";
fileTreeContainer.classList.add("hidden");
}
});
baseBranchSelect.addEventListener("change", () => {
fetchAndBuildTree(repoSelect.value, baseBranchSelect.value);
});
newBranchInput.addEventListener("blur", checkBranchName);
// Initialize
handleAuth();
// --- UI Enhancements: glow cursor on buttons ---
document.querySelectorAll(".btn-glow").forEach((btn) => {
btn.addEventListener("pointermove", (e) => {
const rect = btn.getBoundingClientRect();
const x = ((e.clientX - rect.left) / rect.width) * 100;
const y = ((e.clientY - rect.top) / rect.height) * 100;
btn.style.setProperty("--x", x + "%");
btn.style.setProperty("--y", y + "%");
});
});
// --- Ripple effect for buttons and links ---
function addRipple(e) {
const el = e.currentTarget;
const rect = el.getBoundingClientRect();
const circle = document.createElement("span");
circle.className = "ripple";
circle.style.left = `${e.clientX - rect.left}px`;
circle.style.top = `${e.clientY - rect.top}px`;
el.appendChild(circle);
setTimeout(() => circle.remove(), 600);
}
document.querySelectorAll("button.btn, .button-link").forEach((el) => {
el.addEventListener("click", addRipple);
});
// --- Subtle parallax tilt on hover for panels and phase items ---
const tiltEls = document.querySelectorAll(
".panel, .phase-item, .terminal, .glass-card"
);
tiltEls.forEach((el) => {
let enter = false;
el.addEventListener("pointerenter", () => {
enter = true;
});
el.addEventListener("pointerleave", () => {
enter = false;
el.style.transform = "";
});
el.addEventListener("pointermove", (e) => {
if (!enter) return;
const rect = el.getBoundingClientRect();
const cx = rect.left + rect.width / 2;
const cy = rect.top + rect.height / 2;
const dx = (e.clientX - cx) / rect.width;
const dy = (e.clientY - cy) / rect.height;
const max = 6;
el.style.transform = `rotateX(${(-dy * max).toFixed(2)}deg) rotateY(${(
dx * max
).toFixed(2)}deg) translateZ(0)`;
});
});
// --- Optional: lightweight animated background canvas (respects reduced motion) ---
const prefersReduced = window.matchMedia(
"(prefers-reduced-motion: reduce)"
).matches;
const canvas = document.getElementById("bg-canvas");
if (canvas && !prefersReduced) {
const ctx = canvas.getContext("2d", { alpha: true });
let w, h, dots;
function resize() {
w = canvas.width = window.innerWidth;
h = canvas.height = window.innerHeight;
dots = Array.from(
{ length: Math.min(90, Math.floor((w * h) / 60000)) },
() => ({
x: Math.random() * w,
y: Math.random() * h,
vx: (Math.random() - 0.5) * 0.4,
vy: (Math.random() - 0.5) * 0.4,
})
);
}
function step() {
ctx.clearRect(0, 0, w, h);
ctx.fillStyle = "rgba(34, 211, 238, 0.6)";
const threshold = 120;
for (let i = 0; i < dots.length; i++) {
const a = dots[i];
a.x += a.vx;
a.y += a.vy;
if (a.x < 0 || a.x > w) a.vx *= -1;
if (a.y < 0 || a.y > h) a.vy *= -1;
ctx.beginPath();
ctx.arc(a.x, a.y, 1.2, 0, Math.PI * 2);
ctx.fill();
for (let j = i + 1; j < dots.length; j++) {
const b = dots[j];
const dx = a.x - b.x,
dy = a.y - b.y;
const dist = Math.hypot(dx, dy);
if (dist < threshold) {
const alpha = (1 - dist / threshold) * 0.2;
ctx.strokeStyle = `rgba(96,165,250,${alpha})`;
ctx.beginPath();
ctx.moveTo(a.x, a.y);
ctx.lineTo(b.x, b.y);
ctx.stroke();
}
}
}
requestAnimationFrame(step);
}
window.addEventListener("resize", resize);
resize();
step();
}
});