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:
2025-12-05 14:17:41 +08:00
parent 8902f25f6e
commit 880c23b844
3 changed files with 558 additions and 0 deletions

270
js/api.js Normal file
View 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;
}
}