調派 API 教學整合說明

調派 API 教學整合說明

1. API 基本概念

  • API Key
    用來驗證使用者身分。在程式中宣告了 API_KEY,並在發送請求時於 header 中帶入:


const API_KEY = 'yourgsk_ufAUCEISsIiExQCEVACkWGdyb3FY49moAhuboAAtAPdCDYebcefm';


Endpoint URL
本範例使用的 API 端點為
https://api.groq.com/openai/v1/chat/completions
代表呼叫 ChatGPT 服務的「對話完成」功能。

請求方式與 Header
採用 fetch 並使用 POST 方法,並設定 Content-Typeapplication/json


2. 建立 API 請求

當使用者在輸入框中輸入訊息並按下送出按鈕後,會呼叫 sendMessage() 函式,其重點程式碼如下:


const response = await fetch('https://api.groq.com/openai/v1/chat/completions', {

  method: 'POST',

  headers: {

    'Authorization': `Bearer ${API_KEY}`,

    'Content-Type': 'application/json'

  },

  body: JSON.stringify({

    model: modelSelect.value,     // 模型選擇(例如 deepseek-r1-distill-llama-70b)

    messages: currentChat.messages, // 包含系統、使用者及之前對話的訊息

    temperature: 0.7,             // 控制回應的隨機性

    max_tokens: 2048              // 最大回傳 token 數量

  })

});



說明:

  • JSON Payload
    將目前聊天記錄(currentChat.messages)以陣列方式傳送,每個訊息包含 role(角色,例:user、assistant)與 content(內容)。

  • temperature 與 max_tokens
    分別用來調整生成回應的隨機性與限制回應長度。

  • Authorization
    使用 Bearer Token 驗證請求身分。


3. 處理 API 回應

在 API 回應成功後,程式會:

  • 檢查回應狀態(response.ok),若非 OK 則擲出錯誤。

  • 解析回應 JSON:


const data = await response.json();


  • 從回應中取出 AI 回應內容,並使用 removeThinks() 函式過濾掉 <think>...</think> 區塊(以避免內部調試訊息進入顯示):


const safeAssistantContent = removeThinks(data.choices[0].message.content);

currentChat.messages.push({ role: "assistant", content: safeAssistantContent });



  • 更新聊天畫面,呼叫 updateMessagesDisplay() 並將滾動位置移至底部。


4. 錯誤處理與 UI 互動

  • 錯誤處理
    若 API 呼叫失敗,會在 catch 區塊中捕捉錯誤並顯示錯誤訊息:


errorMessage.textContent = error.message || 'Sorry, an error occurred.';

errorMessage.style.display = 'block';




  • UI 互動
    在等待 API 回應期間,禁用使用者輸入區與送出按鈕,同時顯示「打字中」的動畫指示器,待回應處理完成後再解除禁用。


5. 補充:前端過濾 <think> 區塊

程式中使用了 removeThinks() 函式,該函式利用正則表達式移除文字中所有 <think>...</think> 區塊,確保最終送出與顯示的內容乾淨無雜訊。


function removeThinks(inputText) {

  return inputText.replace(/<think>[\s\S]*?<\/think>/gi, '');

}



6. 整體流程總結

  1. 使用者輸入訊息
    使用者在輸入框中輸入訊息並按下「送出」按鈕。

  2. 前端準備資料
    呼叫 removeThinks() 過濾訊息,並將該訊息與目前聊天記錄組成 JSON。

  3. 發送 API 請求
    使用 fetch 呼叫 API,帶入 API Key、模型選擇及參數。

  4. 接收並處理回應
    解析 API 回應,將 AI 回應加入聊天記錄並更新畫面。

  5. 錯誤處理與回饋
    若發生錯誤則顯示錯誤訊息;若成功則解除打字指示器,恢復使用者輸入。


完整範例程式碼

下面的 HTML 範例程式碼整合了上述說明,提供了一個包含調派 API、回應處理、錯誤處理、以及前端介面更新的完整範例。你可以直接將此程式碼部署到你的專案中,並依需求進行調整。


<!DOCTYPE html>

<html>

