iT邦幫忙

2026 iThome 鐵人賽

0
AI Engineering

Java × AI:30天打造智慧健康助理系列 第 28 篇

用 HTML、CSS 和 JavaScript 打造智慧健康助理聊天介面

  • 分享至 

  • xImage
  •  

Day 28|用 HTML、CSS 和 JavaScript 打造智慧健康助理聊天介面

從最初的 BMI 計算、飲水紀錄、睡眠紀錄,到後來的 AI 健康摘要、多輪對話,以及聊天紀錄資料庫保存。

現在我們的智慧健康助理,已經可以透過 Spring Boot 提供各種 API。

不過到目前為止,我們幾乎都使用 Postman 測試。

例如:

POST http://localhost:8080/health/ai/conversation

再手動輸入:

{
  "question": "什麼是 BMI?"
}

雖然這對開發者來說很方便,但如果未來想讓一般使用者操作,總不能要求每個人都先學會 Postman 吧 XD

所以今天決定挑戰一個我期待很久的功能:

替智慧健康助理建立自己的聊天網頁!

今天的目標是:

使用 HTML、CSS 和 JavaScript 建立簡易聊天介面,透過 fetch() 呼叫 Spring Boot API,讓使用者可以直接在瀏覽器中與 AI 健康助理進行多輪對話。


一、今天要完成什麼功能?

今天準備建立一個簡單的聊天網頁。

畫面大概會長這樣:

┌──────────────────────────────────────┐
│                                      │
│         智慧健康助理                 │
│      AI Health Assistant             │
│                                      │
├──────────────────────────────────────┤
│                                      │
│  AI:你好!有什麼健康知識想了解嗎?  │
│                                      │
│               使用者:什麼是 BMI?   │
│                                      │
│  AI:BMI 是身體質量指數,常用於      │
│      成人體位的初步評估。            │
│                                      │
│               使用者:怎麼計算?     │
│                                      │
│  AI:體重除以身高的平方。            │
│                                      │
├──────────────────────────────────────┤
│                                      │
│  請輸入你的問題……            [送出]  │
│                                      │
│  [開始新對話]                        │
│                                      │
└──────────────────────────────────────┘

今天會完成以下功能:

  1. 建立 HTML 聊天頁面。
  2. 使用 CSS 美化介面。
  3. 使用 JavaScript 取得輸入內容。
  4. 透過 fetch() 呼叫 AI 對話 API。
  5. 將使用者問題顯示在畫面上。
  6. 將 AI 回答顯示在畫面上。
  7. 保存 conversationId,支援多輪對話。
  8. 加入「開始新對話」按鈕。
  9. 顯示 AI 回覆中的等待狀態。
  10. 處理基本的 API 錯誤。

今天不需要新增資料庫,也不需要修改 AI 模型。

因為我們要做的,是把前面已經完成的功能整合成可以操作的網頁。


二、前端與後端是什麼?

在開始之前,先認識兩個常見的名詞:

Frontend

以及:

Backend

也就是:

前端與後端。

前端主要負責使用者看到的畫面,以及操作時的互動。

例如:

聊天視窗
輸入框
送出按鈕
AI 回答顯示

後端則負責處理資料與程式邏輯。

例如:

接收使用者問題
查詢歷史對話
呼叫 AI API
保存聊天紀錄
回傳 JSON

今天的技術分工如下:

技術 負責工作
HTML 建立網頁結構
CSS 設計畫面樣式
JavaScript 處理使用者互動
Fetch API 發送 HTTP Request
Spring Boot 提供後端 API
H2 Database 保存聊天紀錄
OpenAI API 產生 AI 回答

整個流程可以表示成:

使用者
   ↓
HTML 聊天畫面
   ↓
JavaScript
   ↓
fetch()
   ↓
Spring Boot Controller
   ↓
AIConversationService
   │
   ├── ConversationRepository
   │       ↓
   │    H2 Database
   │
   └── AIService
           ↓
       OpenAI API
           ↓
       AI 回答
   ↓
Spring Boot 回傳 JSON
   ↓
JavaScript 更新畫面
   ↓
使用者看到 AI 回答

這樣我們就把前端和後端串起來了。


三、Spring Boot 也可以提供網頁嗎?

之前我們建立的 Spring Boot 專案,主要用來提供 REST API。

例如:

GET /health/summary

或是:

POST /health/ai/conversation

但其實 Spring Boot 也可以直接提供靜態網頁。

只要把 HTML、CSS、JavaScript 放在指定資料夾,Spring Boot 就能提供這些檔案。

今天會使用:

src/main/resources/static

這個資料夾。

