Files

1 line
73 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "adminzachary/autotasker", "files": {"static/app.js": {"4763e96612906546e4fecdc135a1ab315f488755": {"t": 1785746684.811531, "n": 39585, "h": "005bc306f539f7b4", "c": "const defaultAiSettings = {\n provider: \"openai\",\n model: \"\",\n base_url: \"https://api.openai.com/v1\",\n api_key: \"\",\n};\n\nconst stateHelpers = window.AutoTaskerStateHelpers || {};\nconst safeJsonParse =\n stateHelpers.safeJsonParse ||\n function safeJsonParseFallback(raw, fallback) {\n try {\n return JSON.parse(raw);\n } catch {\n return fallback;\n }\n };\nconst createVersionEntry =\n stateHelpers.createVersionEntry ||\n function createVersionEntryFallback(version, assistantMessage, draft) {\n return {\n version,\n assistant_message: assistantMessage,\n task_count: draft.length,\n updated_at: new Date().toISOString(),\n };\n };\nconst readDraftWorkspace =\n stateHelpers.readDraftWorkspace ||\n function readDraftWorkspaceFallback(storage) {\n return {\n draft: safeJsonParse(storage.getItem(\"autotasker-draft\") || \"[]\", []),\n draftGoalText: storage.getItem(\"autotasker-draft-goal\") || \"\",\n draftFeedback: storage.getItem(\"autotasker-draft-feedback\") || \"\",\n draftConversation: safeJsonParse(storage.getItem(\"autotasker-draft-conversation\") || \"[]\", []),\n draftVersion: Number(storage.getItem(\"autotasker-draft-version\") || \"0\"),\n draftVersionHistory: safeJsonParse(storage.getItem(\"autotasker-draft-versions\") || \"[]\", []),\n };\n };\nconst persistDraftWorkspaceState =\n stateHelpers.persistDraftWorkspaceState ||\n function persistDraftWorkspaceStateFallback(storage, workspace) {\n storage.setItem(\"autotasker-draft\", JSON.stringify(workspace.draft || []));\n storage.setItem(\"autotasker-draft-goal\", workspace.draftGoalText || \"\");\n storage.setItem(\"autotasker-draft-feedback\", workspace.draftFeedback || \"\");\n storage.setItem(\"autotasker-draft-conversation\", JSON.stringify(workspace.draftConversation || []));\n storage.setItem(\"autotasker-draft-version\", String(workspace.draftVersion || 0));\n storage.setItem(\"autotasker-draft-versions\", JSON.stringify(workspace.draftVersionHistory || []));\n };\nconst clearDraftWorkspaceState =\n stateHelpers.clearDraftWorkspaceState ||\n function clearDraftWorkspaceStateFallback(storage) {\n storage.removeItem(\"autotasker-draft\");\n storage.removeItem(\"autotasker-draft-goal\");\n storage.removeItem(\"autotasker-draft-feedback\");\n storage.removeItem(\"autotasker-draft-conversation\");\n storage.removeItem(\"autotasker-draft-version\");\n storage.removeItem(\"autotasker-draft-versions\");\n };\nconst buildFallbackUserText =\n stateHelpers.buildFallbackUserText ||\n function buildFallbackUserTextFallback(actions, labels) {\n return (actions || []).map((item) => `[快捷操作] ${labels[item] || item}`).join(\",\");\n };\nconst applyDiscussionResult =\n stateHelpers.applyDiscussionResult ||\n function applyDiscussionResultFallback(workspace, result, options) {\n const normalizedMessage = String(options?.message || \"\").trim();\n const fallbackUserText = buildFallbackUserText(options?.actions || [], options?.quickActionLabels || {});\n const nextConversation = Array.isArray(workspace?.draftConversation) ? [...workspace.draftConversation] : [];\n\n if (normalizedMessage || fallbackUserText) {\n nextConversation.push({\n role: \"user\",\n content: normalizedMessage || fallbackUserText,\n });\n }\n\n nextConversation.push({\n role: \"assistant\",\n content: result.assistant_message,\n });\n\n const nextDraft = Array.isArray(result.updated_plan) ? [...result.updated_plan] : [];\n const nextVersion = Number(result.version || Math.max(Number(workspace?.draftVersion || 0) + 1, 1));\n const nextHistory = Array.isArray(workspace?.draftVersionHistory) ? [...workspace.draftVersionHistory] : [];\n nextHistory.push(createVersionEntry(nextVersion, result.assistant_message, nextDraft));\n\n return {\n draft: nextDraft,\n draftFeedback: result.assistant_message,\n draftConversation: nextConversation,\n draftVersion: nextVersion,\n draftVersionHistory: nextHistory,\n };\n };\n\nconst quickActionLabels = {\n split_tasks: \"拆小任务\",\n compress_schedule: \"压缩周期\",\n delay_one_day: \"推迟一天\",\n avoid_quiet_hours: \"避开安静时段\",\n raise_priority: \"提高优先级\",\n};\n\nconst initialDraftWorkspace = readDraftWorkspace(window.localStorage);\n\nconst state = {\n token: localStorage.getItem(\"autotasker-token\") || \"\",\n user: null,\n tasks: [],\n stats: null,\n userProfile: null,\n draft: initialDraftWorkspace.draft,\n draftGoalText: initialDraftWorkspace.draftGoalText,\n draftFeedback: initialDraftWorkspace.draftFeedback,\n draftConversation: initialDraftWorkspace.draftConversation,\n draftVersion: initialDraftWorkspace.draftVersion,\n draftVersionHistory: initialDraftWorkspace.draftVersionHistory,\n recentLogs: [],\n timer: safeJsonParse(localStorage.getItem(\"autotasker-timer\") || \"null\", null),\n aiSettings: { ...defaultAiSettings, ...safeJsonParse(localStorage.getItem(\"autotasker-ai-settings\") || \"{}\", {}) },\n providerPresets: {\n openai: \"https://api.openai.com/v1\",\n azure_openai: \"\",\n deepseek: \"https://api.deepseek.com/v1\",\n qwen: \"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\",\n glm: \"https://open.bigmodel.cn/api/paas/v4\",\n gemini: \"https://generativelanguage.googleapis.com/v1beta\",\n },\n reviewOverride: localStorage.getItem(\"autotasker-review-override\") || \"\",\n};\n\nconst els = {\n appShell: document.querySelector(\".app-shell, .app-layout\"),\n loginStatus: document.getElementById(\"login-status-chip\"),\n navItems: Array.from(document.querySelectorAll(\".nav-item\")),\n views: Array.from(document.querySelectorAll(\".view\")),\n navFocus: document.getElementById(\"nav-focus\"),\n authPanel: document.getElementById(\"auth-panel\"),\n authForm: document.getElementById(\"auth-form\"),\n authMessage: document.getElementById(\"auth-message\"),\n username: document.getElementById(\"username\"),\n password: document.getElementById(\"password\"),\n profileCard: document.getElementById(\"profile-card\"),\n profileName: document.getElementById(\"profile-name\"),\n profileHint: document.getElementById(\"profile-hint\"),\n preferencesForm: document.getElementById(\"preferences-form\"),\n focusMinutes: document.getElementById(\"focus-minutes\"),\n breakMinutes: document.getElementById(\"break-minutes\"),\n quietHours: document.getElementById(\"quiet-hours\"),\n aiForm: document.getElementById(\"ai-form\"),\n aiProvider: document.getElementById(\"ai-provider\"),\n aiModel: document.getElementById(\"ai-model\"),\n aiBaseUrl: document.getElementById(\"ai-base-url\"),\n aiApiKey: document.getElementById(\"ai-api-key\"),\n goalForm: document.getElementById(\"goal-form\"),\n goalText: document.getElementById(\"goal-text\"),\n goalFeedback: document.getElementById(\"goal-feedback\"),\n discussionVersion: document.getElementById(\"discussion-version\"),\n discussionList: document.getElementById(\"discussion-list\"),\n discussionForm: document.getElementById(\"discussion-form\"),\n discussionInput: document.getElementById(\"discussion-input\"),\n draftList: document.getElementById(\"draft-list\"),\n versionHistory: document.getElementById(\"version-history\"),\n confirmDraft: document.getElementById(\"confirm-draft\"),\n clearDraft: document.getElementById(\"clear-draft\"),\n todoColumn: document.getElementById(\"todo-column\"),\n progressColumn: document.getElementById(\"progress-column\"),\n doneColumn: document.getElementById(\"done-column\"),\n todoCount: document.getElementById(\"todo-count\"),\n progressCount: document.getElementById(\"progress-count\"),\n doneCount: document.getElementById(\"done-count\"),\n pomodoroForm: document.getElementById(\"pomodoro-form\"),\n pomodoroTask: document.getElementById(\"pomodoro-task\"),\n pomodoroMinutes: document.getElementById(\"pomodoro-minutes\"),\n pomodoroPause: document.getElementById(\"pomodoro-pause\"),\n pomodoroDone: document.getElementById(\"pomodoro-done\"),\n pomodoroStop: document.getElementById(\"pomodoro-stop\"),\n timerCardEl: document.getElementById(\"timer-card-el\"),\n timerTaskName: document.getElementById(\"timer-task-name\"),\n timerValue: document.getElementById(\"timer-value\"),\n timerStatus: document.getElementById(\"timer-status\"),\n statsCards: document.getElementById(\"stats-cards\"),\n trendBars: document.getElementById(\"trend-bars\"),\n trendCaption: document.getElementById(\"trend-caption\"),\n reviewText: document.getElementById(\"review-text\"),\n logList: document.getElementById(\"log-list\"),\n generateReview: document.getElementById(\"generate-review\"),\n traitSummary: document.getElementById(\"trait-summary\"),\n traitPeakPeriod: document.getElementById(\"trait-peak-period\"),\n traitFocusWindow: document.getElementById(\"trait-focus-window\"),\n traitTaskStyle: document.getElementById(\"trait-task-style\"),\n traitDiscipline: document.getElementById(\"trait-discipline\"),\n periodBreakdown: document.getElementById(\"period-breakdown\"),\n quickActionButtons: Array.from(document.querySelectorAll(\"[data-quick-action]\")),\n};\n\nlet countdownTicker = null;\nconst visualState = {\n revealObserver: null,\n reduceMotion: window.matchMedia(\"(prefers-reduced-motion: reduce)\"),\n finePointer: window.matchMedia(\"(hover: hover) and (pointer: fine)\"),\n scrollTicking: false,\n};\n\nconst MOTION_CARD_SELECTOR = [\".glass\", \".task-card\", \".draft-card\", \".metric\", \".log-item\", \".trait-card\", \".period-card\"].join(\n \", \"\n);\n\nasync function api(path, options = {}) {\n const headers = { \"Content-Type\": \"application/json\", ...(options.headers || {}) };\n if (state.token) headers.Authorization = `Bearer ${state.token}`;\n const response = await fetch(path, {\n ...options,\n headers,\n body: options.body ? JSON.stringify(options.body) : undefined,\n });\n const data = await response.json().catch(() => ({}));\n if (!response.ok) throw new Error(data.detail || data.error || \"请求失败\");\n return data;\n}\n\nfunction flash(message, isError = false) {\n if (!els.authMessage) return;\n els.authMessage.textContent = message;\n els.authMessage.style.color = isError ? \"#b5542c\" : \"#2c7a52\";\n}\n\nfunction setGoalFeedback(text, isMuted = false) {\n if (!els.goalFeedback) return;\n els.goalFeedback.textContent = text;\n els.goalFeedback.classList.toggle(\"empty\", isMuted || !text);\n}\n\nfunction persistDraftWorkspace() {\n persistDraftWorkspaceState(localStorage, state);\n}\n\nfunction clearDraftState() {\n state.draft = [];\n state.draftGoalText = \"\";\n state.draftFeedback = \"\";\n state.draftConversation = [];\n state.draftVersion = 0;\n state.draftVersionHistory = [];\n clearDraftWorkspaceState(localStorage);\n}\n\nfunction persistTimer() {\n if (state.timer) {\n localStorage.setItem(\"autotasker-timer\", JSON.stringify(state.timer));\n } else {\n localStorage.removeItem(\"autotasker-timer\");\n }\n}\n\nfunction persistAiSettings() {\n localStorage.setItem(\"autotasker-ai-settings\", JSON.stringify(state.aiSettings));\n}\n\nfunction persistReviewOverride() {\n if (state.reviewOverride) {\n localStorage.setItem(\"autotasker-review-override\", state.reviewOverride);\n } else {\n localStorage.removeItem(\"autotasker-review-override\");\n }\n}\n\nfunction formatDateTime(value) {\n if (!value) return \"未安排时间\";\n const date = new Date(value);\n if (Number.isNaN(date.getTime())) return value;\n return `${date.toLocaleDateString(\"zh-CN\", { month: \"2-digit\", day: \"2-digit\" })} ${date.toLocaleTimeString(\"zh-CN\", {\n hour: \"2-digit\",\n minute: \"2-digit\",\n })}`;\n}\n\nfunction formatShortDate(value) {\n if (!value) return \"\";\n const date = new Date(value);\n if (Number.isNaN(date.getTime())) return \"\";\n return `${date.toLocaleDateString(\"zh-CN\", { month: \"2-digit\", day: \"2-digit\" })} ${date.toLocaleTimeString(\"zh-CN\", {\n hour: \"2-digit\",\n minute: \"2-digit\",\n })}`;\n}\n\nfunction escapeHtml(value) {\n return String(value)\n .replaceAll(\"&\", \"&amp;\")\n .replaceAll(\"<\", \"&lt;\")\n .replaceAll(\">\", \"&gt;\")\n .replaceAll('\"', \"&quot;\");\n}\n\nfunction toInputDate(value) {\n if (!value) return \"\";\n const date = new Date(value);\n if (Number.isNaN(date.getTime())) return \"\";\n const offset = date.getTimezoneOffset() * 60_000;\n return new Date(date.getTime() - offset).toISOString().slice(0, 16);\n}\n\nfunction getAiConfig() {\n return {\n provider: els.aiProvider.value,\n model: els.aiModel.value.trim(),\n base_url: els.aiBaseUrl.value.trim(),\n api_key: els.aiApiKey.value.trim(),\n };\n}\n\nfunction hydrateAiForm() {\n els.aiProvider.value = state.aiSettings.provider || \"openai\";\n els.aiModel.value = state.aiSettings.model || \"\";\n els.aiBaseUrl.value = state.aiSettings.base_url || state.providerPresets[state.aiSettings.provider] || \"\";\n els.aiApiKey.value = state.aiSettings.api_key || \"\";\n}\n\nfunction syncBaseUrlPreset(force = false) {\n const provider = els.aiProvider.value;\n if (provider === \"custom_compatible\") return;\n const preset = state.providerPresets[provider] || \"\";\n if (force || !els.aiBaseUrl.value.trim()) {\n els.aiBaseUrl.value = preset;\n }\n}\n\nfunction switchView(viewName) {\n if (!els.navItems.length || !els.views.length) return;\n els.navItems.forEach((button) => button.classList.toggle(\"active\", button.dataset.view === viewName));\n els.views.forEach((view) => view.classList.toggle(\"active\", view.id === `view-${viewName}`));\n}\n\nfunction renderAuth() {\n const loggedIn = Boolean(state.user);\n els.authPanel.classList.toggle(\"hidden\", loggedIn);\n els.profileCard.classList.toggle(\"hidden\", !loggedIn);\n if (els.loginStatus) {\n els.loginStatus.textContent = loggedIn ? state.user.username : \"Offline\";\n els.loginStatus.style.background = loggedIn ? \"#e6ffe6\" : \"#ffeeee\";\n els.loginStatus.style.color = loggedIn ? \"#006600\" : \"var(--accent-red)\";\n }\n if (!loggedIn) {\n switchView(\"settings\");\n return;\n }\n els.profileName.textContent = state.user.username;\n els.profileHint.textContent = `当前默认番茄 ${state.user.focus_minutes} 分钟,休息 ${state.user.break_minutes} 分钟`;\n els.focusMinutes.value = state.user.focus_minutes;\n els.breakMinutes.value = state.user.break_minutes;\n els.quietHours.value = state.user.quiet_hours;\n els.pomodoroMinutes.value = state.user.focus_minutes;\n hydrateAiForm();\n}\n\nfunction renderDiscussion() {\n const hasDraft = state.draft.length > 0;\n els.discussionVersion.textContent = `v${state.draftVersion || 0}`;\n els.discussionInput.disabled = !hasDraft;\n els.discussionForm.querySelector(\"button\").disabled = !hasDraft;\n els.quickActionButtons.forEach((button) => {\n button.disabled = !hasDraft;\n });\n\n if (!state.draftConversation.length) {\n els.discussionList.innerHTML =\n '<p class=\"empty\" style=\"display:block;font-size: 14px;\">生成首版草案后,你可以继续像聊天一样让 AI 重排、拆分、压缩或解释安排。</p>';\n return;\n }\n\n els.discussionList.innerHTML = state.draftConversation\n .map(\n (item) => `\n <article class=\"discussion-bubble ${item.role}\">\n <small>${item.role === \"user\" ? \"User\" : \"Assistant\"}</small>\n <div>${escapeHtml(item.content).replaceAll(\"\\n\", \"<br />\")}</div>\n </article>\n `\n )\n .join(\"\");\n els.discussionList.scrollTop = els.discussionList.scrollHeight;\n}\n\nfunction renderVersionHistory() {\n if (!state.draftVersionHistory.length) {\n els.versionHistory.innerHTML = '<p class=\"empty\" style=\"display:block;font-size:14px;\">讨论版本历史会显示在这里。</p>';\n return;\n }\n\n els.versionHistory.innerHTML = [...state.draftVersionHistory]\n .reverse()\n .map(\n (item) => `\n <article class=\"version-item\">\n <small>${formatShortDate(item.updated_at) || \"刚刚更新\"}</small>\n <strong>v${item.version} · ${item.task_count} 个任务</strong>\n <div>${escapeHtml(item.assistant_message)}</div>\n </article>\n `\n )\n .join(\"\");\n}\n\nfunction renderDraft() {\n const hasDraft = state.draft.length > 0;\n if (!hasDraft) {\n els.draftList.innerHTML = '<p class=\"empty\" style=\"display:block;font-size: 14px;\">生成草案后,可在此微调任务与时间安排。</p>';\n els.confirmDraft.disabled = true;\n els.clearDraft.disabled = true;\n renderVersionHistory();\n return;\n }\n\n els.confirmDraft.disabled = false;\n els.clearDraft.disabled = false;\n els.draftList.innerHTML = state.draft\n .map(\n (task, index) => `\n <article class=\"draft-card\">\n <div class=\"draft-grid\">\n <label>\n <span>任务标题</span>\n <input class=\"task-input\" data-field=\"title\" data-index=\"${index}\" value=\"${escapeHtml(task.title)}\" />\n </label>\n <label>\n <span>开始时间</span>\n <input class=\"task-input\" data-field=\"scheduled_for\" data-index=\"${index}\" type=\"datetime-local\" value=\"${toInputDate(task.scheduled_for)}\" />\n </label>\n <label>\n <span>截止时间</span>\n <input class=\"task-input\" data-field=\"due_at\" data-index=\"${index}\" type=\"datetime-local\" value=\"${toInputDate(task.due_at)}\" />\n </label>\n <label>\n <span>预计分钟</span>\n <input class=\"task-input\" data-field=\"estimated_minutes\" data-index=\"${index}\" type=\"number\" min=\"15\" max=\"180\" value=\"${task.estimated_minutes}\" />\n </label>\n </div>\n <label>\n <span>任务说明</span>\n <textarea class=\"task-input\" data-field=\"description\" data-index=\"${index}\" rows=\"2\">${escapeHtml(task.description || \"\")}</textarea>\n </label>\n </article>\n `\n )\n .join(\"\");\n renderVersionHistory();\n}\n\nfunction renderTaskCards(tasks) {\n if (!tasks.length) return '<p class=\"empty\">这里暂时没有任务。</p>';\n return tasks\n .map(\n (task) => `\n <article class=\"task-card\" draggable=\"true\" data-task-id=\"${task.id}\">\n <div class=\"task-meta\">\n <span class=\"task-badge\">${task.status === \"todo\" ? \"待办\" : task.status === \"in_progress\" ? \"进行中\" : \"已完成\"}</span>\n <span class=\"task-badge focus\">${task.estimated_minutes} 分钟</span>\n ${task.overdue ? '<span class=\"task-badge overdue\">已逾期</span>' : \"\"}\n ${task.status === \"done\" ? '<span class=\"task-badge done\">完成</span>' : \"\"}\n </div>\n <div>\n <h4>${escapeHtml(task.title)}</h4>\n <p class=\"empty\">${escapeHtml(task.description || \"暂无说明\")}</p>\n </div>\n <div class=\"task-meta\">\n <span>${formatDateTime(task.scheduled_for)}</span>\n <span>${task.delay_count} 次延期</span>\n </div>\n <div class=\"task-actions\">\n ${task.status !== \"in_progress\" ? `<button class=\"btn btn-secondary\" data-action=\"status\" data-id=\"${task.id}\" data-status=\"in_progress\">开始</button>` : \"\"}\n ${task.status !== \"done\" ? `<button class=\"btn btn-primary\" data-action=\"status\" data-id=\"${task.id}\" data-status=\"done\">完成</button>` : \"\"}\n ${task.status !== \"todo\" ? `<button class=\"btn btn-ghost\" data-action=\"status\" data-id=\"${task.id}\" data-status=\"todo\">回待办</button>` : \"\"}\n ${task.status !== \"done\" ? `<button class=\"btn btn-ghost\" data-action=\"postpone\" data-id=\"${task.id}\">延期 30 分钟</button>` : \"\"}\n </div>\n </article>\n `\n )\n .join(\"\");\n}\n\nfunction renderPomodoroTasks() {\n const available = state.tasks.filter((task) => task.status !== \"done\");\n els.pomodoroTask.innerHTML = available.length\n ? available.map((task) => `<option value=\"${task.id}\">${escapeHtml(task.title)}</option>`).join(\"\")\n : '<option value=\"\">暂无可执行任务</option>';\n}\n\nfunction renderKanban() {\n const groups = { todo: [], in_progress: [], done: [] };\n state.tasks.forEach((task) => groups[task.status].push(task));\n els.todoCount.textContent = groups.todo.length;\n els.progressCount.textContent = groups.in_progress.length;\n els.doneCount.textContent = groups.done.length;\n els.todoColumn.innerHTML = renderTaskCards(groups.todo);\n els.progressColumn.innerHTML = renderTaskCards(groups.in_progress);\n els.doneColumn.innerHTML = renderTaskCards(groups.done);\n renderPomodoroTasks();\n setupDragAndDrop();\n}\n\nfunction renderStats() {\n const stats = state.stats || {\n total_tasks: 0,\n completion_rate: 0,\n overdue_tasks: 0,\n focus_minutes_total: 0,\n trend: [],\n review: \"暂无数据\",\n };\n els.statsCards.innerHTML = `\n <article class=\"metric\"><span>总任务</span><strong>${stats.total_tasks}</strong></article>\n <article class=\"metric\"><span>完成率</span><strong>${Math.round(stats.completion_rate * 100)}%</strong></article>\n <article class=\"metric\"><span>已逾期</span><strong>${stats.overdue_tasks}</strong></article>\n <article class=\"metric\"><span>专注分钟</span><strong>${stats.focus_minutes_total}</strong></article>\n `;\n const trend = stats.trend || [];\n if (!trend.length) {\n els.trendCaption.textContent = \"暂无完成记录\";\n els.trendBars.innerHTML = '<p class=\"empty\">完成任务后,这里会出现趋势柱状图。</p>';\n } else {\n const max = Math.max(...trend.map((item) => item.count), 1);\n els.trendCaption.textContent = `最近 ${trend.length} 个日期有完成记录`;\n els.trendBars.innerHTML = trend\n .map(\n (item) => `\n <div class=\"trend-bar\">\n <div class=\"trend-bar-fill\" style=\"height:${Math.max(14, (item.count / max) * 92)}px\"></div>\n <strong>${item.count}</strong>\n <span>${item.day.slice(5)}</span>\n </div>\n `\n )\n .join(\"\");\n }\n els.reviewText.textContent = state.reviewOverride || stats.review;\n els.reviewText.classList.toggle(\"empty\", !els.reviewText.textContent);\n}\n\nfunction renderUserProfile() {\n const profile = state.userProfile || {\n summary: \"还没有足够的历史执行数据。\",\n peak_period_label: \"待观察\",\n suggested_focus_window: \"待观察\",\n preferred_task_style_label: \"待观察\",\n scheduling_discipline_label: \"待观察\",\n period_breakdown: [],\n };\n els.traitSummary.textContent = profile.summary;\n els.traitPeakPeriod.textContent = profile.peak_period_label;\n els.traitFocusWindow.textContent = profile.suggested_focus_window;\n els.traitTaskStyle.textContent = profile.preferred_task_style_label;\n els.traitDiscipline.textContent = profile.scheduling_discipline_label;\n\n if (!profile.period_breakdown?.length || profile.total_completed_sessions === 0) {\n els.periodBreakdown.innerHTML =\n '<p class=\"empty\" style=\"display:block;font-size:14px;\">开始记录番茄钟后,这里会识别你更擅长的时段。</p>';\n return;\n }\n\n els.periodBreakdown.innerHTML = profile.period_breakdown\n .map(\n (item) => `\n <article class=\"period-card\">\n <small>${escapeHtml(item.label)}</small>\n <strong>${item.focus_minutes} 分钟专注</strong>\n <div>${item.completed_sessions}/${item.total_sessions} 次完成,完成率 ${Math.round(item.completion_rate * 100)}%</div>\n </article>\n `\n )\n .join(\"\");\n}\n\nfunction renderLogs() {\n if (!state.recentLogs.length) {\n els.logList.innerHTML = '<p class=\"empty\">还没有番茄钟记录,开始第一轮后会展示在这里。</p>';\n return;\n }\n els.logList.innerHTML = state.recentLogs\n .map(\n (item) => `\n <article class=\"log-item\">\n <strong>${escapeHtml(item.task_title)}</strong>\n <div class=\"log-meta\">\n <span>${item.planned_minutes} 分钟</span>\n <span>${Math.round(item.actual_seconds / 60)} 分钟实际专注</span>\n <span>${item.status === \"done\" ? \"完成\" : item.status === \"running\" ? \"进行中\" : \"中断\"}</span>\n </div>\n </article>\n `\n )\n .join(\"\");\n}\n\nfunction renderTimer() {\n if (!state.timer) {\n els.timerCardEl?.classList.remove(\"is-running\");\n els.navFocus?.classList.remove(\"has-timer\");\n els.timerTaskName.textContent = \"未开始\";\n els.timerValue.textContent = `${String(Number(els.pomodoroMinutes.value || 25)).padStart(2, \"0\")}:00`;\n els.timerStatus.textContent = \"等待选择一个任务开始。\";\n els.pomodoroPause.disabled = true;\n els.pomodoroDone.disabled = true;\n els.pomodoroStop.disabled = true;\n els.pomodoroPause.textContent = \"Pause\";\n return;\n }\n const task = state.tasks.find((item) => item.id === state.timer.taskId);\n const remaining = Math.max(0, Math.floor((state.timer.endsAt - Date.now()) / 1000));\n els.navFocus?.classList.add(\"has-timer\");\n els.timerTaskName.textContent = task ? task.title : \"进行中的任务\";\n els.timerValue.textContent = `${String(Math.floor(remaining / 60)).padStart(2, \"0\")}:${String(remaining % 60).padStart(2, \"0\")}`;\n els.timerStatus.textContent = state.timer.paused\n ? `已暂停,已累计 ${Math.round(state.timer.elapsedSeconds / 60)} 分钟。`\n : remaining > 0\n ? \"专注进行中,结束后可直接记为完成。\"\n : \"时间到,可以完成本轮或继续追加。\";\n els.timerCardEl?.classList.toggle(\"is-running\", !state.timer.paused && remaining > 0);\n els.pomodoroPause.disabled = false;\n els.pomodoroDone.disabled = false;\n els.pomodoroStop.disabled = false;\n els.pomodoroPause.textContent = state.timer.paused ? \"Resume\" : \"Pause\";\n}\n\nfunction renderAll() {\n renderAuth();\n renderDiscussion();\n renderDraft();\n renderKanban();\n renderStats();\n renderUserProfile();\n renderLogs();\n renderTimer();\n if (state.draftGoalText) els.goalText.value = state.draftGoalText;\n if (state.draftFeedback) setGoalFeedback(state.draftFeedback);\n refreshImmersiveUi();\n}\n\nfunction createRevealObserver() {\n if (visualState.revealObserver) return visualState.revealObserver;\n visualState.revealObserver = new IntersectionObserver(\n (entries) => {\n entries.forEach((entry) => {\n if (entry.isIntersecting) {\n entry.target.classList.add(\"is-visible\");\n } else if (!visualState.reduceMotion.matches) {\n entry.target.classList.remove(\"is-visible\");\n }\n });\n },\n {\n rootMargin: \"0px 0px -12% 0px\",\n threshold: 0.12,\n }\n );\n return visualState.revealObserver;\n}\n\nfunction applyScrollScene() {\n const progress = Math.min(window.scrollY / Math.max(window.innerHeight, 1), 1);\n document.body.style.setProperty(\"--scroll-progress\", progress.toFixed(3));\n if (els.appShell) {\n els.appShell.style.setProperty(\"--shell-shift\", `${progress * -12}px`);\n }\n}\n\nfunction queueScrollScene() {\n if (visualState.scrollTicking) return;\n visualState.scrollTicking = true;\n window.requestAnimationFrame(() => {\n applyScrollScene();\n visualState.scrollTicking = false;\n });\n}\n\nfunction bindCardTilt(element, index) {\n if (element.dataset.motionBound === \"true\") return;\n element.dataset.motionBound = \"true\";\n element.style.setProperty(\"--reveal-delay\", `${Math.min(index * 35, 280)}ms`);\n\n element.addEventListener(\"pointermove\", (event) => {\n if (visualState.reduceMotion.matches || !visualState.finePointer.matches) return;\n const rect = element.getBoundingClientRect();\n const px = (event.clientX - rect.left) / rect.width;\n const py = (event.clientY - rect.top) / rect.height;\n const rotateY = (px - 0.5) * 12;\n const rotateX = (0.5 - py) * 10;\n const glowX = `${Math.round(px * 100)}%`;\n const glowY = `${Math.round(py * 100)}%`;\n\n element.style.setProperty(\"--tilt-x\", `${rotateX.toFixed(2)}deg`);\n element.style.setProperty(\"--tilt-y\", `${rotateY.toFixed(2)}deg`);\n element.style.setProperty(\"--glow-x\", glowX);\n element.style.setProperty(\"--glow-y\", glowY);\n element.style.setProperty(\"--pointer-alpha\", \"1\");\n });\n\n element.addEventListener(\"pointerleave\", () => {\n element.style.setProperty(\"--tilt-x\", \"0deg\");\n element.style.setProperty(\"--tilt-y\", \"0deg\");\n element.style.setProperty(\"--pointer-alpha\", \"0\");\n });\n}\n\nfunction refreshImmersiveUi() {\n const observer = createRevealObserver();\n document.querySelectorAll(MOTION_CARD_SELECTOR).forEach((element, index) => {\n element.classList.add(\"motion-reveal\");\n bindCardTilt(element, index);\n observer.observe(element);\n });\n}\n\nfunction setupImmersiveUi() {\n document.body.classList.add(\"immersive-ready\");\n applyScrollScene();\n refreshImmersiveUi();\n\n window.addEventListener(\"scroll\", queueScrollScene, { passive: true });\n window.addEventListener(\"resize\", queueScrollScene);\n\n document.addEventListener(\"pointermove\", (event) => {\n if (visualState.reduceMotion.matches || !visualState.finePointer.matches) return;\n const x = (event.clientX / window.innerWidth) * 100;\n const y = (event.clientY / window.innerHeight) * 100;\n document.body.style.setProperty(\"--cursor-x\", `${x.toFixed(2)}%`);\n document.body.style.setProperty(\"--cursor-y\", `${y.toFixed(2)}%`);\n });\n\n visualState.reduceMotion.addEventListener(\"change\", () => {\n document.body.classList.toggle(\"reduced-motion\", visualState.reduceMotion.matches);\n refreshImmersiveUi();\n });\n\n visualState.finePointer.addEventListener(\"change\", () => {\n document.body.classList.toggle(\"fine-pointer\", visualState.finePointer.matches);\n });\n\n document.body.classList.toggle(\"reduced-motion\", visualState.reduceMotion.matches);\n document.body.classList.toggle(\"fine-pointer\", visualState.finePointer.matches);\n}\n\nasync function bootstrap() {\n if (!state.token) {\n renderAll();\n return;\n }\n try {\n const data = await api(\"/api/bootstrap\");\n state.user = data.user;\n state.tasks = data.tasks;\n state.stats = data.stats;\n state.userProfile = data.user_profile;\n state.recentLogs = data.recent_logs;\n state.providerPresets = { ...state.providerPresets, ...(data.provider_presets || {}) };\n renderAll();\n } catch (error) {\n state.token = \"\";\n state.user = null;\n localStorage.removeItem(\"autotasker-token\");\n flash(error.message, true);\n renderAll();\n }\n}\n\nasync function submitAuth(mode) {\n const username = els.username.value.trim();\n const password = els.password.value.trim();\n const path = mode === \"register\" ? \"/api/auth/register\" : \"/api/auth/login\";\n const data = await api(path, { method: \"POST\", body: { username, password } });\n state.token = data.token;\n state.user = data.user;\n localStorage.setItem(\"autotasker-token\", state.token);\n flash(mode === \"register\" ? \"注册成功,已自动登录。\" : \"登录成功。\");\n await bootstrap();\n switchView(\"dashboard\");\n}\n\nasync function generateDraft() {\n if (!state.user) {\n setGoalFeedback(\"请先登录后再生成任务草案。\");\n return;\n }\n const goalText = els.goalText.value.trim();\n if (!goalText) {\n setGoalFeedback(\"请先输入一个明确目标。\");\n return;\n }\n state.aiSettings = getAiConfig();\n persistAiSettings();\n setGoalFeedback(\"AI 正在生成首版草案...\");\n const data = await api(\"/api/goals/analyze\", {\n method: \"POST\",\n body: { goal_text: goalText, ai_config: state.aiSettings },\n });\n\n state.draftGoalText = goalText;\n state.draftFeedback = data.agent_feedback;\n state.draft = data.staging_tasks || [];\n state.draftConversation = data.agent_feedback ? [{ role: \"assistant\", content: data.agent_feedback }] : [];\n state.draftVersion = state.draft.length ? 1 : 0;\n state.draftVersionHistory = state.draft.length ? [createVersionEntry(1, data.agent_feedback, state.draft)] : [];\n persistDraftWorkspace();\n\n const meta = data.provider && data.model ? `当前模型:${data.provider} / ${data.model}` : \"\";\n setGoalFeedback([data.agent_feedback, meta].filter(Boolean).join(\"\\n\"));\n renderDiscussion();\n renderDraft();\n}\n\nasync function discussDraft(message = \"\", actions = []) {\n if (!state.user || !state.draft.length) return;\n const normalizedMessage = message.trim();\n state.aiSettings = getAiConfig();\n persistAiSettings();\n\n const data = await api(\"/api/goals/discuss\", {\n method: \"POST\",\n body: {\n goal_text: state.draftGoalText,\n current_plan: state.draft,\n conversation: state.draftConversation,\n user_message: normalizedMessage,\n actions,\n version: Math.max(state.draftVersion, 1),\n ai_config: state.aiSettings,\n },\n });\n\n const nextWorkspace = applyDiscussionResult(\n {\n draft: state.draft,\n draftFeedback: state.draftFeedback,\n draftConversation: state.draftConversation,\n draftVersion: state.draftVersion,\n draftVersionHistory: state.draftVersionHistory,\n },\n data,\n {\n message: normalizedMessage,\n actions,\n quickActionLabels,\n }\n );\n state.draftConversation = nextWorkspace.draftConversation;\n state.draftFeedback = nextWorkspace.draftFeedback;\n state.draft = nextWorkspace.draft;\n state.draftVersion = nextWorkspace.draftVersion;\n state.draftVersionHistory = nextWorkspace.draftVersionHistory;\n persistDraftWorkspace();\n setGoalFeedback(`当前版本 v${data.version}\\n${data.assistant_message}`);\n els.discussionInput.value = \"\";\n renderDiscussion();\n renderDraft();\n}\n\nasync function confirmDraft() {\n if (!state.draft.length) return;\n await api(\"/api/goals/confirm\", {\n method: \"POST\",\n body: {\n goal_text: state.draftGoalText,\n agent_feedback: state.draftFeedback || \"用户已确认多轮讨论后的最终草案\",\n tasks: state.draft,\n },\n });\n clearDraftState();\n setGoalFeedback(\"草案已同步到正式看板。\");\n await bootstrap();\n switchView(\"kanban\");\n}\n\nasync function updatePreferences() {\n const data = await api(\"/api/preferences\", {\n method: \"PUT\",\n body: {\n focus_minutes: Number(els.focusMinutes.value),\n break_minutes: Number(els.breakMinutes.value),\n quiet_hours: els.quietHours.value.trim(),\n },\n });\n state.user = data.user;\n renderAuth();\n}\n\nfunction saveAiSettings() {\n state.aiSettings = getAiConfig();\n persistAiSettings();\n flash(\"AI 配置已保存在当前浏览器。\");\n}\n\nasync function generateAiReview() {\n state.aiSettings = getAiConfig();\n persistAiSettings();\n const data = await api(\"/api/review/generate\", {\n method: \"POST\",\n body: { ai_config: state.aiSettings },\n });\n state.reviewOverride = `${data.review}\\n\\n来自 ${data.provider} / ${data.model}`;\n persistReviewOverride();\n renderStats();\n}\n\nasync function updateTaskStatus(taskId, status) {\n await api(`/api/tasks/${taskId}/status`, { method: \"PATCH\", body: { status } });\n await bootstrap();\n}\n\nasync function postponeTask(taskId) {\n await api(\"/api/tasks/postpone\", { method: \"POST\", body: { task_id: Number(taskId), minutes: 30 } });\n await bootstrap();\n}\n\nasync function startPomodoro() {\n const taskId = Number(els.pomodoroTask.value);\n if (!taskId) return;\n const minutes = Number(els.pomodoroMinutes.value) || (state.user?.focus_minutes ?? 25);\n const data = await api(\"/api/pomodoro/start\", {\n method: \"POST\",\n body: { task_id: taskId, planned_minutes: minutes },\n });\n state.timer = {\n logId: data.log_id,\n taskId,\n plannedMinutes: minutes,\n startedAt: Date.now(),\n endsAt: Date.now() + minutes * 60 * 1000,\n paused: false,\n pausedAt: null,\n elapsedSeconds: 0,\n };\n persistTimer();\n startTicker();\n await bootstrap();\n switchView(\"focus\");\n}\n\nasync function finishPomodoro(result) {\n if (!state.timer) return;\n const elapsedSeconds = state.timer.paused\n ? state.timer.elapsedSeconds\n : state.timer.elapsedSeconds + Math.max(0, Math.floor((Date.now() - state.timer.startedAt) / 1000));\n await api(\"/api/pomodoro/finish\", {\n method: \"POST\",\n body: {\n log_id: state.timer.logId,\n task_id: state.timer.taskId,\n actual_seconds: elapsedSeconds,\n result,\n },\n });\n state.timer = null;\n persistTimer();\n stopTicker();\n await bootstrap();\n}\n\nfunction togglePause() {\n if (!state.timer) return;\n if (state.timer.paused) {\n const pauseDuration = Date.now() - state.timer.pausedAt;\n state.timer.startedAt += pauseDuration;\n state.timer.endsAt += pauseDuration;\n state.timer.paused = false;\n state.timer.pausedAt = null;\n } else {\n state.timer.elapsedSeconds += Math.max(0, Math.floor((Date.now() - state.timer.startedAt) / 1000));\n state.timer.paused = true;\n state.timer.pausedAt = Date.now();\n }\n persistTimer();\n renderTimer();\n}\n\nfunction startTicker() {\n stopTicker();\n countdownTicker = window.setInterval(() => {\n if (state.timer && !state.timer.paused && state.timer.endsAt <= Date.now()) {\n state.timer.endsAt = Date.now();\n }\n renderTimer();\n persistTimer();\n }, 1000);\n}\n\nfunction stopTicker() {\n if (countdownTicker) {\n clearInterval(countdownTicker);\n countdownTicker = null;\n }\n}\n\nfunction setupDragAndDrop() {\n document.querySelectorAll(\".task-card\").forEach((card) => {\n card.addEventListener(\"dragstart\", (event) => {\n card.classList.add(\"dragging\");\n event.dataTransfer?.setData(\"text/plain\", card.dataset.taskId);\n });\n card.addEventListener(\"dragend\", () => card.classList.remove(\"dragging\"));\n });\n document.querySelectorAll(\".column-body\").forEach((column) => {\n column.addEventListener(\"dragover\", (event) => {\n event.preventDefault();\n column.classList.add(\"drag-over\");\n });\n column.addEventListener(\"dragleave\", () => column.classList.remove(\"drag-over\"));\n column.addEventListener(\"drop\", async (event) => {\n event.preventDefault();\n column.classList.remove(\"drag-over\");\n const taskId = Number(event.dataTransfer.getData(\"text/plain\"));\n const status = column.parentElement.dataset.status;\n if (taskId && status) await updateTaskStatus(taskId, status);\n });\n });\n}\n\ndocument.addEventListener(\"input\", (event) => {\n const target = event.target;\n if (target.classList.contains(\"task-input\")) {\n const index = Number(target.dataset.index);\n const field = target.dataset.field;\n state.draft[index][field] = field === \"estimated_minutes\" ? Number(target.value) : target.value;\n persistDraftWorkspace();\n }\n});\n\ndocument.addEventListener(\"click\", async (event) => {\n const target = event.target;\n if (target.dataset.action === \"status\") {\n await updateTaskStatus(target.dataset.id, target.dataset.status);\n }\n if (target.dataset.action === \"postpone\") {\n await postponeTask(target.dataset.id);\n }\n if (target.dataset.quickAction) {\n try {\n await discussDraft(\"\", [target.dataset.quickAction]);\n } catch (error) {\n setGoalFeedback(`${error.message}\\n当前草案已保留。`);\n }\n }\n});\n\nels.navItems.forEach((button) => {\n button.addEventListener(\"click\", () => switchView(button.dataset.view));\n});\n\nels.aiProvider.addEventListener(\"change\", () => syncBaseUrlPreset(true));\n\nels.authForm.addEventListener(\"submit\", async (event) => {\n event.preventDefault();\n const mode = event.submitter?.dataset.mode || \"login\";\n try {\n await submitAuth(mode);\n } catch (error) {\n flash(error.message, true);\n }\n});\n\nels.preferencesForm.addEventListener(\"submit\", async (event) => {\n event.preventDefault();\n try {\n await updatePreferences();\n flash(\"偏好已更新。\");\n } catch (error) {\n flash(error.message, true);\n }\n});\n\nels.aiForm.addEventListener(\"submit\", (event) => {\n event.preventDefault();\n saveAiSettings();\n});\n\nels.goalForm.addEventListener(\"submit\", async (event) => {\n event.preventDefault();\n try {\n await generateDraft();\n } catch (error) {\n setGoalFeedback(error.message);\n }\n});\n\nels.discussionForm.addEventListener(\"submit\", async (event) => {\n event.preventDefault();\n try {\n await discussDraft(els.discussionInput.value, []);\n } catch (error) {\n setGoalFeedback(`${error.message}\\n当前草案已保留。`);\n }\n});\n\nels.confirmDraft.addEventListener(\"click\", async () => {\n try {\n await confirmDraft();\n } catch (error) {\n setGoalFeedback(error.message);\n }\n});\n\nels.clearDraft.addEventListener(\"click\", () => {\n clearDraftState();\n setGoalFeedback(\"草案与讨论历史已清空。\");\n renderDiscussion();\n renderDraft();\n});\n\nels.generateReview.addEventListener(\"click\", async () => {\n try {\n await generateAiReview();\n } catch (error) {\n state.reviewOverride = \"\";\n persistReviewOverride();\n els.reviewText.textContent = error.message;\n }\n});\n\nels.pomodoroForm.addEventListener(\"submit\", async (event) => {\n event.preventDefault();\n try {\n await startPomodoro();\n } catch (error) {\n els.timerStatus.textContent = error.message;\n }\n});\n\nels.pomodoroPause.addEventListener(\"click\", togglePause);\nels.pomodoroDone.addEventListener(\"click\", async () => finishPomodoro(\"done\"));\nels.pomodoroStop.addEventListener(\"click\", async () => finishPomodoro(\"interrupted\"));\n\nif (state.timer) startTicker();\nhydrateAiForm();\nrenderAll();\nsetupImmersiveUi();\nbootstrap();\n"}, "1ce871aa2ad9c3dadd18d495c0060b04568bf0d7": {"t": 1785766821.9059925, "n": 29149, "h": "8d99f363b519271f", "c": "const defaultAiSettings = {\n provider: \"openai\",\n model: \"\",\n base_url: \"https://api.openai.com/v1\",\n api_key: \"\",\n};\n\nconst state = {\n token: localStorage.getItem(\"autotasker-token\") || \"\",\n user: null,\n tasks: [],\n stats: null,\n draft: JSON.parse(localStorage.getItem(\"autotasker-draft\") || \"[]\"),\n draftGoalText: localStorage.getItem(\"autotasker-draft-goal\") || \"\",\n draftFeedback: localStorage.getItem(\"autotasker-draft-feedback\") || \"\",\n recentLogs: [],\n timer: JSON.parse(localStorage.getItem(\"autotasker-timer\") || \"null\"),\n aiSettings: { ...defaultAiSettings, ...JSON.parse(localStorage.getItem(\"autotasker-ai-settings\") || \"{}\") },\n providerPresets: {\n openai: \"https://api.openai.com/v1\",\n azure_openai: \"\",\n deepseek: \"https://api.deepseek.com/v1\",\n qwen: \"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\",\n glm: \"https://open.bigmodel.cn/api/paas/v4\",\n gemini: \"https://generativelanguage.googleapis.com/v1beta\",\n },\n reviewOverride: localStorage.getItem(\"autotasker-review-override\") || \"\",\n};\n\nconst els = {\n appShell: document.querySelector(\".app-shell, .app-layout\"),\n loginStatus: document.getElementById(\"login-status-chip\"),\n navItems: Array.from(document.querySelectorAll(\".nav-item\")),\n views: Array.from(document.querySelectorAll(\".view\")),\n navFocus: document.getElementById(\"nav-focus\"),\n authPanel: document.getElementById(\"auth-panel\"),\n authForm: document.getElementById(\"auth-form\"),\n authMessage: document.getElementById(\"auth-message\"),\n username: document.getElementById(\"username\"),\n password: document.getElementById(\"password\"),\n profileCard: document.getElementById(\"profile-card\"),\n profileName: document.getElementById(\"profile-name\"),\n profileHint: document.getElementById(\"profile-hint\"),\n preferencesForm: document.getElementById(\"preferences-form\"),\n focusMinutes: document.getElementById(\"focus-minutes\"),\n breakMinutes: document.getElementById(\"break-minutes\"),\n quietHours: document.getElementById(\"quiet-hours\"),\n aiForm: document.getElementById(\"ai-form\"),\n aiProvider: document.getElementById(\"ai-provider\"),\n aiModel: document.getElementById(\"ai-model\"),\n aiBaseUrl: document.getElementById(\"ai-base-url\"),\n aiApiKey: document.getElementById(\"ai-api-key\"),\n goalForm: document.getElementById(\"goal-form\"),\n goalText: document.getElementById(\"goal-text\"),\n goalFeedback: document.getElementById(\"goal-feedback\"),\n draftList: document.getElementById(\"draft-list\"),\n confirmDraft: document.getElementById(\"confirm-draft\"),\n clearDraft: document.getElementById(\"clear-draft\"),\n todoColumn: document.getElementById(\"todo-column\"),\n progressColumn: document.getElementById(\"progress-column\"),\n doneColumn: document.getElementById(\"done-column\"),\n todoCount: document.getElementById(\"todo-count\"),\n progressCount: document.getElementById(\"progress-count\"),\n doneCount: document.getElementById(\"done-count\"),\n pomodoroForm: document.getElementById(\"pomodoro-form\"),\n pomodoroTask: document.getElementById(\"pomodoro-task\"),\n pomodoroMinutes: document.getElementById(\"pomodoro-minutes\"),\n pomodoroPause: document.getElementById(\"pomodoro-pause\"),\n pomodoroDone: document.getElementById(\"pomodoro-done\"),\n pomodoroStop: document.getElementById(\"pomodoro-stop\"),\n timerCardEl: document.getElementById(\"timer-card-el\"),\n timerTaskName: document.getElementById(\"timer-task-name\"),\n timerValue: document.getElementById(\"timer-value\"),\n timerStatus: document.getElementById(\"timer-status\"),\n statsCards: document.getElementById(\"stats-cards\"),\n trendBars: document.getElementById(\"trend-bars\"),\n trendCaption: document.getElementById(\"trend-caption\"),\n reviewText: document.getElementById(\"review-text\"),\n logList: document.getElementById(\"log-list\"),\n generateReview: document.getElementById(\"generate-review\"),\n};\n\nlet countdownTicker = null;\nconst visualState = {\n revealObserver: null,\n reduceMotion: window.matchMedia(\"(prefers-reduced-motion: reduce)\"),\n finePointer: window.matchMedia(\"(hover: hover) and (pointer: fine)\"),\n scrollTicking: false,\n};\n\nconst MOTION_CARD_SELECTOR = [\n \".glass\",\n \".task-card\",\n \".draft-card\",\n \".metric\",\n \".log-item\",\n \".signal-card\",\n \".trend-wrap\",\n \".timer-card\",\n].join(\", \");\n\nasync function api(path, options = {}) {\n const headers = { \"Content-Type\": \"application/json\", ...(options.headers || {}) };\n if (state.token) headers.Authorization = `Bearer ${state.token}`;\n const response = await fetch(path, {\n ...options,\n headers,\n body: options.body ? JSON.stringify(options.body) : undefined,\n });\n const data = await response.json().catch(() => ({}));\n if (!response.ok) throw new Error(data.detail || data.error || \"请求失败\");\n return data;\n}\n\nfunction flash(message, isError = false) {\n els.authMessage.textContent = message;\n els.authMessage.style.color = isError ? \"#b5542c\" : \"#2c7a52\";\n}\n\nfunction setGoalFeedback(text, isMuted = false) {\n els.goalFeedback.textContent = text;\n els.goalFeedback.classList.toggle(\"empty\", isMuted);\n}\n\nfunction persistDraft() {\n localStorage.setItem(\"autotasker-draft\", JSON.stringify(state.draft));\n localStorage.setItem(\"autotasker-draft-goal\", state.draftGoalText || \"\");\n localStorage.setItem(\"autotasker-draft-feedback\", state.draftFeedback || \"\");\n}\n\nfunction clearDraftState() {\n state.draft = [];\n state.draftGoalText = \"\";\n state.draftFeedback = \"\";\n localStorage.removeItem(\"autotasker-draft\");\n localStorage.removeItem(\"autotasker-draft-goal\");\n localStorage.removeItem(\"autotasker-draft-feedback\");\n}\n\nfunction persistTimer() {\n if (state.timer) {\n localStorage.setItem(\"autotasker-timer\", JSON.stringify(state.timer));\n } else {\n localStorage.removeItem(\"autotasker-timer\");\n }\n}\n\nfunction persistAiSettings() {\n localStorage.setItem(\"autotasker-ai-settings\", JSON.stringify(state.aiSettings));\n}\n\nfunction persistReviewOverride() {\n if (state.reviewOverride) {\n localStorage.setItem(\"autotasker-review-override\", state.reviewOverride);\n } else {\n localStorage.removeItem(\"autotasker-review-override\");\n }\n}\n\nfunction formatDateTime(value) {\n if (!value) return \"未安排时间\";\n const date = new Date(value);\n if (Number.isNaN(date.getTime())) return value;\n return `${date.toLocaleDateString(\"zh-CN\", { month: \"2-digit\", day: \"2-digit\" })} ${date.toLocaleTimeString(\"zh-CN\", {\n hour: \"2-digit\",\n minute: \"2-digit\",\n })}`;\n}\n\nfunction getAiConfig() {\n return {\n provider: els.aiProvider.value,\n model: els.aiModel.value.trim(),\n base_url: els.aiBaseUrl.value.trim(),\n api_key: els.aiApiKey.value.trim(),\n };\n}\n\nfunction hydrateAiForm() {\n els.aiProvider.value = state.aiSettings.provider || \"openai\";\n els.aiModel.value = state.aiSettings.model || \"\";\n els.aiBaseUrl.value = state.aiSettings.base_url || state.providerPresets[state.aiSettings.provider] || \"\";\n els.aiApiKey.value = state.aiSettings.api_key || \"\";\n}\n\nfunction syncBaseUrlPreset(force = false) {\n const provider = els.aiProvider.value;\n if (provider === \"custom_compatible\") return;\n const preset = state.providerPresets[provider] || \"\";\n if (force || !els.aiBaseUrl.value.trim()) {\n els.aiBaseUrl.value = preset;\n }\n}\n\nfunction switchView(viewName) {\n if (!els.navItems.length || !els.views.length) return;\n els.navItems.forEach((button) => button.classList.toggle(\"active\", button.dataset.view === viewName));\n els.views.forEach((view) => view.classList.toggle(\"active\", view.id === `view-${viewName}`));\n}\n\nfunction renderAuth() {\n const loggedIn = Boolean(state.user);\n els.authPanel.classList.toggle(\"hidden\", loggedIn);\n els.profileCard.classList.toggle(\"hidden\", !loggedIn);\n if (els.loginStatus) {\n els.loginStatus.textContent = loggedIn ? state.user.username : \"Offline\";\n els.loginStatus.style.background = loggedIn ? \"#e6ffe6\" : \"#ffeeee\";\n els.loginStatus.style.color = loggedIn ? \"#006600\" : \"var(--accent-red)\";\n }\n if (!loggedIn) {\n switchView(\"settings\");\n return;\n }\n els.profileName.textContent = state.user.username;\n els.profileHint.textContent = `当前默认番茄 ${state.user.focus_minutes} 分钟,休息 ${state.user.break_minutes} 分钟`;\n els.focusMinutes.value = state.user.focus_minutes;\n els.breakMinutes.value = state.user.break_minutes;\n els.quietHours.value = state.user.quiet_hours;\n els.pomodoroMinutes.value = state.user.focus_minutes;\n hydrateAiForm();\n}\n\nfunction renderDraft() {\n if (!state.draft.length) {\n els.draftList.innerHTML = `<p class=\"empty\">生成草案后,你可以在这里调整任务标题、时间和时长。</p>`;\n els.confirmDraft.disabled = true;\n els.clearDraft.disabled = true;\n return;\n }\n els.confirmDraft.disabled = false;\n els.clearDraft.disabled = false;\n els.draftList.innerHTML = state.draft\n .map(\n (task, index) => `\n <article class=\"draft-card\">\n <div class=\"draft-grid\">\n <label>\n <span>任务标题</span>\n <input class=\"task-input\" data-field=\"title\" data-index=\"${index}\" value=\"${escapeHtml(task.title)}\" />\n </label>\n <label>\n <span>开始时间</span>\n <input class=\"task-input\" data-field=\"scheduled_for\" data-index=\"${index}\" type=\"datetime-local\" value=\"${toInputDate(task.scheduled_for)}\" />\n </label>\n <label>\n <span>截止时间</span>\n <input class=\"task-input\" data-field=\"due_at\" data-index=\"${index}\" type=\"datetime-local\" value=\"${toInputDate(task.due_at)}\" />\n </label>\n <label>\n <span>预计分钟</span>\n <input class=\"task-input\" data-field=\"estimated_minutes\" data-index=\"${index}\" type=\"number\" min=\"15\" max=\"180\" value=\"${task.estimated_minutes}\" />\n </label>\n </div>\n <label>\n <span>任务说明</span>\n <textarea class=\"task-input\" data-field=\"description\" data-index=\"${index}\" rows=\"2\">${escapeHtml(task.description || \"\")}</textarea>\n </label>\n </article>\n `\n )\n .join(\"\");\n}\n\nfunction renderKanban() {\n const groups = { todo: [], in_progress: [], done: [] };\n state.tasks.forEach((task) => groups[task.status].push(task));\n els.todoCount.textContent = groups.todo.length;\n els.progressCount.textContent = groups.in_progress.length;\n els.doneCount.textContent = groups.done.length;\n els.todoColumn.innerHTML = renderTaskCards(groups.todo);\n els.progressColumn.innerHTML = renderTaskCards(groups.in_progress);\n els.doneColumn.innerHTML = renderTaskCards(groups.done);\n setupDragAndDrop();\n renderPomodoroTasks();\n}\n\nfunction renderTaskCards(tasks) {\n if (!tasks.length) return `<p class=\"empty\">这里暂时没有任务。</p>`;\n return tasks\n .map(\n (task) => `\n <article class=\"task-card\" draggable=\"true\" data-task-id=\"${task.id}\">\n <div class=\"task-meta\">\n <span class=\"task-badge\">${task.status === \"todo\" ? \"待办\" : task.status === \"in_progress\" ? \"进行中\" : \"已完成\"}</span>\n <span class=\"task-badge focus\">${task.estimated_minutes} 分钟</span>\n ${task.overdue ? `<span class=\"task-badge overdue\">已逾期</span>` : \"\"}\n ${task.status === \"done\" ? `<span class=\"task-badge done\">完成</span>` : \"\"}\n </div>\n <div>\n <h4>${escapeHtml(task.title)}</h4>\n <p class=\"empty\">${escapeHtml(task.description || \"暂无说明\")}</p>\n </div>\n <div class=\"task-meta\">\n <span>${formatDateTime(task.scheduled_for)}</span>\n <span>${task.delay_count} 次延期</span>\n </div>\n <div class=\"task-actions\">\n ${task.status !== \"in_progress\" ? `<button class=\"btn btn-secondary\" data-action=\"status\" data-id=\"${task.id}\" data-status=\"in_progress\">开始</button>` : \"\"}\n ${task.status !== \"done\" ? `<button class=\"btn btn-primary\" data-action=\"status\" data-id=\"${task.id}\" data-status=\"done\">完成</button>` : \"\"}\n ${task.status !== \"todo\" ? `<button class=\"btn btn-ghost\" data-action=\"status\" data-id=\"${task.id}\" data-status=\"todo\">回待办</button>` : \"\"}\n ${task.status !== \"done\" ? `<button class=\"btn btn-ghost\" data-action=\"postpone\" data-id=\"${task.id}\">延期 30 分钟</button>` : \"\"}\n </div>\n </article>\n `\n )\n .join(\"\");\n}\n\nfunction renderPomodoroTasks() {\n const available = state.tasks.filter((task) => task.status !== \"done\");\n els.pomodoroTask.innerHTML = available.length\n ? available.map((task) => `<option value=\"${task.id}\">${escapeHtml(task.title)}</option>`).join(\"\")\n : `<option value=\"\">暂无可执行任务</option>`;\n}\n\nfunction renderStats() {\n const stats = state.stats || {\n total_tasks: 0,\n completion_rate: 0,\n overdue_tasks: 0,\n focus_minutes_total: 0,\n trend: [],\n review: \"暂无数据\",\n };\n els.statsCards.innerHTML = `\n <article class=\"metric\"><span>总任务</span><strong>${stats.total_tasks}</strong></article>\n <article class=\"metric\"><span>完成率</span><strong>${Math.round(stats.completion_rate * 100)}%</strong></article>\n <article class=\"metric\"><span>已逾期</span><strong>${stats.overdue_tasks}</strong></article>\n <article class=\"metric\"><span>专注分钟</span><strong>${stats.focus_minutes_total}</strong></article>\n `;\n const trend = stats.trend || [];\n if (!trend.length) {\n els.trendCaption.textContent = \"暂无完成记录\";\n els.trendBars.innerHTML = `<p class=\"empty\">完成任务后,这里会出现趋势柱状图。</p>`;\n } else {\n const max = Math.max(...trend.map((item) => item.count), 1);\n els.trendCaption.textContent = `最近 ${trend.length} 个日期有完成记录`;\n els.trendBars.innerHTML = trend\n .map(\n (item) => `\n <div class=\"trend-bar\">\n <div class=\"trend-bar-fill\" style=\"height:${Math.max(14, (item.count / max) * 92)}px\"></div>\n <strong>${item.count}</strong>\n <span>${item.day.slice(5)}</span>\n </div>\n `\n )\n .join(\"\");\n }\n els.reviewText.textContent = state.reviewOverride || stats.review;\n els.reviewText.classList.toggle(\"empty\", !els.reviewText.textContent);\n}\n\nfunction renderLogs() {\n if (!state.recentLogs.length) {\n els.logList.innerHTML = `<p class=\"empty\">还没有番茄钟记录,开始第一轮后会展示在这里。</p>`;\n return;\n }\n els.logList.innerHTML = state.recentLogs\n .map(\n (item) => `\n <article class=\"log-item\">\n <strong>${escapeHtml(item.task_title)}</strong>\n <div class=\"log-meta\">\n <span>${item.planned_minutes} 分钟</span>\n <span>${Math.round(item.actual_seconds / 60)} 分钟实际专注</span>\n <span>${item.status === \"done\" ? \"完成\" : item.status === \"running\" ? \"进行中\" : \"中断\"}</span>\n </div>\n </article>\n `\n )\n .join(\"\");\n}\n\nfunction renderTimer() {\n if (!state.timer) {\n els.timerCardEl?.classList.remove(\"is-running\");\n els.navFocus?.classList.remove(\"has-timer\");\n els.timerTaskName.textContent = \"未开始\";\n els.timerValue.textContent = `${String(Number(els.pomodoroMinutes.value || 25)).padStart(2, \"0\")}:00`;\n els.timerStatus.textContent = \"等待选择一个任务开始。\";\n els.pomodoroPause.disabled = true;\n els.pomodoroDone.disabled = true;\n els.pomodoroStop.disabled = true;\n els.pomodoroPause.textContent = \"Pause\";\n return;\n }\n const task = state.tasks.find((item) => item.id === state.timer.taskId);\n const remaining = Math.max(0, Math.floor((state.timer.endsAt - Date.now()) / 1000));\n els.navFocus?.classList.add(\"has-timer\");\n els.timerTaskName.textContent = task ? task.title : \"进行中的任务\";\n els.timerValue.textContent = `${String(Math.floor(remaining / 60)).padStart(2, \"0\")}:${String(remaining % 60).padStart(2, \"0\")}`;\n els.timerStatus.textContent = state.timer.paused\n ? `已暂停,已累计 ${Math.round(state.timer.elapsedSeconds / 60)} 分钟。`\n : remaining > 0\n ? \"专注进行中,结束后可直接记为完成。\"\n : \"时间到,可以完成本轮或继续追加。\";\n els.timerCardEl?.classList.toggle(\"is-running\", !state.timer.paused && remaining > 0);\n els.pomodoroPause.disabled = false;\n els.pomodoroDone.disabled = false;\n els.pomodoroStop.disabled = false;\n els.pomodoroPause.textContent = state.timer.paused ? \"Resume\" : \"Pause\";\n}\n\nfunction renderAll() {\n renderAuth();\n renderDraft();\n renderKanban();\n renderStats();\n renderLogs();\n renderTimer();\n if (state.draftGoalText) els.goalText.value = state.draftGoalText;\n if (state.draftFeedback) setGoalFeedback(state.draftFeedback);\n refreshImmersiveUi();\n}\n\nfunction createRevealObserver() {\n if (visualState.revealObserver) return visualState.revealObserver;\n visualState.revealObserver = new IntersectionObserver(\n (entries) => {\n entries.forEach((entry) => {\n if (entry.isIntersecting) {\n entry.target.classList.add(\"is-visible\");\n } else if (!visualState.reduceMotion.matches) {\n entry.target.classList.remove(\"is-visible\");\n }\n });\n },\n {\n rootMargin: \"0px 0px -12% 0px\",\n threshold: 0.12,\n }\n );\n return visualState.revealObserver;\n}\n\nfunction applyScrollScene() {\n const progress = Math.min(window.scrollY / Math.max(window.innerHeight, 1), 1);\n document.body.style.setProperty(\"--scroll-progress\", progress.toFixed(3));\n if (els.appShell) {\n els.appShell.style.setProperty(\"--shell-shift\", `${progress * -12}px`);\n }\n}\n\nfunction queueScrollScene() {\n if (visualState.scrollTicking) return;\n visualState.scrollTicking = true;\n window.requestAnimationFrame(() => {\n applyScrollScene();\n visualState.scrollTicking = false;\n });\n}\n\nfunction bindCardTilt(element, index) {\n if (element.dataset.motionBound === \"true\") return;\n element.dataset.motionBound = \"true\";\n element.style.setProperty(\"--reveal-delay\", `${Math.min(index * 35, 280)}ms`);\n\n element.addEventListener(\"pointermove\", (event) => {\n if (visualState.reduceMotion.matches || !visualState.finePointer.matches) return;\n const rect = element.getBoundingClientRect();\n const px = (event.clientX - rect.left) / rect.width;\n const py = (event.clientY - rect.top) / rect.height;\n const rotateY = (px - 0.5) * 12;\n const rotateX = (0.5 - py) * 10;\n const glowX = `${Math.round(px * 100)}%`;\n const glowY = `${Math.round(py * 100)}%`;\n\n element.style.setProperty(\"--tilt-x\", `${rotateX.toFixed(2)}deg`);\n element.style.setProperty(\"--tilt-y\", `${rotateY.toFixed(2)}deg`);\n element.style.setProperty(\"--glow-x\", glowX);\n element.style.setProperty(\"--glow-y\", glowY);\n element.style.setProperty(\"--pointer-alpha\", \"1\");\n });\n\n element.addEventListener(\"pointerleave\", () => {\n element.style.setProperty(\"--tilt-x\", \"0deg\");\n element.style.setProperty(\"--tilt-y\", \"0deg\");\n element.style.setProperty(\"--pointer-alpha\", \"0\");\n });\n}\n\nfunction refreshImmersiveUi() {\n const observer = createRevealObserver();\n document.querySelectorAll(MOTION_CARD_SELECTOR).forEach((element, index) => {\n element.classList.add(\"motion-reveal\");\n bindCardTilt(element, index);\n observer.observe(element);\n });\n}\n\nfunction setupImmersiveUi() {\n document.body.classList.add(\"immersive-ready\");\n applyScrollScene();\n refreshImmersiveUi();\n\n window.addEventListener(\"scroll\", queueScrollScene, { passive: true });\n window.addEventListener(\"resize\", queueScrollScene);\n\n document.addEventListener(\"pointermove\", (event) => {\n if (visualState.reduceMotion.matches || !visualState.finePointer.matches) return;\n const x = (event.clientX / window.innerWidth) * 100;\n const y = (event.clientY / window.innerHeight) * 100;\n document.body.style.setProperty(\"--cursor-x\", `${x.toFixed(2)}%`);\n document.body.style.setProperty(\"--cursor-y\", `${y.toFixed(2)}%`);\n });\n\n visualState.reduceMotion.addEventListener(\"change\", () => {\n document.body.classList.toggle(\"reduced-motion\", visualState.reduceMotion.matches);\n refreshImmersiveUi();\n });\n\n visualState.finePointer.addEventListener(\"change\", () => {\n document.body.classList.toggle(\"fine-pointer\", visualState.finePointer.matches);\n });\n\n document.body.classList.toggle(\"reduced-motion\", visualState.reduceMotion.matches);\n document.body.classList.toggle(\"fine-pointer\", visualState.finePointer.matches);\n}\n\nasync function bootstrap() {\n if (!state.token) {\n renderAll();\n return;\n }\n try {\n const data = await api(\"/api/bootstrap\");\n state.user = data.user;\n state.tasks = data.tasks;\n state.stats = data.stats;\n state.recentLogs = data.recent_logs;\n state.providerPresets = { ...state.providerPresets, ...(data.provider_presets || {}) };\n renderAll();\n } catch (error) {\n state.token = \"\";\n state.user = null;\n localStorage.removeItem(\"autotasker-token\");\n flash(error.message, true);\n renderAll();\n }\n}\n\nasync function submitAuth(mode) {\n const username = els.username.value.trim();\n const password = els.password.value.trim();\n const path = mode === \"register\" ? \"/api/auth/register\" : \"/api/auth/login\";\n const data = await api(path, { method: \"POST\", body: { username, password } });\n state.token = data.token;\n state.user = data.user;\n localStorage.setItem(\"autotasker-token\", state.token);\n flash(mode === \"register\" ? \"注册成功,已自动登录。\" : \"登录成功。\");\n await bootstrap();\n switchView(\"dashboard\");\n}\n\nasync function generateDraft() {\n const goalText = els.goalText.value.trim();\n if (!goalText) {\n setGoalFeedback(\"请先输入一个明确目标。\");\n return;\n }\n state.aiSettings = getAiConfig();\n persistAiSettings();\n const data = await api(\"/api/goals/analyze\", {\n method: \"POST\",\n body: { goal_text: goalText, ai_config: state.aiSettings },\n });\n state.draftGoalText = goalText;\n state.draftFeedback = data.agent_feedback;\n state.draft = data.staging_tasks || [];\n persistDraft();\n const meta = data.provider && data.model ? `当前模型:${data.provider} / ${data.model}` : \"\";\n setGoalFeedback([data.agent_feedback, meta].filter(Boolean).join(\"\\n\"));\n renderDraft();\n}\n\nasync function confirmDraft() {\n if (!state.draft.length) return;\n await api(\"/api/goals/confirm\", {\n method: \"POST\",\n body: {\n goal_text: state.draftGoalText,\n agent_feedback: state.draftFeedback,\n tasks: state.draft,\n },\n });\n clearDraftState();\n setGoalFeedback(\"草案已同步到正式看板。\");\n await bootstrap();\n switchView(\"kanban\");\n}\n\nasync function updatePreferences() {\n const data = await api(\"/api/preferences\", {\n method: \"PUT\",\n body: {\n focus_minutes: Number(els.focusMinutes.value),\n break_minutes: Number(els.breakMinutes.value),\n quiet_hours: els.quietHours.value.trim(),\n },\n });\n state.user = data.user;\n renderAuth();\n}\n\nfunction saveAiSettings() {\n state.aiSettings = getAiConfig();\n persistAiSettings();\n flash(\"AI 配置已保存在当前浏览器。\");\n}\n\nasync function generateAiReview() {\n state.aiSettings = getAiConfig();\n persistAiSettings();\n const data = await api(\"/api/review/generate\", {\n method: \"POST\",\n body: { ai_config: state.aiSettings },\n });\n state.reviewOverride = `${data.review}\\n\\n来自 ${data.provider} / ${data.model}`;\n persistReviewOverride();\n renderStats();\n}\n\nasync function updateTaskStatus(taskId, status) {\n await api(`/api/tasks/${taskId}/status`, { method: \"PATCH\", body: { status } });\n await bootstrap();\n}\n\nasync function postponeTask(taskId) {\n await api(\"/api/tasks/postpone\", { method: \"POST\", body: { task_id: Number(taskId), minutes: 30 } });\n await bootstrap();\n}\n\nasync function startPomodoro() {\n const taskId = Number(els.pomodoroTask.value);\n if (!taskId) return;\n const minutes = Number(els.pomodoroMinutes.value) || (state.user?.focus_minutes ?? 25);\n const data = await api(\"/api/pomodoro/start\", {\n method: \"POST\",\n body: { task_id: taskId, planned_minutes: minutes },\n });\n state.timer = {\n logId: data.log_id,\n taskId,\n plannedMinutes: minutes,\n startedAt: Date.now(),\n endsAt: Date.now() + minutes * 60 * 1000,\n paused: false,\n pausedAt: null,\n elapsedSeconds: 0,\n };\n persistTimer();\n startTicker();\n await bootstrap();\n switchView(\"focus\");\n}\n\nasync function finishPomodoro(result) {\n if (!state.timer) return;\n const elapsedSeconds = state.timer.paused\n ? state.timer.elapsedSeconds\n : state.timer.elapsedSeconds + Math.max(0, Math.floor((Date.now() - state.timer.startedAt) / 1000));\n await api(\"/api/pomodoro/finish\", {\n method: \"POST\",\n body: {\n log_id: state.timer.logId,\n task_id: state.timer.taskId,\n actual_seconds: elapsedSeconds,\n result,\n },\n });\n state.timer = null;\n persistTimer();\n stopTicker();\n await bootstrap();\n}\n\nfunction togglePause() {\n if (!state.timer) return;\n if (state.timer.paused) {\n const pauseDuration = Date.now() - state.timer.pausedAt;\n state.timer.startedAt += pauseDuration;\n state.timer.endsAt += pauseDuration;\n state.timer.paused = false;\n state.timer.pausedAt = null;\n } else {\n state.timer.elapsedSeconds += Math.max(0, Math.floor((Date.now() - state.timer.startedAt) / 1000));\n state.timer.paused = true;\n state.timer.pausedAt = Date.now();\n }\n persistTimer();\n renderTimer();\n}\n\nfunction startTicker() {\n stopTicker();\n countdownTicker = window.setInterval(() => {\n if (state.timer && !state.timer.paused && state.timer.endsAt <= Date.now()) {\n state.timer.endsAt = Date.now();\n }\n renderTimer();\n persistTimer();\n }, 1000);\n}\n\nfunction stopTicker() {\n if (countdownTicker) {\n clearInterval(countdownTicker);\n countdownTicker = null;\n }\n}\n\nfunction setupDragAndDrop() {\n document.querySelectorAll(\".task-card\").forEach((card) => {\n card.addEventListener(\"dragstart\", (event) => {\n card.classList.add(\"dragging\");\n event.dataTransfer?.setData(\"text/plain\", card.dataset.taskId);\n });\n card.addEventListener(\"dragend\", () => card.classList.remove(\"dragging\"));\n });\n document.querySelectorAll(\".column-body\").forEach((column) => {\n column.addEventListener(\"dragover\", (event) => {\n event.preventDefault();\n column.classList.add(\"drag-over\");\n });\n column.addEventListener(\"dragleave\", () => column.classList.remove(\"drag-over\"));\n column.addEventListener(\"drop\", async (event) => {\n event.preventDefault();\n column.classList.remove(\"drag-over\");\n const taskId = Number(event.dataTransfer.getData(\"text/plain\"));\n const status = column.parentElement.dataset.status;\n if (taskId && status) await updateTaskStatus(taskId, status);\n });\n });\n}\n\nfunction escapeHtml(value) {\n return String(value)\n .replaceAll(\"&\", \"&amp;\")\n .replaceAll(\"<\", \"&lt;\")\n .replaceAll(\">\", \"&gt;\")\n .replaceAll('\"', \"&quot;\");\n}\n\nfunction toInputDate(value) {\n if (!value) return \"\";\n const date = new Date(value);\n if (Number.isNaN(date.getTime())) return \"\";\n const offset = date.getTimezoneOffset() * 60_000;\n return new Date(date.getTime() - offset).toISOString().slice(0, 16);\n}\n\ndocument.addEventListener(\"input\", (event) => {\n const target = event.target;\n if (target.classList.contains(\"task-input\")) {\n const index = Number(target.dataset.index);\n const field = target.dataset.field;\n state.draft[index][field] = field === \"estimated_minutes\" ? Number(target.value) : target.value;\n persistDraft();\n }\n});\n\ndocument.addEventListener(\"click\", async (event) => {\n const target = event.target;\n if (target.dataset.action === \"status\") {\n await updateTaskStatus(target.dataset.id, target.dataset.status);\n }\n if (target.dataset.action === \"postpone\") {\n await postponeTask(target.dataset.id);\n }\n});\n\nels.navItems.forEach((button) => {\n button.addEventListener(\"click\", () => switchView(button.dataset.view));\n});\n\nels.aiProvider.addEventListener(\"change\", () => syncBaseUrlPreset(true));\n\nels.authForm.addEventListener(\"submit\", async (event) => {\n event.preventDefault();\n const mode = event.submitter?.dataset.mode || \"login\";\n try {\n await submitAuth(mode);\n } catch (error) {\n flash(error.message, true);\n }\n});\n\nels.preferencesForm.addEventListener(\"submit\", async (event) => {\n event.preventDefault();\n try {\n await updatePreferences();\n flash(\"偏好已更新。\");\n } catch (error) {\n flash(error.message, true);\n }\n});\n\nels.aiForm.addEventListener(\"submit\", (event) => {\n event.preventDefault();\n saveAiSettings();\n});\n\nels.goalForm.addEventListener(\"submit\", async (event) => {\n event.preventDefault();\n try {\n await generateDraft();\n } catch (error) {\n setGoalFeedback(error.message);\n }\n});\n\nels.confirmDraft.addEventListener(\"click\", async () => {\n try {\n await confirmDraft();\n } catch (error) {\n setGoalFeedback(error.message);\n }\n});\n\nels.clearDraft.addEventListener(\"click\", () => {\n clearDraftState();\n setGoalFeedback(\"草案已清空。\");\n renderDraft();\n});\n\nels.generateReview.addEventListener(\"click\", async () => {\n try {\n await generateAiReview();\n } catch (error) {\n state.reviewOverride = \"\";\n persistReviewOverride();\n els.reviewText.textContent = error.message;\n }\n});\n\nels.pomodoroForm.addEventListener(\"submit\", async (event) => {\n event.preventDefault();\n try {\n await startPomodoro();\n } catch (error) {\n els.timerStatus.textContent = error.message;\n }\n});\n\nels.pomodoroPause.addEventListener(\"click\", togglePause);\nels.pomodoroDone.addEventListener(\"click\", async () => finishPomodoro(\"done\"));\nels.pomodoroStop.addEventListener(\"click\", async () => finishPomodoro(\"interrupted\"));\n\nif (state.timer) startTicker();\nhydrateAiForm();\nrenderAll();\nsetupImmersiveUi();\nbootstrap();\n"}}}}