// U-008: hash-based client-side router (no History API / server catch-all // needed). U-022 adds the first real page, #/setup (v_project_setup, 02-V // §2.1); U-023 adds #/goals/new and #/bugs/new -- everything else is still // a PH3 placeholder. // 02-V's console has no multi-project switcher UI (decision 2), but // c_task_create/c_bug_report_create both require project_id (03-C §3.1; // §3.1.1 doesn't list it, but needs it too -- see bug_record.py's // create_bug_report docstring). Until v_dashboard (U-025) exists to carry // "current project" state, the id picked/created in v_project_setup is // stashed here and read back by the pages that need it. const CURRENT_PROJECT_KEY = "graphlemon_project_id"; function getCurrentProjectId() { return localStorage.getItem(CURRENT_PROJECT_KEY); } function setCurrentProjectId(projectId) { localStorage.setItem(CURRENT_PROJECT_KEY, projectId); } const routes = { "#/": { render: () => "

Dashboard

placeholder -- real content is PH3 (v_dashboard, U-025)

" }, "#/tasks": { render: () => "

Tasks

placeholder -- real content is PH3 (v_task_board, U-025)

" }, "#/settings": { render: () => "

Settings

placeholder -- real content is PH3 (U-030)

" }, "#/setup": { render: renderProjectSetup, mount: mountProjectSetup }, "#/goals/new": { render: renderGoalComposer, mount: mountGoalComposer }, "#/bugs/new": { render: renderBugFeedback, mount: mountBugFeedback }, }; function render() { const hash = window.location.hash || "#/"; const route = routes[hash] || { render: () => "

Not found

" + hash + "

" }; document.getElementById("app").innerHTML = route.render(); if (route.mount) route.mount(); for (const a of document.querySelectorAll("#nav a")) { a.classList.toggle("active", a.getAttribute("href") === hash); } } window.addEventListener("hashchange", render); window.addEventListener("DOMContentLoaded", render); // ---- v_project_setup (02-V §2.1): create/switch project ---- function renderProjectSetup() { return `
← 返回仪表盘

创建 / 切换项目

选择一个本地文件夹作为目标项目的代码空间
点击"选择文件夹"会由后端代为弹出系统原生对话框,不支持手动输入路径(除非返回 DIALOG_UNAVAILABLE)
`; } function mountProjectSetup() { const folderInput = document.getElementById("folder_path"); const folderHint = document.getElementById("folder_path_hint"); const nameInput = document.getElementById("project_name"); const errorBox = document.getElementById("setup_error"); const confirmBanner = document.getElementById("confirm_banner"); function showError(message) { errorBox.textContent = message; errorBox.style.display = "block"; } function clearError() { errorBox.style.display = "none"; } document.getElementById("btn_pick_folder").addEventListener("click", async () => { clearError(); const resp = await fetch("/api/system/pick-folder", { method: "POST", body: "{}" }); const body = await resp.json(); if (resp.status === 422 && body.error === "DIALOG_UNAVAILABLE") { folderInput.removeAttribute("readonly"); folderHint.textContent = "当前环境不支持弹窗选择,请手动输入路径"; return; } if (body.selected_path) { folderInput.value = body.selected_path; } // user canceled the native dialog -> folder_path stays as it was (02-V §2.1) }); async function createProject(initGitIfMissing) { clearError(); confirmBanner.style.display = "none"; const folderPath = folderInput.value.trim(); if (!folderPath) { showError("请先选择目标文件夹"); return; } const resp = await fetch("/api/projects", { method: "POST", body: JSON.stringify({ target_folder_path: folderPath, name: nameInput.value.trim() || undefined, init_git_if_missing: initGitIfMissing, }), }); const body = await resp.json(); if (resp.status === 200) { setCurrentProjectId(body.project_id); window.location.hash = "#/"; return; } if (body.error === "GIT_INIT_DECLINED") { confirmBanner.style.display = "block"; return; } showError(body.error + (body.detail ? ":" + body.detail : "")); } document.getElementById("btn_create").addEventListener("click", () => createProject(false)); document.getElementById("btn_confirm_init").addEventListener("click", () => createProject(true)); document.getElementById("btn_cancel_init").addEventListener("click", () => { confirmBanner.style.display = "none"; }); } // ---- shared: pages that need a "current project" to already exist ---- function renderNoProjectSelected() { return `
还没有选择项目。请先创建或切换项目。
`; } // ---- v_goal_composer (02-V §2.3): propose a new goal ---- const GOAL_TEXT_MAX_LEN = 4000; function renderGoalComposer() { if (!getCurrentProjectId()) return renderNoProjectSelected(); return `
← 返回仪表盘