我們準備新增三個檔案:

index.html
style.css
app.js

最後的結構如下:

src
└── main
    └── resources
        ├── application.properties
        ├── schema.sql
        │
        └── static
            ├── index.html
            ├── style.css
            └── app.js

其中:

index.html

是主要網頁。

style.css

負責畫面設計。

app.js

負責呼叫 API 與更新聊天內容。

因為我們之前已經加入 spring-boot-starter-web,今天不需要額外安裝前端框架。

也不需要先學 React 或 Vue。

先用最基本的 HTML、CSS 和 JavaScript,就可以完成今天的功能!


Part 1|建立 HTML 聊天畫面

四、建立 index.html

首先在:

src/main/resources/static

新增:

index.html

完整程式如下:

<!DOCTYPE html>
<html lang="zh-Hant">

<head>
    <meta charset="UTF-8">

    <meta
        name="viewport"
        content="width=device-width, initial-scale=1.0"
    >

    <title>智慧健康助理</title>

    <link rel="stylesheet" href="/style.css">
</head>

<body>

    <div class="chat-container">

        <header class="chat-header">

            <div>
                <h1>智慧健康助理</h1>
                <p>AI Health Assistant</p>
            </div>

            <button
                id="newChatButton"
                class="new-chat-button"
                type="button"
            >
                新對話
            </button>

        </header>

        <main
            id="chatMessages"
            class="chat-messages"
            aria-live="polite"
            aria-relevant="additions"
        >

            <div class="message assistant">

                <div class="message-content">
                    你好!我是智慧健康助理。
                    你可以詢問我一般健康知識,
                    例如 BMI、飲水習慣或睡眠相關問題。
                </div>

            </div>

        </main>

        <div
            id="statusMessage"
            class="status-message"
            role="status"
        ></div>

        <form id="chatForm" class="chat-input-area">

            <input
                type="text"
                id="questionInput"
                placeholder="請輸入你的問題……"
                maxlength="500"
                autocomplete="off"
                aria-label="聊天問題"
                required
            >

            <button
                id="sendButton"
                type="submit"
            >
                送出
            </button>

        </form>

        <footer class="chat-footer">
            本系統僅提供一般健康資訊,
            不取代專業醫療診斷或治療。
        </footer>

    </div>

    <script src="/app.js"></script>

</body>
</html>

這個 HTML 看起來有點長,但其實可以分成四個主要區域。


五、認識 HTML 的主要區塊

1. Header:網頁標題

首先是:

<header class="chat-header">

這裡會顯示:

智慧健康助理
AI Health Assistant

以及:

新對話

按鈕。

未來當使用者想重新開始聊天,就可以按下這個按鈕。

2. Main:聊天訊息區

接著是:

<main id="chatMessages" class="chat-messages">

這裡負責顯示所有聊天訊息。

例如:

AI:你好!
使用者:什麼是 BMI?
AI:BMI 是身體質量指數。

我們給它一個 ID:

chatMessages

這樣 JavaScript 就可以找到這個區域,並且動態加入新的訊息。

3. Form:問題輸入區

接著是:

<form id="chatForm" class="chat-input-area">

裡面包含:

<input>

以及:

<button>

使用者可以在輸入框輸入問題,再按下送出。

這裡使用 <form>,也可以讓使用者直接按 Enter 送出問題。

4. Footer:健康資訊提醒

最後是:

<footer class="chat-footer">

顯示:

本系統僅提供一般健康資訊,
不取代專業醫療診斷或治療。

因為今天的專案涉及健康資訊,所以我希望使用者能清楚知道:

AI 可以協助整理一般健康知識,但不能取代醫療專業人員。


Part 2|使用 CSS 美化聊天畫面

六、建立 style.css

接下來新增:

style.css

這次想讓畫面看起來簡潔一點,所以選擇白色搭配淺綠色,呈現比較清爽的健康主題。

完整程式如下:

* {
    box-sizing: border-box;
}

body {
    margin: 0;
    padding: 20px;

    font-family:
        Arial,
        "Noto Sans TC",
        sans-serif;

    background-color: #f1f7f5;

    display: flex;
    justify-content: center;
    align-items: center;

    min-height: 100vh;
}

.chat-container {
    width: 100%;
    max-width: 800px;
    height: min(85vh, 750px);

    background-color: #ffffff;

    border-radius: 18px;

    box-shadow:
        0 8px 30px rgba(0, 0, 0, 0.08);

    display: flex;
    flex-direction: column;

    overflow: hidden;
}

.chat-header {
    padding: 20px 24px;

    background-color: #4a9b84;
    color: white;

    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 12px;
}

