昨天把新增和修改功能搬進網頁後,現在已經可以直接在頁面上管理 Todo。
但做到這裡其實還有一個問題:
前面明明已經讓 Gemini 會根據任務資料判斷優先順序,現在網頁卻還是只能讓使用者自己看著任務決定要做什麼。
所以今天要把前面做好的 /ai/next 正式接進網頁。
讓使用者只要輸入「我現在有多少時間」,Gemini 就會從 MySQL 的未完成任務中,找出目前最推薦先做的一個。
在你 index.html 裡找到:
<h1>AI Todo</h1>
<p class="subtitle">今天要做什麼?</p>
就在它下面、+新增任務上面加入:
<div class="ai-planner">
<h2>AI 幫我安排</h2>
<p>我現在有多少時間?</p>
<div class="ai-input">
<input
type="number"
id="available-minutes"
placeholder="例如:60"
min="1"
>
<span>分鐘</span>
<button onclick="getAIRecommendation()">
AI 幫我安排
</button>
</div>
<div id="ai-result" class="ai-result">
</div>
</div>
放進你原本的 <style>:
.ai-planner {
background: white;
padding: 20px;
margin-bottom: 20px;
border-radius: 10px;
}
.ai-planner h2 {
margin-top: 0;
}
.ai-input {
display: flex;
align-items: center;
gap: 10px;
}
.ai-input input {
width: 120px;
padding: 10px;
border: 1px solid #ccc;
border-radius: 6px;
}
.ai-input button {
background: #222;
color: white;
}
.ai-result {
display: none;
margin-top: 20px;
padding: 20px;
background: #f5f5f5;
border-radius: 8px;
}
.ai-result h3 {
margin-top: 0;
}
.ai-reason {
color: #555;
line-height: 1.6;
}
做到這裡重新整理,應該會看到
但現在按鈕還不能用。
在你最下面的 裡加入:
async function getAIRecommendation() {
const availableMinutes =
document.getElementById("available-minutes").value;
const resultBox =
document.getElementById("ai-result");
if (!availableMinutes || Number(availableMinutes) <= 0) {
alert("請輸入可以使用的時間");
return;
}
resultBox.style.display = "block";
resultBox.innerHTML = "<p>AI 正在幫你安排...</p>";
try {
const response = await fetch(
`/ai/next?available_minutes=${availableMinutes}`
);
const data = await response.json();
if (!response.ok) {
resultBox.innerHTML =
`<p>${data.error || "AI 分析失敗"}</p>`;
return;
}
console.log("AI 回傳結果:", data);
resultBox.innerHTML = `
<h3>AI 建議你現在先做</h3>
<p>
任務 ID:${data.task_id}
</p>
<p class="ai-reason">
${data.reason}
</p>
`;
} catch (error) {
console.error(error);
resultBox.innerHTML =
"<p>AI 分析時發生錯誤</p>";
}
}
這裡會先取得使用者輸入的 available_minutes,再把它放到網址參數中:
/ai/next?available_minutes=60
Flask 收到 60 分鐘後,就會把目前未完成的任務和可用時間一起交給 Gemini 判斷。
現在的 /ai/next 最後回傳的是 Gemini 產生的 JSON:
{
"task_id": 1,
"priority": 1,
"reason": "原因"
}
所以前端目前可以直接拿 task_id 和 reason。
我們先確認 網頁 → Flask → Gemini → 網頁 這整條真的通。
打開:http://127.0.0.1:5000/

