iT邦幫忙

2026 iThome 鐵人賽

DAY 29
0

前言

昨天把新增和修改功能搬進網頁後,現在已經可以直接在頁面上管理 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>

加入 CSS

放進你原本的 <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;
}

做到這裡重新整理,應該會看到
image

但現在按鈕還不能用。

讓網頁呼叫 /ai/next

在你最下面的 裡加入:

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/

螢幕擷取畫面 2026-10-13 003103
不過使用者看到任務 ID:13

根本不知道 13 是哪一個 Todo,
所以接下來不能只把 Gemini 的結果直接丟到網頁,還要找到 task_id = 13 對應的真正任務資料。

讓 AI 顯示「任務名稱」

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 筆任務。

修改 app.py 的 /ai/next

剛剛第一次測試雖然成功了,但目前 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 自己產生一次。

加入 remaining_minutes

這裡我也把前面做過的 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 給使用者。
螢幕擷取畫面 2026-10-13 004155

怎麼全部變成 undefined 了?

原本以為改完之後就可以直接看到完整資料,結果重新測試時卻變成:

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 分鐘測試。
螢幕擷取畫面 2026-10-13 004414

最後測試

重新測試後,這次終於成功了。

我輸入目前有:

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 天到底完成了什麼。


上一篇
Day 28|不用再開 Postman:直接在網頁新增、修改 Todo
下一篇
Day 30|最終篇:總結從 Todo API 到 AI 任務規劃系統
系列文
如果 AI 幫我安排每日的任務?——讓待辦清單不只是清單:LLM × Flask × MySQL 打造動態任務規劃系統 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言