.chat-header h1 {
    margin: 0;
    font-size: 24px;
}

.chat-header p {
    margin: 5px 0 0;
    font-size: 13px;
    opacity: 0.9;
}

.new-chat-button {
    padding: 9px 14px;

    border: 1px solid white;
    border-radius: 8px;

    background-color: transparent;
    color: white;

    cursor: pointer;
}

.new-chat-button:hover {
    background-color: rgba(255, 255, 255, 0.15);
}

.chat-messages {
    flex: 1;

    padding: 24px;

    overflow-y: auto;

    display: flex;
    flex-direction: column;
    gap: 16px;
}

.message {
    display: flex;
    width: 100%;
}

.message.user {
    justify-content: flex-end;
}

.message.assistant {
    justify-content: flex-start;
}

.message-content {
    max-width: 75%;

    padding: 12px 16px;

    border-radius: 14px;

    line-height: 1.7;
    font-size: 15px;

    white-space: pre-wrap;
    overflow-wrap: anywhere;
}

.message.user .message-content {
    background-color: #d9f0e8;
    color: #244b3f;

    border-bottom-right-radius: 4px;
}

.message.assistant .message-content {
    background-color: #f1f3f5;
    color: #333333;

    border-bottom-left-radius: 4px;
}

.status-message {
    min-height: 24px;

    padding: 0 24px;

    font-size: 13px;
    color: #777777;
}

.chat-input-area {
    display: flex;
    gap: 10px;

    padding: 16px 20px;

    border-top: 1px solid #eeeeee;
}

.chat-input-area input {
    flex: 1;
    min-width: 0;

    padding: 12px 14px;

    border: 1px solid #dddddd;
    border-radius: 10px;

    font-size: 15px;
    outline: none;
}

.chat-input-area input:focus {
    border-color: #4a9b84;
}

.chat-input-area button {
    padding: 12px 20px;

    border: none;
    border-radius: 10px;

    background-color: #4a9b84;
    color: white;

    font-size: 15px;
    cursor: pointer;
}

.chat-input-area button:hover {
    background-color: #397c69;
}

.chat-input-area button:disabled,
.new-chat-button:disabled {
    opacity: 0.55;
    cursor: not-allowed;
}

.chat-footer {
    padding: 10px 20px 16px;

    text-align: center;

    color: #888888;
    font-size: 12px;
}

@media (max-width: 600px) {

    body {
        padding: 0;
    }

    .chat-container {
        height: 100dvh;
        max-width: none;
        border-radius: 0;
    }

    .chat-header {
        padding: 16px;
    }

    .chat-header h1 {
        font-size: 20px;
    }

    .chat-messages {
        padding: 16px;
    }

    .message-content {
        max-width: 88%;
    }

    .chat-input-area {
        padding: 12px;
    }

    .chat-input-area button {
        padding: 12px 14px;
    }
}

這裡我們使用了幾個 CSS 概念。


七、認識 Flexbox

首先看到:

display: flex;

這是 CSS 的 Flexbox 排版方式。

今天我們主要用它來安排聊天畫面。

例如:

.chat-container {
    display: flex;
    flex-direction: column;
}

代表聊天介面會由上到下排列。

Header
   ↓
Messages
   ↓
Status
   ↓
Input
   ↓
Footer

另外:

.message.user {
    justify-content: flex-end;
}

代表使用者訊息靠右。

而:

.message.assistant {
    justify-content: flex-start;
}

代表 AI 訊息靠左。

這樣就能做出常見的聊天介面效果。


八、讓聊天訊息可以捲動

接下來看到:

.chat-messages {
    flex: 1;
    overflow-y: auto;
}

其中:

flex: 1;

讓聊天區域使用剩餘空間。

而:

overflow-y: auto;

代表當聊天訊息太多時,可以在這個區域上下捲動。

這樣就不會因為對話越來越長,導致整個網頁一直往下延伸。


九、讓手機也可以使用

今天還加入:

@media (max-width: 600px)

這是 CSS 的 Media Query。

可以根據螢幕大小調整版面。

例如:

.chat-container {
    height: 100dvh;
    max-width: none;
    border-radius: 0;
}

當使用者使用手機時,聊天畫面就可以更接近全螢幕。

雖然今天只是簡易版,但至少不會只能在電腦上使用。


Part 3|使用 JavaScript 串接 Spring Boot

十、建立 app.js

接下來是今天最重要的部分!

我們已經有 HTML 畫面,也完成 CSS 樣式。

但目前按下「送出」還不會發生任何事情。

所以接下來要使用 JavaScript,讓網頁可以和 Spring Boot API 溝通。

新增:

app.js

