refactor: 建立核心 JavaScript 模組
- 建立 js 目錄 - 分離核心模組: * config.js - API 端點、常數、資料對應表 * utils.js - XSS 防護、表單工具、Toast、錯誤處理 * api.js - LLM API、Position API、CSV API ✨ 使用 ES6 Modules 架構 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
270
js/api.js
Normal file
270
js/api.js
Normal file
@@ -0,0 +1,270 @@
|
||||
/**
|
||||
* API - API 呼叫函式
|
||||
* 包含所有與後端 API 的通訊邏輯
|
||||
*/
|
||||
|
||||
import { API_BASE_URL } from './config.js';
|
||||
import { showCopyableError } from './utils.js';
|
||||
|
||||
// ==================== LLM API ====================
|
||||
|
||||
/**
|
||||
* 獲取當前選擇的 Ollama 模型
|
||||
* @returns {string} - 模型名稱
|
||||
*/
|
||||
function getOllamaModel() {
|
||||
return localStorage.getItem('selectedOllamaModel') || 'deepseek-r1:8b';
|
||||
}
|
||||
|
||||
/**
|
||||
* 調用 Claude/Ollama LLM API
|
||||
* @param {string} prompt - Prompt 內容
|
||||
* @param {string} api - API 類型('ollama' 或其他)
|
||||
* @returns {Promise<Object>} - 解析後的 JSON 回應
|
||||
*/
|
||||
export async function callClaudeAPI(prompt, api = 'ollama') {
|
||||
try {
|
||||
// 準備請求資料
|
||||
const requestData = {
|
||||
api: api,
|
||||
prompt: prompt,
|
||||
max_tokens: 2000
|
||||
};
|
||||
|
||||
// 如果使用 Ollama API,加入選擇的模型
|
||||
if (api === 'ollama') {
|
||||
requestData.model = getOllamaModel();
|
||||
}
|
||||
|
||||
// 調用後端 Flask API,避免 CORS 錯誤
|
||||
const response = await fetch(`${API_BASE_URL}/llm/generate`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(requestData)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || `API 請求失敗: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.error || 'API 調用失敗');
|
||||
}
|
||||
|
||||
// 清理 JSON 代碼塊標記
|
||||
let responseText = data.text;
|
||||
responseText = responseText.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim();
|
||||
return JSON.parse(responseText);
|
||||
} catch (error) {
|
||||
console.error('Error calling LLM API:', error);
|
||||
|
||||
// 嘗試解析更詳細的錯誤訊息
|
||||
let errorDetails = error.message;
|
||||
try {
|
||||
const errorJson = JSON.parse(error.message);
|
||||
errorDetails = JSON.stringify(errorJson, null, 2);
|
||||
} catch (e) {
|
||||
// 不是 JSON,使用原始訊息
|
||||
}
|
||||
|
||||
// 創建可複製的錯誤對話框
|
||||
showCopyableError({
|
||||
title: 'AI 生成錯誤',
|
||||
message: error.message,
|
||||
details: errorDetails,
|
||||
suggestions: [
|
||||
'Flask 後端已啟動 (python start_server.py)',
|
||||
'已在 .env 文件中配置有效的 LLM API Key',
|
||||
'網路連線正常',
|
||||
'嘗試使用不同的 LLM API (DeepSeek 或 OpenAI)'
|
||||
]
|
||||
});
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Position API ====================
|
||||
|
||||
/**
|
||||
* 保存崗位至崗位清單
|
||||
* @param {Object} positionData - 崗位資料
|
||||
* @returns {Promise<Object>} - API 回應
|
||||
*/
|
||||
export async function savePositionToList(positionData) {
|
||||
const response = await fetch(`${API_BASE_URL}/positions`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(positionData)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || '保存失敗');
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* 載入崗位清單
|
||||
* @returns {Promise<Array>} - 崗位清單
|
||||
*/
|
||||
export async function loadPositionList() {
|
||||
const response = await fetch(`${API_BASE_URL}/position-list`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('載入崗位清單失敗');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.data || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 獲取單一崗位描述
|
||||
* @param {string} positionCode - 崗位編號
|
||||
* @returns {Promise<Object>} - 崗位描述資料
|
||||
*/
|
||||
export async function getPositionDescription(positionCode) {
|
||||
const response = await fetch(`${API_BASE_URL}/position-descriptions/${positionCode}`);
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
return null; // 未找到描述
|
||||
}
|
||||
throw new Error('載入崗位描述失敗');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存崗位描述
|
||||
* @param {Object} descData - 崗位描述資料
|
||||
* @returns {Promise<Object>} - API 回應
|
||||
*/
|
||||
export async function savePositionDescription(descData) {
|
||||
const response = await fetch(`${API_BASE_URL}/position-descriptions`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(descData)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || '保存崗位描述失敗');
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
// ==================== CSV API ====================
|
||||
|
||||
/**
|
||||
* 下載崗位 CSV 範本
|
||||
*/
|
||||
export async function downloadPositionCSVTemplate() {
|
||||
window.location.href = `${API_BASE_URL}/positions/csv-template`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 下載職務 CSV 範本
|
||||
*/
|
||||
export async function downloadJobCSVTemplate() {
|
||||
window.location.href = `${API_BASE_URL}/jobs/csv-template`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 匯入崗位 CSV
|
||||
* @param {File} file - CSV 檔案
|
||||
* @returns {Promise<Object>} - API 回應
|
||||
*/
|
||||
export async function importPositionsCSV(file) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/positions/import-csv`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || 'CSV 匯入失敗');
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* 匯入職務 CSV
|
||||
* @param {File} file - CSV 檔案
|
||||
* @returns {Promise<Object>} - API 回應
|
||||
*/
|
||||
export async function importJobsCSV(file) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/jobs/import-csv`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || 'CSV 匯入失敗');
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* 匯出完整崗位資料
|
||||
*/
|
||||
export async function exportCompletePositionData() {
|
||||
window.location.href = `${API_BASE_URL}/position-list/export`;
|
||||
}
|
||||
|
||||
// ==================== Ollama Connection Test ====================
|
||||
|
||||
/**
|
||||
* 測試 Ollama 連線
|
||||
* @returns {Promise<boolean>} - 連線是否成功
|
||||
*/
|
||||
export async function testOllamaConnection() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/llm/generate`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
api: 'ollama',
|
||||
model: getOllamaModel(),
|
||||
prompt: '請回答:「連線測試成功」',
|
||||
max_tokens: 50
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.success === true;
|
||||
} catch (error) {
|
||||
console.error('Ollama 連線測試失敗:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user