使用SpringBoot與React串接Gemini

Google AI API

先去這個網址取得API !

https://aistudio.google.com

Gemini 官方結構

Gemini API request 格式:

{
  "contents": [
    {
      "parts": [
        { "text": "Hello" }
      ]
    }
  ]
}

所以我們要將使用者的輸入,加上JSON Body和Header包成完整的REST API格式傳給gimini,作為 HTTP POST 的請求物件

SpringBoot

先建立一個檔案存放API金鑰 src\main\resources\application.properties

google.gemini.api.key=AIzaSyXXXXXXX...

接著建立一個service,主要的操作都在service上 !

操作流程:

  1. 程從設定檔讀取 Gemini API Key (@Value)
  2. 將使用者輸入的 prompt 組成 Gemini API 要求格式
  3. 透過 RestTemplate 呼叫 Google Gemini
  4. 解析回傳 JSON
  5. 回傳 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();
        }
    }
}

React

前端要負責的事就比較簡單:

  1. 管理對話狀態( useState)
  2. 接收使用者輸入並送出至後端 ( fetch )
  3. 接收 AI 回應並更新對話紀錄
  4. 控制送出行為( useEffect )
  5. 處理非同步流程與錯誤狀態 (async / await )
  6. 維持對話可用性(自動捲動 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>
  );
}

Previous

Leave a Reply

Your email address will not be published. Required fields are marked *