完整程式如下:

const API_URL = "/health/ai/conversation";

const chatForm = document.getElementById("chatForm");
const questionInput = document.getElementById("questionInput");
const sendButton = document.getElementById("sendButton");
const newChatButton = document.getElementById("newChatButton");
const chatMessages = document.getElementById("chatMessages");
const statusMessage = document.getElementById("statusMessage");

let conversationId = null;
let isLoading = false;

function addMessage(role, content) {

    const messageDiv = document.createElement("div");
    messageDiv.classList.add("message", role);

    const contentDiv = document.createElement("div");
    contentDiv.classList.add("message-content");

    contentDiv.textContent = content;

    messageDiv.appendChild(contentDiv);
    chatMessages.appendChild(messageDiv);

    chatMessages.scrollTop = chatMessages.scrollHeight;
}

function setLoading(loading) {

    isLoading = loading;

    sendButton.disabled = loading;
    questionInput.disabled = loading;
    newChatButton.disabled = loading;

    statusMessage.textContent = loading
        ? "AI 正在思考中……"
        : "";
}

async function sendQuestion(question) {

    if (isLoading) {
        return;
    }

    setLoading(true);

    addMessage("user", question);

    const requestBody = {
        question: question
    };

    if (conversationId !== null) {
        requestBody.conversationId = conversationId;
    }

    try {

        const response = await fetch(API_URL, {

            method: "POST",

            headers: {
                "Content-Type": "application/json"
            },

            body: JSON.stringify(requestBody)
        });

        if (!response.ok) {
            throw new Error(
                "API 請求失敗,HTTP Status:" + response.status
            );
        }

        const data = await response.json();

        if (!data.conversationId || !data.answer) {
            throw new Error("API 回傳資料格式不正確");
        }

        conversationId = data.conversationId;

        addMessage("assistant", data.answer);

    } catch (error) {

        console.error("Chat request failed:", error);

        addMessage(
            "assistant",
            "抱歉,目前無法取得回答,請稍後再試。"
        );

    } finally {

        setLoading(false);
        questionInput.focus();
    }
}

chatForm.addEventListener("submit", function (event) {

    event.preventDefault();

    if (isLoading) {
        return;
    }

    const question = questionInput.value.trim();

    if (question === "") {
        return;
    }

    if (question.length > 500) {
        alert("問題不能超過 500 個字元。");
        return;
    }

    questionInput.value = "";

    sendQuestion(question);
});

newChatButton.addEventListener("click", function () {

    if (isLoading) {
        return;
    }

    conversationId = null;

    chatMessages.replaceChildren();

    addMessage(
        "assistant",
        "已開始新對話!你可以輸入新的健康相關問題。"
    );

    questionInput.focus();
});

這樣就完成今天最核心的 JavaScript 程式。

接下來我們把它拆開來看。


十一、使用 getElementById 取得 HTML 元素

首先看到:

const chatForm = document.getElementById("chatForm");

這裡使用:

document.getElementById()

取得 HTML 中指定 ID 的元素。

例如:

<form id="chatForm">

JavaScript 就可以透過:

document.getElementById("chatForm")

找到它。

其他像是:

const questionInput =
    document.getElementById("questionInput");

就是取得輸入框。

而:

const chatMessages =
    document.getElementById("chatMessages");

則是取得聊天訊息區域。

這樣 JavaScript 才能讀取使用者輸入,以及修改網頁內容。


十二、使用 addMessage 顯示聊天訊息

接下來看到:

function addMessage(role, content)

這個 Function 負責將訊息顯示在畫面上。

例如:

addMessage("user", "什麼是 BMI?");

就會新增一則使用者訊息。

而:

addMessage("assistant", "BMI 是身體質量指數。");

則會新增一則 AI 訊息。

其中:

document.createElement("div")

代表建立新的 HTML 元素。

接著:

messageDiv.classList.add("message", role);

替元素加入 CSS Class。

例如:

message user

或:

message assistant

這樣 CSS 就能決定訊息應該靠左還是靠右。

最後:

chatMessages.appendChild(messageDiv);

把訊息加入聊天區域。


十三、為什麼使用 textContent?

今天有一個值得注意的地方。

我們使用:

contentDiv.textContent = content;

而不是:

contentDiv.innerHTML = content;

為什麼?

因為使用者輸入和 AI 回覆,都不應該直接被當成 HTML 執行。

例如,假設有人輸入:

<script>alert("Hello")</script>

如果不小心把這段文字當成 HTML 插入,就可能產生安全問題。

使用:

textContent

可以把內容當成一般文字顯示。

所以今天不需要讓 AI 回覆包含可執行的 HTML。

