串接Gemini
使用Server Action
安裝最新的官方 Gemini SDK:
npm install @google/genaiCode language: CSS (css)
在專案根目錄的 .env 檔案中,加入 Gemini API Key(至 Google AI Studio 申請):
GEMINI_API_KEY=your_actual_api_key_here
可使用的模型及接法可參考官方文件:https://ai.google.dev/gemini-api/docs/models?hl=zh-tw
server action
"use server";
import { GoogleGenAI } from "@google/genai";
// 初始化 Google Gen AI 用戶端(會自動讀取 process.env.GEMINI_API_KEY)
const ai = new GoogleGenAI();
interface ChatMessage {
role: "user" | "model";
text: string;
}
/**
* 處理與 Gemini 對話的 Server Action
* @param userInput 使用者最新輸入的文字
* @param history 過去的對話歷史紀錄,格式必須符合 Gemini 要求
*/
export async function chatWithGemini(userInput: string, history: ChatMessage[] = []) {
if (!userInput.trim()) {
return { success: false, error: "輸入內容不能為空" };
}
try {
// 1. 將前端傳入的歷史紀錄轉換為 @google/genai SDK 所需的 contents 格式
const contents = history.map((msg) => ({
role: msg.role,
parts: [{ text: msg.text }],
}));
// 2. 將當前使用者的新訊息加入對話內容中
contents.push({
role: "user",
parts: [{ text: userInput }],
});
// 3. 呼叫 Gemini API 產生回應
const response = await ai.models.generateContent({
model: "gemini-2.5-flash",
contents: contents,
// 這裡可以選擇性加入 systemInstruction 來規範 AI 的角色設定
config: {
systemInstruction: "你是一位專業且親切的醫療系統助理,請用繁體中文回答。",
temperature: 0.7,
},
});
// 4. 取得 AI 回傳的純文字
const replyText = response.text || "抱歉,我暫時無法回應。";
return {
success: true,
reply: replyText,
};
} catch (error) {
console.error("Gemini API 呼叫失敗:", error);
return {
success: false,
error: error instanceof Error ? error.message : "連線至 AI 伺服器時發生未知錯誤",
};
}
}Code language: JavaScript (javascript)
使用SpringBoot與React
Google AI API
先去這個網址取得API !
Gemini 官方結構
Gemini API request 格式:
{
"contents": [
{
"parts": [
{ "text": "Hello" }
]
}
]
}
Code language: JSON / JSON with Comments (json)
所以我們要將使用者的輸入,加上JSON Body和Header包成完整的REST API格式傳給gimini,作為 HTTP POST 的請求物件
SpringBoot
先建立一個檔案存放API金鑰 src\main\resources\application.properties
google.gemini.api.key=AIzaSyXXXXXXX...
接著建立一個service,主要的操作都在service上 !
操作流程:
- 程從設定檔讀取 Gemini API Key (@Value)
- 將使用者輸入的
prompt組成 Gemini API 要求格式 - 透過
RestTemplate呼叫 Google Gemini - 解析回傳 JSON
- 回傳 AI 產生的文字結果(String)
package demo.service;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.http.*;
import java.util.*;
@Service
public class AIService {
@Value("${google.gemini.api.key}")
private String apiKey;
private final String API_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-3-flash-preview:generateContent?key=";
public String getAiResponse(String prompt) {
RestTemplate restTemplate = new RestTemplate();
// 1. 設定 Header
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
// 2. 構建請求 Body
Map<String, Object> textPart = Map.of("text", prompt);
Map<String, Object> parts = Map.of("parts", List.of(textPart));
Map<String, Object> contents = Map.of("contents", List.of(parts));
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(contents, headers);
try {
// 3. 發送請求
ResponseEntity<Map> response = restTemplate.postForEntity(API_URL + apiKey, entity, Map.class); // 呼叫 Gemini API
// 4. 解析 Gemini 的 JSON (路徑: candidates[0].content.parts[0].text)
Map body = response.getBody();
if (body != null && body.containsKey("candidates")) {
List candidates = (List) body.get("candidates");
Map firstCandidate = (Map) candidates.get(0);
Map content = (Map) firstCandidate.get("content");
List resParts = (List) content.get("parts");
Map firstPart = (Map) resParts.get(0);
return firstPart.get("text").toString();
}
return "AI 回傳格式異常";
} catch (Exception e) {
e.printStackTrace();
return "AI 服務呼叫失敗: " + e.getMessage();
}
}
}Code language: JavaScript (javascript)
React
前端要負責的事就比較簡單:
- 管理對話狀態( useState)
- 接收使用者輸入並送出至後端 ( fetch )
- 接收 AI 回應並更新對話紀錄
- 控制送出行為( useEffect )
- 處理非同步流程與錯誤狀態 (async / await )
- 維持對話可用性(自動捲動 useEffect)
import React, { useState, useRef, useEffect } from 'react';
import { FaAngleRight, FaPaperPlane } from 'react-icons/fa';
interface Message {
role: 'user' | 'ai';
text: string;
}
interface Props {
onClose: () => void;
medicalData: any;
}
export default function RightPanelContent({
onClose,
medicalData
}: Props) {
const [messages, setMessages] = useState<Message[]>([
{
role: 'ai',
text: '你好!我是您的醫療 AI 助手,已準備好分析病歷資料。請問有什麼我可以幫您的?'
}
]);
const [inputValue, setInputValue] = useState('');
const [loading, setLoading] = useState(false);
const scrollRef = useRef<HTMLDivElement>(null);
// 自動捲動到最底部
useEffect(() => {
if (scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
}, [messages, loading]);
const handleSendMessage = async () => {
if (!inputValue.trim() || loading) return;
const userMessage = inputValue;
setMessages(prev => [...prev, { role: 'user', text: userMessage }]);
setInputValue('');
setLoading(true);
try { // inlineFetch的寫法
const response = await fetch('http://localhost:8080/api/ai/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: userMessage,
context: JSON.stringify(medicalData)
})
});
const result = await response.json();
setMessages(prev => [...prev, { role: 'ai', text: result.reply }]);
} catch (error) {
setMessages(prev => [
...prev,
{ role: 'ai', text: '連線失敗,請檢查後端服務。' }
]);
} finally {
setLoading(false);
}
};
return (
<div>
{/* Header */}
<div>
<span>AI 智能對話助手</span>
<button onClick={onClose}>
<FaAngleRight />
</button>
</div>
{/* 對話區 */}
<div ref={scrollRef}>
{messages.map((msg, index) => (
<div key={index}>
<strong>{msg.role === 'user' ? 'User' : 'AI'}:</strong>
<span>{msg.text}</span>
</div>
))}
{loading && <div>AI 正在思考中...</div>}
</div>
{/* 輸入區 */}
<div>
<input
type="text"
placeholder="請輸入問題..."
value={inputValue}
onChange={e => setInputValue(e.target.value)}
onKeyDown={e => e.key === 'Enter' && handleSendMessage()}
/>
<button onClick={handleSendMessage}>
<FaPaperPlane />
</button>
</div>
</div>
);
}
Code language: JavaScript (javascript) 