提出新目标

用自然语言描述你想让 graphlemon 完成的目标
`; } function mountGoalComposer() { if (!getCurrentProjectId()) return; const textArea = document.getElementById("goal_text"); const errorBox = document.getElementById("goal_error"); const successBox = document.getElementById("goal_success"); const form = document.getElementById("goal_form"); const confirmBanner = document.getElementById("goal_confirm_banner"); const confirmSummary = document.getElementById("goal_confirm_summary"); document.getElementById("btn_submit_goal").addEventListener("click", () => { errorBox.style.display = "none"; const goalText = textArea.value.trim(); if (!goalText) { errorBox.textContent = "目标描述不能为空"; errorBox.style.display = "block"; return; } confirmSummary.textContent = "即将创建任务:" + goalText; form.style.display = "none"; confirmBanner.style.display = "block"; }); document.getElementById("btn_edit_goal").addEventListener("click", () => { confirmBanner.style.display = "none"; form.style.display = "block"; }); document.getElementById("btn_confirm_goal").addEventListener("click", async () => { const resp = await fetch("/api/tasks", { method: "POST", body: JSON.stringify({ project_id: getCurrentProjectId(), goal_text: textArea.value.trim(), task_type: "goal", created_via: "human_console", }), }); const body = await resp.json(); if (resp.status !== 200) { confirmBanner.style.display = "none"; form.style.display = "block"; errorBox.textContent = body.error + (body.detail ? ":" + body.detail : ""); errorBox.style.display = "block"; return; } confirmBanner.style.display = "none"; // v_task_detail doesn't exist yet (U-025) -- show the created task_id // here instead of redirecting to a route that isn't built. successBox.innerHTML = "已创建任务 " + body.task_id + "(" + body.status + ")。" + '返回仪表盘'; successBox.style.display = "block"; }); } // ---- v_bug_feedback (02-V §2.4): report a bug ---- function renderBugFeedback() { if (!getCurrentProjectId()) return renderNoProjectSelected(); return `
← 返回仪表盘

反馈 Bug

人工反馈,与 Validator 自动生成的 bug_record 复用同一张表(source=human)
任务列表选择器随 U-025 补上,目前先直接填 task_id
`; } function mountBugFeedback() { if (!getCurrentProjectId()) return; const descriptionArea = document.getElementById("bug_description"); const reproStepsArea = document.getElementById("bug_repro_steps"); const relatedTaskIdInput = document.getElementById("bug_related_task_id"); const errorBox = document.getElementById("bug_error"); const successBox = document.getElementById("bug_success"); const form = document.getElementById("bug_form"); document.getElementById("btn_submit_bug").addEventListener("click", async () => { errorBox.style.display = "none"; const description = descriptionArea.value.trim(); if (!description) { errorBox.textContent = "问题描述不能为空"; errorBox.style.display = "block"; return; } const resp = await fetch("/api/bug-reports", { method: "POST", body: JSON.stringify({ project_id: getCurrentProjectId(), description: description, repro_steps: reproStepsArea.value.trim() || undefined, related_task_id: relatedTaskIdInput.value.trim() || undefined, }), }); const body = await resp.json(); if (resp.status !== 200) { errorBox.textContent = body.error + (body.detail ? ":" + body.detail : ""); errorBox.style.display = "block"; return; } form.style.display = "none"; successBox.innerHTML = "已提交,Validator 将进行归因确认(bug " + body.bug_id + ")。" + '返回仪表盘'; successBox.style.display = "block"; }); }