這是建立聊天介面時很重要的安全習慣。


十四、認識 JavaScript 的 fetch()

接下來是今天最重要的概念:

fetch()

它可以讓瀏覽器發送 HTTP Request。

例如:

const response = await fetch(API_URL, {

    method: "POST",

    headers: {
        "Content-Type": "application/json"
    },

    body: JSON.stringify(requestBody)
});

這段程式會向 Spring Boot 發送:

POST /health/ai/conversation

並傳送 JSON。

例如:

{
  "question": "什麼是 BMI?"
}

如果是第二輪對話,就會包含:

{
  "conversationId": "550e8400-e29b-41d4-a716-446655440000",
  "question": "那它怎麼計算?"
}

這和我們之前在 Postman 裡面做的事情其實一樣。

只是現在改成由 JavaScript 自動完成。


十五、認識 JSON.stringify()

在發送 Request 時,我們使用:

JSON.stringify(requestBody)

因為 JavaScript 裡面的物件:

const requestBody = {
    question: "什麼是 BMI?"
};

不是直接要傳送的 JSON 字串。

所以需要透過:

JSON.stringify()

轉換。

轉換後就會變成:

{
  "question": "什麼是 BMI?"
}

這樣 Spring Boot 才能透過:

@RequestBody

接收資料。

也就是:

JavaScript Object
       ↓
JSON.stringify()
       ↓
JSON Request Body
       ↓
Spring Boot @RequestBody

十六、認識 async 與 await

今天另一個新的語法是:

async function sendQuestion(question)

以及:

await fetch(...)

因為呼叫 AI API 需要時間。

有時候可能只要一兩秒,有時候可能需要更久。

所以 JavaScript 不能假設 Request 發送後,AI 就會立刻回覆。

我們使用:

async

宣告非同步 Function。

再使用:

await

等待 Promise 完成。

例如:

const response = await fetch(API_URL);

這裡會等待 HTTP Response。

接著:

const data = await response.json();

再將 Response Body 解析成 JavaScript 物件。

這樣就可以取得:

data.answer

以及:

data.conversationId

最後更新畫面。


十七、讓 AI 可以連續聊天

還記得 Day 25 建立的多輪對話功能嗎?

當時我們使用:

conversationId

區分不同對話。

今天在 JavaScript 裡面也需要保存這個 ID。

首先宣告:

let conversationId = null;

當使用者第一次發送問題時:

const requestBody = {
    question: question
};

因為:

conversationId === null

所以不會附上對話 ID。

Spring Boot 就會建立新對話。

例如回傳:

{
  "conversationId": "550e8400-e29b-41d4-a716-446655440000",
  "answer": "BMI 是身體質量指數。"
}

接著 JavaScript 會執行:

conversationId = data.conversationId;

將 ID 保存起來。

當使用者再次提問:

if (conversationId !== null) {
    requestBody.conversationId = conversationId;
}

就會把同一個 ID 傳給後端。

這樣 AI 就可以取得之前的歷史訊息,繼續對話。

整個流程如下:

第一次提問
    ↓
conversationId = null
    ↓
Spring Boot 建立新對話
    ↓
回傳 conversationId
    ↓
JavaScript 保存 ID
    ↓
第二次提問
    ↓
帶上相同 conversationId
    ↓
Spring Boot 查詢歷史對話
    ↓
AI 根據上下文回答

這樣就把之前建立的多輪對話功能,真正整合到網頁上了!


十八、加入 AI 思考中的提示

使用 AI 時,有一個常見的問題。

就是使用者按下送出後,可能需要等待幾秒鐘。

如果畫面完全沒有反應,使用者可能會以為按鈕沒有作用,然後一直重複點擊。

所以今天加入:

function setLoading(loading)

當 API 正在處理時:

setLoading(true);

畫面就會顯示:

AI 正在思考中……

同時停用:

sendButton.disabled = loading;

以及:

questionInput.disabled = loading;

避免使用者在上一個問題還沒完成時,又發送下一個問題。

當 API 回覆後:

setLoading(false);

就會重新啟用輸入框和按鈕。

這樣使用者就能清楚知道系統目前的狀態。


十九、加入錯誤處理

今天我們也使用:

try
catch

以及:

finally

處理 API 請求。

例如:

try {

    // 呼叫 API

} catch (error) {

    // 發生錯誤

} finally {

    // 不論成功或失敗都會執行
}

如果 Spring Boot 沒有啟動,或是 AI API 發生錯誤,就會進入:

catch

並顯示:

抱歉,目前無法取得回答,請稍後再試。

這樣使用者就不會只看到空白畫面。