<head>

  <base href="/">

  <title>ChatGPT</title>

  <link href="https://chat.openai.com/favicon.ico" rel="icon" type="image/x-icon">

  <style>

    /* 基本重置與佈局 */

    * { box-sizing: border-box; margin: 0; padding: 0; }

    body {

      font-family: Söhne, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Ubuntu, Cantarell, "Noto Sans", sans-serif, "Helvetica Neue", Arial;

      line-height: 1.4;

      font-size: 16px;

      background: #343541;

      color: #ECECF1;

      display: flex;

    }

    /* 下方聊天與設定區 */

    .sidebar {

      width: 100%;

      height: 80px; /* 根據需求調整高度 */

      background: #202123;

      padding: 15px;

      display: flex;

      flex-direction: row;

      position: fixed;

      bottom: 0;

      left: 0;

    }

    .new-chat-btn, .settings-btn {

      margin-top: auto;

      width: 100%;

      padding: 12px;

      background: transparent;

      border: 1px solid #4A4B53;

      color: #ECECF1;

      border-radius: 5px;

      cursor: pointer;

      display: flex;

      align-items: center;

      gap: 10px;

      font-size: 14px;

    }

    .new-chat-btn:hover, .settings-btn:hover {

      background: #2D2E35;

      transform: translateY(-1px);

      box-shadow: 0 4px 12px rgba(0,0,0,0.1);

    }

    .new-chat-btn svg, .settings-btn svg { margin-right: 8px; }

    .chats-list { flex: 1; overflow-y: auto; }

    .chat-item {

      padding: 12px 14px;

      margin-bottom: 5px;

      border-radius: 12px;

      cursor: pointer;

      display: flex;

      justify-content: space-between;

      align-items: center;

      font-size: 14px;

      line-height: 1.3;

      transition: all 0.2s ease;

    }

    .chat-item:hover {

      background: #2D2E35;

      transform: translateY(-1px);

      box-shadow: 0 4px 12px rgba(0,0,0,0.1);

    }

    .chat-item.active { background: #343541; }

    .delete-chat {

      opacity: 0;

      color: #8E8EA0;

      cursor: pointer;

      padding: 5px;

    }

    .chat-item:hover .delete-chat { opacity: 1; }

    .main-content {

      flex: 1;

      height: calc(100vh - 80px); /* 80px 為下欄高度 */

      margin-bottom: 80px; /* 留出下欄空間 */

    }

    .chat-container {

      flex: 1;

      display: flex;

      flex-direction: column;

      max-width: 1000px;

      margin: 0 auto;

      width: 100%;

      padding: 20px;

      height: 100%;

      position: relative;

    }

    .messages {

      flex: 1;

      overflow-y: auto;

      padding: 20px;

      margin-bottom: 20px;

    }

    .message {

      margin-bottom: 24px;

      padding: 16px;

      line-height: 1.75;

      font-size: 16px;

    }

    .user-message {

      background: #343541;

      color: #ECECF1;

      width: 100%;

      padding: 24px;

      border-bottom: 1px solid rgba(32,33,35,0.5);

      border-radius: 16px;

      margin: 8px 0;

      box-shadow: 0 2px 8px rgba(0,0,0,0.1);

    }

    .assistant-message {

      background: #444654;

      color: #D1D5DB;

      width: 100%;

      padding: 24px;

      border-bottom: 1px solid rgba(32,33,35,0.5);

      border-radius: 16px;

      margin: 8px 0;

      box-shadow: 0 2px 8px rgba(0,0,0,0.1);

    }

    .input-container {

      position: fixed;

      top: 0;

      left: 0;

      right: 0;

      background: #343541;

      padding: 20px;

      display: flex;

      gap: 10px;

      max-width: 1000px;

      margin: 0 auto;

      border-bottom: 1px solid #4A4B53;

      z-index: 999;

    }

    textarea {

      flex: 1;

      padding: 12px;

      border: 1px solid #4A4B53;

      border-radius: 12px;

      resize: none;

      height: 50px;

      background: #40414F;

      color: #ECECF1;

      font-size: 16px;

      line-height: 1.5;

      transition: all 0.2s ease;

    }

    textarea::placeholder { color: #8E8EA0; }

    textarea:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0,0,0,0.1); }

    button {

      padding: 10px 20px;

      background: #19C37D;

      color: white;

      border: none;

      border-radius: 12px;

      cursor: pointer;

      transition: background 0.3s, all 0.2s ease;

    }

    button:hover { background: #1A8870; transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0,0,0,0.1); }

    button:disabled { background: #4A4B53; cursor: not-allowed; }

    .typing-indicator {

      display: none;

      padding: 10px;

      background: #444654;

      border-radius: 12px;

      margin-bottom: 10px;

    }

    .dot {

      display: inline-block;

      width: 8px;

      height: 8px;

      background: #8E8EA0;

      border-radius: 50%;

      margin-right: 3px;

      animation: bounce 1.4s infinite ease-in-out;

    }

    .dot:nth-child(2) { animation-delay: 0.2s; }

    .dot:nth-child(3) { animation-delay: 0.4s; }

    @keyframes bounce {

      0%, 80%, 100% { transform: translateY(0); }

      40% { transform: translateY(-10px); }

    }

    .model-selector {

      border-radius: 12px;

      margin: 12px;

      padding: 16px;

      margin-bottom: 10px;

      display: none;

    }

    select {

      padding: 8px;

      border-radius: 12px;

      border: 1px solid #4A4B53;

      margin-left: 10px;

      background: #40414F;

      color: #ECECF1;

      font-size: 14px;

      transition: all 0.2s ease;

    }

    select:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0,0,0,0.1); }

    select option { background: #40414F; color: #ECECF1; }

    .error-message {

      background: #442222;

      color: #FF4444;

      padding: 10px;

      border-radius: 12px;

      margin-bottom: 10px;

      display: none;

    }

    .settings-modal {

      display: none;

      position: fixed;

      top: 50%;

      left: 50%;

      transform: translate(-50%, -50%);

      background: #202123;

      padding: 20px;

      border-radius: 16px;

      z-index: 1000;

      min-width: 300px;

      box-shadow: 0 8px 32px rgba(0,0,0,0.24);

      transition: all 0.3s ease;

    }

    .settings-modal h2 { margin-bottom: 20px; color: #ECECF1; }

    .modal-overlay {

      display: none;

      position: fixed;

      top: 0; left: 0;

      right: 0; bottom: 0;

      background: rgba(0,0,0,0.5);

      z-index: 999;

      transition: all 0.3s ease;

    }

    .scroll-bottom-btn {

      position: fixed;

      bottom: 100px;

      right: 30px;

      width: 45px;

      height: 45px;

      border-radius: 50%;

      background: #19C37D;

      color: white;

      border: none;

      cursor: pointer;

      display: none;

      align-items: center;

      justify-content: center;

      box-shadow: 0 4px 12px rgba(0,0,0,0.2);

      transition: all 0.3s ease;

      z-index: 100;

    }

    .scroll-bottom-btn:hover {

      background: #1A8870;

      transform: translateY(-2px);

      box-shadow: 0 6px 16px rgba(0,0,0,0.3);

    }

    .scroll-bottom-btn svg { width: 20px; height: 20px; }

    ::-webkit-scrollbar { width: 10px; }

    ::-webkit-scrollbar-track { background: transparent; }

    ::-webkit-scrollbar-thumb {

      border-radius: 10px;

      background: rgba(255,255,255,0.1);

    }

    ::-webkit-scrollbar-thumb:hover { background: #565869; }

  </style>

</head>

<body>

  <!-- 下方聊天與設定選單 -->

  <div class="sidebar">

      <button class="new-chat-btn" onclick="createNewChat()">

          <svg fill="none" height="16" stroke-width="1.5" stroke="currentColor" viewbox="0 0 24 24" width="16" xmlns="http://www.w3.org/2000/svg">

            <path d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z" stroke-linecap="round" stroke-linejoin="round"></path>

          </svg>

          New chat

      </button>

      <div class="chats-list" id="chatsList"></div>

      <button class="settings-btn" onclick="openSettings()">

        <svg fill="none" height="16" stroke-width="2" stroke="currentColor" viewbox="0 0 24 24" width="16" xmlns="http://www.w3.org/2000/svg">

          <path d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" stroke-linecap="round" stroke-linejoin="round"></path>

        </svg>

        Settings

      </button>

  </div>


  <!-- 主要對話內容區 -->

  <div class="main-content">

      <div class="chat-container">

          <div class="error-message" id="errorMessage"></div>

          <div class="messages" id="messages"></div>

          <div class="typing-indicator" id="typingIndicator">

              <div class="dot"></div>

              <div class="dot"></div>

              <div class="dot"></div>

          </div>

      </div>

      <!-- 輸入區 -->

      <div class="input-container">

          <textarea id="userInput" placeholder="Send a message"></textarea>

          <button id="sendButton" onclick="sendMessage()">Send</button>

      </div>

  </div>


  <button class="scroll-bottom-btn" id="scrollBottomBtn" onclick="smoothScrollToBottom()">

    <svg fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" stroke="currentColor" viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">

      <polyline points="6 9 12 15 18 9"></polyline>

    </svg>

  </button>


  <!-- 設定視窗 -->

  <div class="modal-overlay" id="modalOverlay"></div>

  <div class="settings-modal" id="settingsModal">

    <h2>Settings</h2>

    <div class="model-selector" id="modelSelector">

        ChatGPT Model:

        <select id="modelSelect">

            <option value="">Loading models...</option>

        </select>

    </div>

    <button onclick="closeSettings()">Close</button>

  </div>


  <script>

    // API 金鑰與全域變數

    const API_KEY = 'yourgsk_ufAUCEISsIiExQCEVACkWGdyb3FY49moAhuboAAtAPdCDYebcefm';

    let chats = [{

      id: 'chat-' + Date.now(),

      title: 'New chat',

      messages: []

    }];

    let currentChatId = chats[0].id;

    const messagesContainer = document.getElementById('messages');


    // 工具函式:捲動到底部

    function scrollToBottom() {

      messagesContainer.scrollTop = messagesContainer.scrollHeight;

    }


    // 更新聊天列表(下方側邊欄)

    function updateChatsList() {

      const chatsList = document.getElementById('chatsList');

      chatsList.innerHTML = '';

      chats.forEach(chat => {

        const chatItem = document.createElement('div');

        chatItem.className = `chat-item ${chat.id === currentChatId ? 'active' : ''}`;

        chatItem.innerHTML = `

          <span onclick="switchChat('${chat.id}')">${chat.title}</span>

          <span class="delete-chat" onclick="deleteChat('${chat.id}')">×</span>

        `;

        chatsList.appendChild(chatItem);

      });

    }


    // 建立新聊天

    function createNewChat() {

      const newChat = {

        id: 'chat-' + Date.now(),

        title: 'New chat',

        messages: []

      };

      chats.unshift(newChat);

      currentChatId = newChat.id;

      saveChatsToLocalStorage();

      updateChatsList();

      updateMessagesDisplay();

      document.getElementById('userInput').focus();

    }


    // 刪除聊天

    function deleteChat(chatId) {

      if (chats.length === 1) {

        createNewChat();

        return;

      }

      chats = chats.filter(chat => chat.id !== chatId);

      if (chatId === currentChatId) {

        currentChatId = chats[0].id;

      }

      saveChatsToLocalStorage();

      updateChatsList();

      updateMessagesDisplay();

    }


    // 切換聊天

    function switchChat(chatId) {

      const chatExists = chats.some(chat => chat.id === chatId);

      if (!chatExists) {

        currentChatId = chats[0].id;

      } else {

        currentChatId = chatId;

      }

      saveChatsToLocalStorage();

      updateChatsList();

      updateMessagesDisplay();

      scrollToBottom();

      document.getElementById('userInput').focus();

    }


    // 更新顯示所有訊息

    function updateMessagesDisplay() {

      const currentChat = chats.find(chat => chat.id === currentChatId);

      if (!currentChat) {

        createNewChat();

        return;

      }

      messagesContainer.innerHTML = '';

      currentChat.messages.forEach(message => {

        const messageDiv = document.createElement('div');

        // 根據角色套用不同樣式:使用者訊息或 AI 回應訊息

        messageDiv.className = `message ${message.role}-message`;

        messageDiv.textContent = message.content;

        messagesContainer.appendChild(messageDiv);

      });

      scrollToBottom();

    }


    // 前端工具函式:移除 <think> 區塊

    function removeThinks(inputText) {

      return inputText.replace(/<think>[\s\S]*?<\/think>/gi, '');

    }


    // 送出訊息並調用 API

    async function sendMessage() {

      const userInput = document.getElementById('userInput');

      const typingIndicator = document.getElementById('typingIndicator');

      const modelSelect = document.getElementById('modelSelect');

      const sendButton = document.getElementById('sendButton');

      const errorMessage = document.getElementById('errorMessage');

      

      if (!userInput.value.trim()) return;

      

      const currentChat = chats.find(chat => chat.id === currentChatId);

      // 禁用輸入與送出按鈕,清除錯誤訊息

      userInput.disabled = true;

      sendButton.disabled = true;

      errorMessage.style.display = 'none';

      

      // 將使用者訊息加入對話記錄

      currentChat.messages.push({

        role: "user",

        content: userInput.value

      });

      

      // 若為第一筆訊息則更新對話標題(取前 30 字)

      if (currentChat.messages.length === 1) {

        currentChat.title = userInput.value.substring(0, 30) + (userInput.value.length > 30 ? '...' : '');

        updateChatsList();

      }

      

      updateMessagesDisplay();

      userInput.value = '';

      typingIndicator.style.display = 'block';

      scrollToBottom();


      try {

        const response = await fetch('https://api.groq.com/openai/v1/chat/completions', {

          method: 'POST',

          headers: {

            'Authorization': `Bearer ${API_KEY}`,

            'Content-Type': 'application/json'

          },

          body: JSON.stringify({

            model: modelSelect.value,            // 例如 deepseek-r1-distill-llama-70b

            messages: currentChat.messages,        // 包含對話歷史

            temperature: 0.7,                      // 隨機性參數

            max_tokens: 2048                       // 最大回應 token 數量

          })

        });

        

        const data = await response.json();

        if (!response.ok) {

          throw new Error(data.error?.message || 'An error occurred while communicating with the API.');

        }

        // 從回應中取出 AI 回應,並使用 removeThinks() 過濾不必要區塊

        currentChat.messages.push({

          role: "assistant",

          content: removeThinks(data.choices[0].message.content)

        });

        

        saveChatsToLocalStorage();

        updateMessagesDisplay();

        scrollToBottom();

      } catch (error) {

        console.error('Error:', error);

        errorMessage.textContent = error.message || 'Sorry, an error occurred. Please try again.';

        errorMessage.style.display = 'block';

      } finally {

        typingIndicator.style.display = 'none';

        userInput.disabled = false;

        sendButton.disabled = false;

      }

    }


    // 監聽鍵盤事件(Enter 送出訊息)

    document.getElementById('userInput').addEventListener('keypress', function (e) {

      if (e.key === 'Enter' && !e.shiftKey) {

        e.preventDefault();

        sendMessage();

      }

    });


    // 開啟設定視窗

    function openSettings() {

      document.getElementById('settingsModal').style.display = 'block';

      document.getElementById('modalOverlay').style.display = 'block';

      document.getElementById('modelSelector').style.display = 'block';

    }


    // 關閉設定視窗

    function closeSettings() {

      document.getElementById('settingsModal').style.display = 'none';

      document.getElementById('modalOverlay').style.display = 'none';

      document.getElementById('modelSelector').style.display = 'none';

    }


    document.getElementById('modalOverlay').addEventListener('click', closeSettings);


    // 儲存與載入對話記錄至 localStorage

    function saveChatsToLocalStorage() {

      localStorage.setItem('chats', JSON.stringify(chats));

      localStorage.setItem('currentChatId', currentChatId);

    }


    function loadChatsFromLocalStorage() {

      const savedChats = localStorage.getItem('chats');

      const savedCurrentChatId = localStorage.getItem('currentChatId');

      if (savedChats) {

        try {

          const parsedChats = JSON.parse(savedChats);

          if (Array.isArray(parsedChats) && parsedChats.length > 0) {

            chats = parsedChats;

            if (savedCurrentChatId && chats.some(chat => chat.id === savedCurrentChatId)) {

              currentChatId = savedCurrentChatId;

            } else {

              currentChatId = chats[0].id;

            }

          } else {

            createNewChat();

          }

        } catch (e) {

          console.error('Error parsing saved chats:', e);

          createNewChat();

        }

      }

    }


    // 從 API 取得可用模型

    async function fetchModels() {

      const modelSelect = document.getElementById('modelSelect');

      const errorMessage = document.getElementById('errorMessage');

      try {

        const response = await fetch('https://api.groq.com/openai/v1/models', {

          headers: { 'Authorization': `Bearer ${API_KEY}` }

        });

        if (!response.ok) {

          throw new Error('Failed to fetch models');

        }

        const data = await response.json();

        modelSelect.innerHTML = '';

        const sortedModels = data.data.sort((a, b) => a.id.localeCompare(b.id));

        sortedModels.forEach(model => {

          const option = document.createElement('option');

          option.value = model.id;

          option.textContent = model.id;

          modelSelect.appendChild(option);

        });

      } catch (error) {

        console.error('Error fetching models:', error);

        errorMessage.textContent = 'Failed to load available models. Using default model list.';

        errorMessage.style.display = 'block';

        const defaultModels = ['gemma-7b-it', 'llama2-70b-4096', 'mixtral-8x7b-32768'].sort();

        modelSelect.innerHTML = defaultModels.map(model => `<option value="${model}">${model}</option>`).join('');

      }

    }


    // 平滑捲動到底部

    function smoothScrollToBottom() {

      messagesContainer.scrollTop = messagesContainer.scrollHeight;

    }


    // 當使用者捲動時,判斷是否顯示捲動按鈕

    messagesContainer.addEventListener('scroll', function() {

      const scrollBtn = document.getElementById('scrollBottomBtn');

      const scrollThreshold = 100; 

      const isNearBottom = messagesContainer.scrollHeight - messagesContainer.scrollTop - messagesContainer.clientHeight < scrollThreshold;

      scrollBtn.style.display = isNearBottom ? 'none' : 'flex';

    });


    // 初始化:取得模型、載入對話記錄與更新畫面

    fetchModels();

    loadChatsFromLocalStorage();

    updateChatsList();

    updateMessagesDisplay();

  </script>

</body>

</html>