backup: 完成 HR_position_ 表格前綴重命名與欄位對照表整理

變更內容:
- 所有資料表加上 HR_position_ 前綴
- 整理完整欄位顯示名稱與 ID 對照表
- 模組化 JS 檔案 (admin.js, ai.js, csv.js 等)
- 專案結構優化 (docs/, scripts/, tests/ 等)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-09 12:05:20 +08:00
parent a068ef9704
commit a6af297623
82 changed files with 8685 additions and 4933 deletions

773
js/ai.js Normal file
View File

@@ -0,0 +1,773 @@
/**
* AI 生成功能模組
* 處理 LLM API 調用和表單自動填充
*/
import { DEFAULT_AI_PROMPTS } from './prompts.js';
// ==================== 工具函數 ====================
/**
* 消毒 HTML 字串,防止 XSS 攻擊
*/
export function sanitizeHTML(str) {
if (str === null || str === undefined) return '';
const temp = document.createElement('div');
temp.textContent = str;
return temp.innerHTML;
}
/**
* 安全設定元素文字內容
*/
export function safeSetText(element, text) {
if (element) {
element.textContent = text;
}
}
/**
* 只在欄位為空時填入值
*/
export function fillIfEmpty(elementId, value) {
const el = document.getElementById(elementId);
if (el && !el.value.trim() && value) {
el.value = value;
return true;
}
return false;
}
/**
* 獲取欄位當前值
*/
export function getFieldValue(elementId) {
const el = document.getElementById(elementId);
return el ? el.value.trim() : '';
}
/**
* 獲取空白欄位列表
*/
export function getEmptyFields(fieldIds) {
return fieldIds.filter(id => !getFieldValue(id));
}
/**
* 獲取 Ollama 模型選項
*/
export function getOllamaModel() {
const select = document.getElementById('ollamaModel');
return select ? select.value : 'llama3.2';
}
// ==================== AI API 調用 ====================
/**
* 調用 LLM API
*/
export async function callClaudeAPI(prompt, api = 'ollama') {
try {
const requestData = {
api: api,
prompt: prompt,
max_tokens: 2000
};
if (api === 'ollama') {
requestData.model = getOllamaModel();
}
const response = await fetch("/api/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 調用失敗');
}
let responseText = data.text;
// 移除 markdown 代碼塊標記
responseText = responseText.replace(/```json\n?/g, "").replace(/```\n?/g, "").trim();
// 嘗試提取 JSON 物件
const jsonMatch = responseText.match(/\{[\s\S]*\}/);
if (jsonMatch) {
const allMatches = responseText.match(/\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/g);
if (allMatches && allMatches.length > 0) {
for (let i = allMatches.length - 1; i >= 0; i--) {
try {
const parsed = JSON.parse(allMatches[i]);
if (Object.keys(parsed).length > 0) {
return parsed;
}
} catch (e) {
continue;
}
}
}
try {
return JSON.parse(jsonMatch[0]);
} catch (e) {
// 繼續嘗試其他方法
}
}
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 app.py)',
'已在 .env 文件中配置有效的 LLM API Key',
'網路連線正常',
'確認 Prompt 要求返回純 JSON 格式',
'嘗試使用不同的 LLM API (切換到其他模型)',
'檢查 LLM 模型是否支援繁體中文'
]
});
throw error;
}
}
// ==================== 按鈕狀態控制 ====================
export function setButtonLoading(btn, loading) {
if (loading) {
btn.disabled = true;
btn.innerHTML = '<div class="spinner"></div><span>AI 生成中...</span>';
} else {
btn.disabled = false;
btn.innerHTML = '<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg><span>✨ I\'m feeling lucky</span>';
}
}
// ==================== AI 幫我想功能 ====================
/**
* 初始化所有 prompt 編輯器
*/
export function initializePromptEditors() {
const modules = Object.keys(DEFAULT_AI_PROMPTS);
modules.forEach(module => {
const textarea = document.getElementById(`prompt_${module}`);
if (textarea) {
const savedPrompt = localStorage.getItem(`aiPrompt_${module}`);
textarea.value = savedPrompt || DEFAULT_AI_PROMPTS[module];
textarea.addEventListener('input', () => {
localStorage.setItem(`aiPrompt_${module}`, textarea.value);
});
}
});
}
/**
* 重置 prompt 為預設值
*/
export function resetPromptToDefault(module) {
const textarea = document.getElementById(`prompt_${module}`);
if (textarea && DEFAULT_AI_PROMPTS[module]) {
textarea.value = DEFAULT_AI_PROMPTS[module];
localStorage.removeItem(`aiPrompt_${module}`);
if (typeof showToast === 'function') {
showToast('已重置為預設 Prompt');
}
}
}
/**
* 執行 AI 幫我想
*/
export async function executeAIHelper(module) {
const container = document.querySelector(`.ai-helper-container[data-module="${module}"]`);
const btn = container.querySelector('.ai-helper-btn');
const textarea = document.getElementById(`prompt_${module}`);
if (!textarea || !textarea.value.trim()) {
if (typeof showToast === 'function') {
showToast('請輸入 Prompt 指令', 'error');
}
return;
}
const originalHTML = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<div class="spinner"></div><span>AI 生成中...</span>';
try {
const prompt = textarea.value.trim();
const data = await callClaudeAPI(prompt);
fillFormWithAIHelperResult(module, data);
if (typeof showToast === 'function') {
showToast('AI 生成完成!已填入表單');
}
} catch (error) {
console.error('AI Helper error:', error);
} finally {
btn.disabled = false;
btn.innerHTML = originalHTML;
}
}
/**
* 根據模組填入 AI 結果
*/
export function fillFormWithAIHelperResult(module, data) {
const fieldMappings = {
positionBasic: {
positionCode: 'positionCode',
positionName: 'positionName',
positionCategory: 'positionCategory',
positionNature: 'positionNature',
headcount: 'headcount',
positionLevel: 'positionLevel',
positionDesc: 'positionDesc',
positionRemark: 'positionRemark'
},
positionRecruit: {
education: 'recruitEducation',
experience: 'recruitExperience',
skills: 'recruitSkills',
certificates: 'recruitCertificates',
languages: 'recruitLanguages',
specialRequirements: 'recruitSpecialReq'
},
jobBasic: {
jobCode: 'jobCode',
jobName: 'jobName',
jobGrade: 'jobGrade',
jobCategory: 'jobCategory',
careerPath: 'careerPath',
jobDesc: 'jobDesc'
},
deptFunction: {
deptCode: 'deptCode',
deptName: 'deptName',
parentDept: 'parentDept',
deptHead: 'deptHead',
deptFunction: 'deptFunction',
kpi: 'kpi'
},
jobDesc: {
positionName: 'descPositionName',
department: 'descDepartment',
directSupervisor: 'descDirectSupervisor',
positionPurpose: 'descPositionPurpose',
mainResponsibilities: 'descMainResponsibilities',
education: 'descEducation',
basicSkills: 'descBasicSkills',
professionalKnowledge: 'descProfessionalKnowledge'
}
};
const mapping = fieldMappings[module];
if (!mapping) return;
let filledCount = 0;
for (const [dataKey, elementId] of Object.entries(mapping)) {
if (data[dataKey] !== undefined) {
const value = Array.isArray(data[dataKey])
? data[dataKey].join('\n')
: String(data[dataKey]);
if (fillIfEmpty(elementId, value)) {
filledCount++;
}
}
}
if (filledCount > 0) {
console.log(`[AI Helper] 已填入 ${filledCount} 個欄位`);
}
}
// ==================== 錯誤顯示 ====================
/**
* 顯示可複製的錯誤訊息
*/
export function showCopyableError(options) {
const { title, message, details, suggestions } = options;
const modal = document.createElement('div');
modal.style.cssText = `
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
animation: fadeIn 0.3s;
`;
modal.innerHTML = `
<div style="
background: white;
border-radius: 12px;
max-width: 600px;
width: 90%;
max-height: 80vh;
overflow: hidden;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
display: flex;
flex-direction: column;
">
<div style="
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
color: white;
padding: 20px;
display: flex;
align-items: center;
gap: 15px;
">
<span style="font-size: 2rem;">❌</span>
<h3 style="margin: 0; font-size: 1.3rem; flex: 1;">${sanitizeHTML(title)}</h3>
<button onclick="this.closest('[style*=\"position: fixed\"]').remove()" style="
background: rgba(255,255,255,0.2);
border: none;
color: white;
width: 30px;
height: 30px;
border-radius: 50%;
cursor: pointer;
font-size: 1.2rem;
">×</button>
</div>
<div style="padding: 25px; overflow-y: auto; flex: 1;">
<div style="color: #333; line-height: 1.6; margin-bottom: 20px;">${sanitizeHTML(message)}</div>
${suggestions && suggestions.length > 0 ? `
<div style="background: #fff3cd; border: 1px solid #ffc107; border-radius: 6px; padding: 15px; margin-bottom: 20px;">
<strong style="color: #856404; display: block; margin-bottom: 10px;">💡 請確保:</strong>
<ul style="margin: 0; padding-left: 20px; color: #856404;">
${suggestions.map(s => `<li style="margin: 5px 0;">${sanitizeHTML(s)}</li>`).join('')}
</ul>
</div>
` : ''}
${details ? `
<details style="background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 6px; padding: 15px;">
<summary style="cursor: pointer; font-weight: 600; color: #495057;">🔍 詳細錯誤訊息(點擊展開)</summary>
<pre id="errorDetailsText" style="background: white; padding: 15px; border-radius: 4px; overflow-x: auto; font-size: 0.85rem; color: #666; margin: 10px 0 0 0; white-space: pre-wrap; word-break: break-word; max-height: 300px; overflow-y: auto;">${sanitizeHTML(details)}</pre>
</details>
` : ''}
</div>
<div style="padding: 15px 25px; border-top: 1px solid #f0f0f0; display: flex; justify-content: flex-end;">
<button onclick="this.closest('[style*=\"position: fixed\"]').remove()" style="
background: #007bff;
color: white;
border: none;
padding: 10px 25px;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
">確定</button>
</div>
</div>
`;
document.body.appendChild(modal);
modal.addEventListener('click', (e) => {
if (e.target === modal) {
modal.remove();
}
});
}
// ==================== 各表單 AI 生成函數 ====================
/**
* 生成崗位基礎資料
*/
export async function generatePositionBasic(event) {
const btn = event.target.closest('.ai-generate-btn');
const allFields = ['positionCode', 'positionName', 'positionCategory', 'positionNature', 'headcount', 'positionLevel', 'positionDesc', 'positionRemark'];
const emptyFields = getEmptyFields(allFields);
if (emptyFields.length === 0) {
if (typeof showToast === 'function') showToast('所有欄位都已填寫完成!');
return;
}
setButtonLoading(btn, true);
try {
const existingData = {};
allFields.forEach(field => {
const value = getFieldValue(field);
if (value) existingData[field] = value;
});
const contextInfo = Object.keys(existingData).length > 0
? `\n\n已填寫的資料(請參考這些內容來生成相關的資料):\n${JSON.stringify(existingData, null, 2)}`
: '';
const prompt = `你是專業人資顧問,熟悉半導體製造業的人資所有流程。請生成崗位基礎資料。請用繁體中文回覆。
${contextInfo}
請「只生成」以下這些尚未填寫的欄位:${emptyFields.join(', ')}
欄位說明:
- positionCode: 崗位編號(格式如 ENG-001, MGR-002, SAL-003
- positionName: 崗位名稱
- positionCategory: 崗位類別代碼01=技術職, 02=管理職, 03=業務職, 04=行政職)
- positionNature: 崗位性質代碼FT=全職, PT=兼職, CT=約聘, IN=實習)
- headcount: 編制人數1-10之間的數字字串
- positionLevel: 崗位級別L1到L7
- positionDesc: 崗位描述(條列式,用換行分隔)
- positionRemark: 崗位備注(條列式,用換行分隔)
重要請「只」返回純JSON格式不要有任何解釋、思考過程或額外文字。格式如下
{
${emptyFields.map(f => `"${f}": "..."`).join(',\n ')}
}`;
const data = await callClaudeAPI(prompt);
let filledCount = 0;
if (fillIfEmpty('positionCode', data.positionCode)) filledCount++;
if (fillIfEmpty('positionName', data.positionName)) filledCount++;
if (fillIfEmpty('positionCategory', data.positionCategory)) {
filledCount++;
if (typeof updateCategoryName === 'function') updateCategoryName();
}
if (fillIfEmpty('positionNature', data.positionNature)) {
filledCount++;
if (typeof updateNatureName === 'function') updateNatureName();
}
if (fillIfEmpty('headcount', data.headcount)) filledCount++;
if (fillIfEmpty('positionLevel', data.positionLevel)) filledCount++;
if (fillIfEmpty('positionDesc', data.positionDesc)) filledCount++;
if (fillIfEmpty('positionRemark', data.positionRemark)) filledCount++;
if (typeof updatePreview === 'function') updatePreview();
if (typeof showToast === 'function') showToast(`✨ AI 已補充 ${filledCount} 個欄位!`);
} catch (error) {
if (typeof showToast === 'function') showToast('生成失敗,請稍後再試');
} finally {
setButtonLoading(btn, false);
}
}
/**
* 生成招聘要求資料
*/
export async function generatePositionRecruit(event) {
const btn = event.target.closest('.ai-generate-btn');
const allFields = ['minEducation', 'requiredGender', 'salaryRange', 'workExperience', 'minAge', 'maxAge', 'jobType', 'recruitPosition', 'jobTitle', 'jobDesc', 'positionReq', 'skillReq', 'langReq', 'otherReq'];
const emptyFields = getEmptyFields(allFields);
if (emptyFields.length === 0) {
if (typeof showToast === 'function') showToast('所有欄位都已填寫完成!');
return;
}
setButtonLoading(btn, true);
try {
const positionName = getFieldValue('positionName') || '一般職位';
const existingData = { positionName };
allFields.forEach(field => {
const value = getFieldValue(field);
if (value) existingData[field] = value;
});
const prompt = `請生成「${positionName}」的招聘要求資料。請用繁體中文回覆。
已填寫的資料(請參考這些內容來生成相關的資料):
${JSON.stringify(existingData, null, 2)}
請「只生成」以下這些尚未填寫的欄位:${emptyFields.join(', ')}
欄位說明:
- minEducation: 最低學歷代碼HS=高中職, JC=專科, BA=大學, MA=碩士, PHD=博士)
- requiredGender: 要求性別(空字串=不限, M=男, F=女)
- salaryRange: 薪酬范圍代碼A=30000以下, B=30000-50000, C=50000-80000, D=80000-120000, E=120000以上, N=面議)
- workExperience: 工作經驗年數0=不限, 1, 3, 5, 10
- minAge: 最小年齡18-30之間的數字字串
- maxAge: 最大年齡35-55之間的數字字串
- jobType: 工作性質代碼FT=全職, PT=兼職, CT=約聘, DP=派遣)
- recruitPosition: 招聘職位代碼ENG=工程師, MGR=經理, AST=助理, OP=作業員, SAL=業務)
- jobTitle: 職位名稱
- jobDesc: 職位描述2-3句話
- positionReq: 崗位要求(條列式,用換行分隔)
- skillReq: 技能要求(條列式,用換行分隔)
- langReq: 語言要求(條列式,用換行分隔)
- otherReq: 其他要求(條列式,用換行分隔)
重要請「只」返回純JSON格式不要有任何解釋、思考過程或額外文字。格式如下
{
${emptyFields.map(f => `"${f}": "..."`).join(',\n ')}
}`;
const data = await callClaudeAPI(prompt);
let filledCount = 0;
if (fillIfEmpty('minEducation', data.minEducation)) filledCount++;
if (fillIfEmpty('requiredGender', data.requiredGender)) filledCount++;
if (fillIfEmpty('salaryRange', data.salaryRange)) filledCount++;
if (fillIfEmpty('workExperience', data.workExperience)) filledCount++;
if (fillIfEmpty('minAge', data.minAge)) filledCount++;
if (fillIfEmpty('maxAge', data.maxAge)) filledCount++;
if (fillIfEmpty('jobType', data.jobType)) filledCount++;
if (fillIfEmpty('recruitPosition', data.recruitPosition)) filledCount++;
if (fillIfEmpty('jobTitle', data.jobTitle)) filledCount++;
if (fillIfEmpty('jobDesc', data.jobDesc)) filledCount++;
if (fillIfEmpty('positionReq', data.positionReq)) filledCount++;
if (fillIfEmpty('skillReq', data.skillReq)) filledCount++;
if (fillIfEmpty('langReq', data.langReq)) filledCount++;
if (fillIfEmpty('otherReq', data.otherReq)) filledCount++;
if (typeof updatePreview === 'function') updatePreview();
if (typeof showToast === 'function') showToast(`✨ AI 已補充 ${filledCount} 個欄位!`);
} catch (error) {
if (typeof showToast === 'function') showToast('生成失敗,請稍後再試');
} finally {
setButtonLoading(btn, false);
}
}
/**
* 生成職務基礎資料
*/
export async function generateJobBasic(event) {
const btn = event.target.closest('.ai-generate-btn');
const allFields = ['jobCategoryCode', 'jobCode', 'jobName', 'jobNameEn', 'jobHeadcount', 'jobSortOrder', 'jobRemark', 'jobLevel'];
const emptyFields = getEmptyFields(allFields);
const attBonusChk = document.getElementById('job_hasAttBonus');
const houseAllowChk = document.getElementById('job_hasHouseAllow');
const needCheckboxes = !(attBonusChk?.checked) && !(houseAllowChk?.checked);
if (emptyFields.length === 0 && !needCheckboxes) {
if (typeof showToast === 'function') showToast('所有欄位都已填寫完成!');
return;
}
setButtonLoading(btn, true);
try {
const existingData = {};
allFields.forEach(field => {
const value = getFieldValue(field);
if (value) existingData[field] = value;
});
const contextInfo = Object.keys(existingData).length > 0
? `\n\n已填寫的資料(請參考這些內容來生成相關的資料):\n${JSON.stringify(existingData, null, 2)}`
: '';
const fieldsToGenerate = [...emptyFields];
if (needCheckboxes) {
fieldsToGenerate.push('hasAttendanceBonus', 'hasHousingAllowance');
}
const prompt = `你是專業人資顧問,熟悉半導體製造業的人資所有流程。請生成職務基礎資料。請用繁體中文回覆。
${contextInfo}
請「只生成」以下這些尚未填寫的欄位:${fieldsToGenerate.join(', ')}
欄位說明:
- jobCategoryCode: 職務類別代碼MGR=管理職, TECH=技術職, SALE=業務職, ADMIN=行政職, RD=研發職, PROD=生產職)
- jobCode: 職務編號(格式如 MGR-001, TECH-002
- jobName: 職務名稱
- jobNameEn: 職務英文名稱
- jobHeadcount: 編制人數1-20之間的數字字串
- jobSortOrder: 排列順序10, 20, 30...的數字字串)
- jobRemark: 備注說明
- jobLevel: 職務層級(可以是 *保密* 或具體層級)
- hasAttendanceBonus: 是否有全勤true/false
- hasHousingAllowance: 是否住房補貼true/false
重要請「只」返回純JSON格式不要有任何解釋、思考過程或額外文字。格式如下
{
${fieldsToGenerate.map(f => `"${f}": "..."`).join(',\n ')}
}`;
const data = await callClaudeAPI(prompt);
let filledCount = 0;
if (fillIfEmpty('jobCategoryCode', data.jobCategoryCode)) {
filledCount++;
if (typeof updateJobCategoryName === 'function') updateJobCategoryName();
}
if (fillIfEmpty('jobCode', data.jobCode)) filledCount++;
if (fillIfEmpty('jobName', data.jobName)) filledCount++;
if (fillIfEmpty('jobNameEn', data.jobNameEn)) filledCount++;
if (fillIfEmpty('jobHeadcount', data.jobHeadcount)) filledCount++;
if (fillIfEmpty('jobSortOrder', data.jobSortOrder)) filledCount++;
if (fillIfEmpty('jobRemark', data.jobRemark)) filledCount++;
if (fillIfEmpty('jobLevel', data.jobLevel)) filledCount++;
if (needCheckboxes) {
const attendanceCheckbox = document.getElementById('job_hasAttBonus');
const housingCheckbox = document.getElementById('job_hasHouseAllow');
if (data.hasAttendanceBonus === true && attendanceCheckbox) {
attendanceCheckbox.checked = true;
document.getElementById('attendanceLabel').textContent = '是';
filledCount++;
}
if (data.hasHousingAllowance === true && housingCheckbox) {
housingCheckbox.checked = true;
document.getElementById('housingLabel').textContent = '是';
filledCount++;
}
}
if (typeof updatePreview === 'function') updatePreview();
if (typeof showToast === 'function') showToast(`✨ AI 已補充 ${filledCount} 個欄位!`);
} catch (error) {
if (typeof showToast === 'function') showToast('生成失敗,請稍後再試');
} finally {
setButtonLoading(btn, false);
}
}
/**
* 生成崗位描述資料
*/
export async function generateJobDesc(event) {
const btn = event.target.closest('.ai-generate-btn');
const allFields = ['jd_empNo', 'jd_empName', 'jd_positionCode', 'jd_versionDate', 'jd_positionName', 'jd_department', 'jd_positionEffectiveDate', 'jd_directSupervisor', 'jd_directReports', 'jd_workLocation', 'jd_empAttribute', 'jd_positionPurpose', 'jd_mainResponsibilities', 'jd_education', 'jd_basicSkills', 'jd_professionalKnowledge', 'jd_workExperienceReq', 'jd_otherRequirements'];
const emptyFields = allFields.filter(id => {
const el = document.getElementById(id);
const value = el ? el.value.trim() : '';
if (id === 'jd_mainResponsibilities') {
return !value || value === '1、\n2、\n3、\n4、' || value === '1、\n2、\n3、\n4、\n5、';
}
return !value;
});
if (emptyFields.length === 0) {
if (typeof showToast === 'function') showToast('所有欄位都已填寫完成!');
return;
}
setButtonLoading(btn, true);
try {
const existingData = {};
allFields.forEach(field => {
const el = document.getElementById(field);
const value = el ? el.value.trim() : '';
if (value && value !== '1、\n2、\n3、\n4、') {
existingData[field.replace('jd_', '')] = value;
}
});
const contextInfo = Object.keys(existingData).length > 0
? `\n\n已填寫的資料(請參考這些內容來生成相關的資料):\n${JSON.stringify(existingData, null, 2)}`
: '';
const fieldsToGenerate = emptyFields.map(f => f.replace('jd_', ''));
const prompt = `請生成崗位描述資料。請用繁體中文回覆。
${contextInfo}
請「只生成」以下這些尚未填寫的欄位:${fieldsToGenerate.join(', ')}
欄位說明:
- empNo: 工號(格式如 A001234
- empName: 員工姓名
- positionCode: 崗位代碼
- versionDate: 版本日期YYYY-MM-DD格式
- positionName: 崗位名稱
- department: 所屬部門
- positionEffectiveDate: 崗位生效日期YYYY-MM-DD格式
- directSupervisor: 直接領導職務
- directReports: 直接下級(格式如「工程師 x 5人」
- workLocation: 任職地點代碼HQ=總部, TPE=台北, TYC=桃園, KHH=高雄, SH=上海, SZ=深圳)
- empAttribute: 員工屬性代碼FT=正式員工, CT=約聘, PT=兼職, IN=實習, DP=派遣)
- positionPurpose: 崗位設置目的1句話說明
- mainResponsibilities: 主要崗位職責用「1、」「2、」「3、」「4、」「5、」格式每項換行\\n分隔
- education: 教育程度要求(條列式,用換行分隔)
- basicSkills: 基本技能要求(條列式,用換行分隔)
- professionalKnowledge: 專業知識要求(條列式,用換行分隔)
- workExperienceReq: 工作經驗要求(條列式,用換行分隔)
- otherRequirements: 其他要求(條列式,用換行分隔)
重要請「只」返回純JSON格式不要有任何解釋、思考過程或額外文字。格式如下
{
${fieldsToGenerate.map(f => `"${f}": "..."`).join(',\n ')}
}`;
const data = await callClaudeAPI(prompt);
let filledCount = 0;
const fieldMapping = {
'empNo': 'jd_empNo',
'empName': 'jd_empName',
'positionCode': 'jd_positionCode',
'versionDate': 'jd_versionDate',
'positionName': 'jd_positionName',
'department': 'jd_department',
'positionEffectiveDate': 'jd_positionEffectiveDate',
'directSupervisor': 'jd_directSupervisor',
'directReports': 'jd_directReports',
'workLocation': 'jd_workLocation',
'empAttribute': 'jd_empAttribute',
'positionPurpose': 'jd_positionPurpose',
'mainResponsibilities': 'jd_mainResponsibilities',
'education': 'jd_education',
'basicSkills': 'jd_basicSkills',
'professionalKnowledge': 'jd_professionalKnowledge',
'workExperienceReq': 'jd_workExperienceReq',
'otherRequirements': 'jd_otherRequirements'
};
Object.keys(fieldMapping).forEach(apiField => {
const htmlId = fieldMapping[apiField];
if (data[apiField]) {
const el = document.getElementById(htmlId);
const currentValue = el ? el.value.trim() : '';
const isEmpty = !currentValue || currentValue === '1、\n2、\n3、\n4、';
if (isEmpty) {
el.value = data[apiField];
filledCount++;
}
}
});
if (typeof updatePreview === 'function') updatePreview();
if (typeof showToast === 'function') showToast(`✨ AI 已補充 ${filledCount} 個欄位!`);
} catch (error) {
if (typeof showToast === 'function') showToast('生成失敗,請稍後再試');
} finally {
setButtonLoading(btn, false);
}
}