另外要注意,今天的簡易版本如果 API 請求失敗,畫面上的問題仍然會保留,但後端不一定已經保存這一輪對話。

因此這個畫面比較適合作為本機功能展示。

未來如果要做成正式聊天系統,還可以加入重試機制與訊息狀態管理。


Part 4|加入新對話功能

二十、讓使用者可以重新開始聊天

目前我們已經可以進行多輪對話。

但如果使用者想開始另一個主題呢?

例如:

對話 A:BMI

結束後想開始:

對話 B:睡眠習慣

就需要建立新的對話。

所以今天在 HTML 裡面加入:

<button id="newChatButton">
    新對話
</button>

再透過 JavaScript:

newChatButton.addEventListener("click", function () {

    if (isLoading) {
        return;
    }

    conversationId = null;

    chatMessages.replaceChildren();

    addMessage(
        "assistant",
        "已開始新對話!你可以輸入新的健康相關問題。"
    );

    questionInput.focus();
});

這裡最重要的是:

conversationId = null;

代表下一次送出問題時,不會附上原本的對話 ID。

Spring Boot 就會建立新的對話。

另外:

chatMessages.replaceChildren();

會清空目前畫面上的聊天訊息。

但要注意:

這個按鈕不會刪除資料庫中的舊對話。

它只是讓目前網頁開始新的聊天。

如果真的想刪除之前的對話,還是需要使用 Day 27 建立的 DELETE API。


Part 5|啟動 Spring Boot 測試網頁

二十一、啟動專案

接下來確認三個檔案都已經建立:

static
├── index.html
├── style.css
└── app.js

然後重新啟動 Spring Boot。

如果使用 Maven,可以在專案目錄執行:

./mvnw spring-boot:run

Windows 可以使用:

.\mvnw.cmd spring-boot:run

或是直接透過 IDE 執行:

HealthAssistantApplication

記得確認之前設定的 OpenAI API Key 仍然有效。

因為今天的聊天功能會實際呼叫外部 AI API,可能產生 API 使用費用。


二十二、打開瀏覽器

啟動成功後,打開:

http://localhost:8080/

如果檔案位置正確,Spring Boot 就會提供:

index.html

畫面應該可以看到:

智慧健康助理
AI Health Assistant

以及聊天訊息區和問題輸入框。

如果網頁沒有正常顯示,可以先確認:

src/main/resources/static/index.html

是否放在正確位置。

另外,今天我們把網頁和 API 都放在同一個 Spring Boot 專案。

因此網頁可以直接呼叫:

fetch("/health/ai/conversation")

不需要另外設定跨來源請求。

如果未來將前端部署到不同網域,才需要進一步處理 CORS 等設定。


二十三、測試第一輪聊天

接下來在輸入框輸入:

什麼是 BMI?

按下:

送出

畫面會先顯示:

使用者:什麼是 BMI?

接著:

AI 正在思考中……

如果後端成功呼叫 AI API,預期會看到類似:

AI:BMI 是身體質量指數,
通常用於成人體位的初步評估。

實際回答內容會依照模型產生的文字而不同。

這樣就代表:

HTML → JavaScript → Spring Boot → AI API

整個流程已經成功串接!


二十四、測試第二輪聊天

接著不要按新對話。

直接輸入:

那它怎麼計算?

如果 JavaScript 已經保存前一輪回傳的:

conversationId

就會將相同的 ID 傳給 Spring Boot。

後端便可以從 H2 Database 查詢歷史對話。

預期 AI 可能回答:

AI:BMI 的計算方式是體重(公斤)
除以身高(公尺)的平方。

這樣就代表網頁已經支援多輪對話。

而且這次不需要手動複製 UUID,也不需要開啟 Postman。

所有操作都可以直接在瀏覽器完成!


二十五、測試新對話按鈕

接下來點擊:

新對話

畫面上的聊天內容會被清空。

接著顯示:

已開始新對話!
你可以輸入新的健康相關問題。

再輸入:

為什麼規律作息很重要?

因為:

conversationId = null;

所以 Spring Boot 會建立另一段新對話。

這樣就可以將不同主題分開。

例如:

Conversation A:BMI
Conversation B:睡眠

而且兩段對話都會保存在 H2 Database 中。


Part 6|驗證聊天紀錄是否真的保存

二十六、使用 Day 27 的 API 查詢對話

今天我們雖然建立了網頁介面,但資料庫功能仍然沿用之前的設計。

所以可以透過 Day 27 的 API 驗證。

打開 Postman:

GET http://localhost:8080/health/ai/conversations

應該可以看到剛剛建立的對話。

例如:

