1 line
73 KiB
JSON
1 line
73 KiB
JSON
{"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(\"&\", \"&\")\n .replaceAll(\"<\", \"<\")\n .replaceAll(\">\", \">\")\n .replaceAll('\"', \""\");\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(\"&\", \"&\")\n .replaceAll(\"<\", \"<\")\n .replaceAll(\">\", \">\")\n .replaceAll('\"', \""\");\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"}}}} |