不過使用者看到任務 ID:13
根本不知道 13 是哪一個 Todo,
所以接下來不能只把 Gemini 的結果直接丟到網頁,還要找到 task_id = 13 對應的真正任務資料。
Gemini 選出最推薦的 task_id 後,再從原本 MySQL 查到的 tasks 裡找出對應任務:
recommended_task = None
for task in tasks:
if task["task_id"] == result["task_id"]:
recommended_task = task
break
如果 AI 回傳:task_id = 13
程式就會去找到 MySQL 裡真正的第 13 筆任務。
剛剛第一次測試雖然成功了,但目前 Gemini 回傳的資料只有:
{
"task_id": 13,
"priority": 1,
"reason": "原因"
}
所以網頁只能顯示「任務 ID:13」。
但對使用者來說,根本不會知道 13 代表哪一個任務,因此接下來要根據 Gemini 選出的 task_id,回去找到 MySQL 中真正的任務資料。
原本 /ai/next 最後只有:
result = json.loads(response.text)
return result
這裡改成:
result = json.loads(response.text)
recommended_task = None
for task in tasks:
if task["task_id"] == result["task_id"]:
recommended_task = task
break
if recommended_task is None:
return {
"error": "找不到 AI 推薦的任務"
}, 404
remaining_minutes = (
recommended_task["estimated_minutes"]
* (100 - recommended_task["progress"])
/ 100
)
return {
"task_id": recommended_task["task_id"],
"title": recommended_task["title"],
"deadline": recommended_task["deadline"].strftime("%Y-%m-%d %H:%M:%S"),
"importance": recommended_task["importance"],
"estimated_minutes": recommended_task["estimated_minutes"],
"progress": recommended_task["progress"],
"remaining_minutes": remaining_minutes,
"reason": result["reason"]
}
這裡 Gemini 還是只負責判斷「哪一個 task_id 最推薦」。
例如 Gemini 選出:
task_id = 13
Flask 再從剛才 MySQL 查到的 tasks 中找到真正的第 13 筆任務:
if task["task_id"] == result["task_id"]:
找到之後,再把任務名稱、截止日期、重要程度、預估時間和目前進度一起回傳給前端。
這些資料本來就在 MySQL 裡,所以不需要再叫 Gemini 自己產生一次。
這裡我也把前面做過的 progress 再利用一次:
remaining_minutes = (
recommended_task["estimated_minutes"]
* (100 - recommended_task["progress"])
/ 100
)
例如一個任務原本預估需要 60 分鐘,目前完成 50%,那麼真正剩下的時間就是:
60 × (100 - 50) / 100
= 30 分鐘
這樣最後顯示的就不只是原本的預估時間,也可以看到這個任務目前實際還剩下多少時間。
後端已經會回傳完整的任務資料後,接著修改 index.html。
原本只有顯示:
resultBox.innerHTML = `
<h3>AI 建議你現在先做</h3>
<p>
任務 ID:${data.task_id}
</p>
<p class="ai-reason">
${data.reason}
</p>
`;
現在把它改成:
resultBox.innerHTML = `
<h3>AI 建議你現在先做</h3>
<h2>${data.title}</h2>
<p>
截止日期:${data.deadline}
</p>
<p>
重要程度:${data.importance}
</p>
<p>
預估時間:${data.estimated_minutes} 分鐘
</p>
<p>
目前進度:${data.progress}%
</p>
<p>
剩餘時間:${data.remaining_minutes} 分鐘
</p>
<p class="ai-reason">
<strong>AI 推薦原因:</strong><br>
${data.reason}
</p>
`;
這樣前端就不會只顯示看不懂的 task_id,而是會直接把 Gemini 推薦的 Todo 資料顯示出來。
原本:
AI 建議你現在先做
任務 ID:13
現在就會變成:
AI 建議你現在先做
測試高重要程度任務
截止日期:2026-09-10 23:59:00
重要程度:3
預估時間:60 分鐘
目前進度:0%
剩餘時間:60 分鐘
看起來終於比較像是真的在推薦「任務」,而不是只丟一個資料庫 ID 給使用者。
原本以為改完之後就可以直接看到完整資料,結果重新測試時卻變成:
undefined
截止日期:undefined
重要程度:undefined
預估時間:undefined 分鐘
目前進度:undefined%
剩餘時間:undefined 分鐘
但奇怪的是,最下面的「AI 推薦原因」卻有正常顯示。
所以 Gemini 本身應該沒有出問題。
後來檢查 /ai/next 才發現,原本的:
return result
還沒有正確換掉。
因此 Flask 實際回傳給前端的還是:
{
"task_id": 13,
"priority": 1,
"reason": "原因"
}
裡面根本沒有 title、deadline、importance、estimated_minutes、progress 和 remaining_minutes。
所以當 JavaScript 執行:
data.title
自然就只會拿到:
undefined
把 /ai/next 最後的回傳內容修正後,再重新啟動 Flask:
python app.py
接著重新輸入 60 分鐘測試。
重新測試後,這次終於成功了。
我輸入目前有:
60 分鐘
Gemini 最後推薦:
測試高重要程度任務
而且這次不只有推薦原因,任務的截止日期、重要程度、預估時間、目前進度和剩餘時間也都有成功顯示。
Gemini 給出的原因是這個任務的 Deadline 最近,而且重要程度較高,加上預估時間剛好符合目前可以使用的 60 分鐘,因此建議優先處理。
到這裡,前面做過的 /ai/next 終於不再只是用 API 測試,而是真的被放進 Todo 網頁裡了。
現在除了可以新增、修改、完成和刪除任務之外,也可以直接告訴系統「我現在有多少時間」,再讓 Gemini 根據 MySQL 裡真正的任務資料,判斷現在最推薦先做哪一件事情。
從一開始只能在 Postman 測試 API,到現在已經可以直接在網頁上操作,前面分開完成的功能也終於慢慢接在一起了。
30 天也只剩最後一天。
Day 30 就不再繼續加入新功能,而是把這 30 天做過的 Flask、MySQL、Todo CRUD、Gemini 排序到最後的 AI Todo 完整整理一次,看看這 30 天到底完成了什麼。