[
  {
    "conversationId": "550e8400-e29b-41d4-a716-446655440000",
    "createdAt": "2026-10-17T14:00:00",
    "messageCount": 4
  },
  {
    "conversationId": "9c1e4b20-2f7a-4c88-a2d3-123456789abc",
    "createdAt": "2026-10-17T14:05:00",
    "messageCount": 2
  }
]

接著使用:

GET /health/ai/conversations/{id}

也可以查看完整聊天紀錄。

這代表我們今天的前端雖然是新建立的,但後端仍然使用前面完成的 API 和資料庫。

這也是我覺得分層開發很方便的地方。

因為我們不用重新寫一次 AI 功能,只需要建立新的操作介面。


二十七、重新整理網頁會發生什麼事?

今天還有一個值得注意的問題。

我們使用:

let conversationId = null;

保存目前的對話 ID。

但這個變數只存在於目前載入的網頁中。

所以如果使用者重新整理瀏覽器:

重新整理網頁
       ↓
JavaScript 重新執行
       ↓
conversationId = null

就會失去目前對話的 ID。

雖然 H2 Database 裡面的聊天紀錄仍然存在,但今天的網頁還沒有提供恢復舊對話的功能。

這也是目前這個版本的限制。

未來可以考慮使用:

sessionStorage

暫時保存對話 ID。

或是使用 Day 27 建立的:

GET /health/ai/conversations

讓使用者選擇想繼續的對話。

不過要注意,瀏覽器儲存對話 ID 並不等於完成身分驗證。

正式系統仍然需要登入機制與對話存取權限。

今天先以完成基本聊天畫面為主。


Part 7|今天的安全與設計注意事項

二十八、不要把 OpenAI API Key 放進 JavaScript

這點非常重要!

今天我們的 JavaScript 只會呼叫:

fetch("/health/ai/conversation")

也就是我們自己的 Spring Boot API。

它不會直接呼叫:

https://api.openai.com/v1/responses

原因是:

OpenAI API Key 不應該直接放在前端程式碼裡。

例如,千萬不要在 app.js 寫:

const apiKey = "sk-你的金鑰";

因為前端 JavaScript 會被下載到使用者的瀏覽器。

任何人都可能透過瀏覽器開發者工具查看程式碼。

如果 API Key 放在裡面,就可能外洩。

所以今天採用:

瀏覽器
   ↓
Spring Boot
   ↓
OpenAI API

讓 API Key 留在後端。

這樣比較符合基本的安全設計。


二十九、今天的網頁還有哪些限制?

雖然今天已經可以在瀏覽器聊天,但它仍然是教學用的簡易版本。

1. 尚未加入登入功能

目前只要可以連到這個 Spring Boot 服務,就可能呼叫聊天 API。

因此不適合直接公開部署。

正式系統需要加入使用者驗證、存取權限和請求限制。

2. 尚未提供歷史對話列表

雖然 Day 27 已經建立查詢 API,但今天的網頁還沒有將它做成可點選的列表。

所以目前只能繼續當前網頁中的對話。

3. 尚未加入 Markdown 顯示

目前 AI 回答使用:

textContent

顯示成純文字。

如果 AI 回覆包含:

**粗體**

畫面就會直接顯示星號。

未來如果要支援 Markdown,需要使用適當的解析與清理機制,避免 XSS 問題。

4. 尚未加入完整的訊息重試功能

目前如果 API 發生錯誤,網頁會顯示錯誤提示。

但還沒有提供單則訊息重試按鈕。

5. 尚未加入對話長度與費用管理

雖然後端已經限制歷史訊息數量,但每次 AI 呼叫仍然可能產生費用。

正式系統需要進一步限制請求頻率與使用量。

6. 健康資訊仍然需要注意隱私

因為使用者可能輸入個人健康狀況,所以未來如果公開使用,還需要加入隱私告知、資料保護,以及對話刪除等機制。


三十、目前的專案結構

今天新增了三個前端檔案:

index.html
style.css
app.js

目前專案結構如下:

