Files
hr-position-system/js/ai.js
DonaldFang 方士碩 a6af297623 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>
2025-12-09 12:05:20 +08:00

774 lines
29 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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);
}
}