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:
773
js/ai.js
Normal file
773
js/ai.js
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user