health-assistant
│
├── src/main/java/com/example/healthassistant
│   │
│   ├── HealthAssistantApplication.java
│   │
│   ├── controller
│   │   ├── HealthController.java
│   │   ├── WaterController.java
│   │   ├── SleepController.java
│   │   ├── HealthSummaryController.java
│   │   └── AIController.java
│   │
│   ├── service
│   │   ├── BmiService.java
│   │   ├── WaterService.java
│   │   ├── SleepService.java
│   │   ├── HealthSummaryService.java
│   │   ├── AIService.java
│   │   ├── AIHealthSummaryService.java
│   │   ├── AIChatService.java
│   │   ├── AIConversationService.java
│   │   ├── ConversationPersistenceService.java
│   │   └── ConversationManagementService.java
│   │
│   ├── repository
│   │   ├── WaterRepository.java
│   │   ├── SleepRepository.java
│   │   └── ConversationRepository.java
│   │
│   ├── dto
│   │   ├── ConversationRequest.java
│   │   ├── ConversationResponse.java
│   │   ├── ConversationMessage.java
│   │   ├── ConversationListResponse.java
│   │   └── ConversationDetailResponse.java
│   │
│   └── exception
│       ├── GlobalExceptionHandler.java
│       └── ConversationNotFoundException.java
│
├── src/main/resources
│   │
│   ├── application.properties
│   ├── schema.sql
│   │
│   └── static
│       ├── index.html
│       ├── style.css
│       └── app.js
│
└── pom.xml

這裡只列出部分 DTO,其他之前建立的健康紀錄 DTO 仍然保留。

現在我們的智慧健康助理不再只是後端 API,而是終於有了可以操作的網頁!


Day 28 小結

今天成功使用 HTML、CSS 和 JavaScript,替智慧健康助理建立了第一個聊天介面!

原本每次都需要打開 Postman,手動輸入 JSON,再查看 Response。

現在使用者只要開啟瀏覽器:

http://localhost:8080/

就可以直接和 AI 健康助理聊天。

今天完成的功能包括:

  1. 認識前端與後端的分工
  2. 使用 Spring Boot 提供靜態網頁
  3. 建立 HTML 聊天畫面
  4. 使用 CSS 設計聊天介面
  5. 使用 Flexbox 排列訊息
  6. 使用 JavaScript 操作 HTML 元素
  7. 使用 fetch() 呼叫 Spring Boot API
  8. 使用 JSON.stringify() 傳送 JSON
  9. 使用 async 和 await 處理非同步請求
  10. 顯示使用者問題與 AI 回答
  11. 保存 conversationId 支援多輪對話
  12. 加入新對話按鈕
  13. 加入等待提示與基本錯誤處理
  14. 使用瀏覽器測試完整聊天流程

目前我們的智慧健康助理已經具備:

智慧健康助理
│
├── 健康紀錄功能
│   ├── BMI 計算與分類
│   ├── 飲水紀錄
│   ├── 睡眠紀錄
│   └── 每日健康摘要
│
├── AI 功能
│   ├── AI 每日健康摘要
│   ├── AI 健康問答
│   ├── AI 多輪對話
│   └── AI 聊天紀錄管理
│
├── 資料庫
│   ├── 飲水紀錄
│   ├── 睡眠紀錄
│   └── AI 對話紀錄
│
└── 網頁介面
    ├── 聊天訊息顯示
    ├── 問題輸入
    ├── AI 回答
    ├── 多輪對話
    └── 開始新對話

今天最大的收穫是:

後端 API 負責提供功能,而前端介面則讓這些功能真正變成使用者可以操作的服務。

以前在 Postman 裡面看到 JSON 回傳時,雖然知道程式成功了,但總覺得它還不像一個完整的應用程式。

直到今天把 HTML、CSS 和 JavaScript 串起來,才終於有一種:

「啊!我真的做出一個可以聊天的 AI 健康助理了!」的感覺 XD

雖然目前還是很基本的版本,但已經完成從前端到後端、資料庫,再到外部 AI API 的整個流程。


明日預告

Day 29|讓健康助理更完整!整合健康摘要、聊天紀錄與網頁功能

今天我們已經建立基本聊天介面。

不過目前的網頁只能聊天,還沒有把之前完成的健康紀錄功能整合進來。

例如:

今日飲水量
今日睡眠時數

以及:

AI 每日健康摘要

另外,Day 27 建立的歷史對話查詢 API,也還沒有真正放進網頁。

所以下一篇準備挑戰:

將健康資料與 AI 聊天功能整合到同一個網頁!

預計會完成:

  • 在網頁顯示每日健康摘要
  • 使用 JavaScript 查詢飲水與睡眠紀錄
  • 加入 AI 健康摘要按鈕
  • 顯示歷史對話列表
  • 點選並恢復之前的聊天紀錄
  • 整理網頁的操作流程
  • 測試完整的智慧健康助理功能

讓我們的專案不只是 AI 聊天機器人,而是更接近一個真正的智慧健康助理!

最後兩天,繼續加油!


上一篇
用 Spring Boot 建立 AI 對話查詢與刪除 API
下一篇
打造智慧健康助理完整網頁
系列文
Java × AI:30天打造智慧健康助理 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言