- 取得連結
- X
- 以電子郵件傳送
- 其他應用程式
調派 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-Type 為 application/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. 整體流程總結
使用者輸入訊息
使用者在輸入框中輸入訊息並按下「送出」按鈕。前端準備資料
呼叫 removeThinks() 過濾訊息,並將該訊息與目前聊天記錄組成 JSON。發送 API 請求
使用 fetch 呼叫 API,帶入 API Key、模型選擇及參數。接收並處理回應
解析 API 回應,將 AI 回應加入聊天記錄並更新畫面。錯誤處理與回饋
若發生錯誤則顯示錯誤訊息;若成功則解除打字指示器,恢復使用者輸入。
完整範例程式碼
下面的 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> |
- 取得連結
- X
- 以電子郵件傳送
- 其他應用程式