Files
hr-position-system/js/ai-bags.js
DonaldFang 方士碩 8069f1b628 feat: 實作三個錦囊 AI 功能
- 新增 AI 錦囊 CSS 樣式到 components.css
- 創建 js/ai-bags.js 模組,包含:
  * 5個模組各3個錦囊的預設 prompt 模板
  * executeAIBag() - 執行 AI 生成並填充表單
  * editBagPrompt() - 編輯自定義 prompt
  * LocalStorage 管理自定義 prompt
- 更新 index.html:
  * 替換 5 處 AI 按鈕為三個錦囊(崗位基礎、招聘要求、職務、部門職責、崗位描述)
  * 新增 Prompt 編輯模態框
- 更新 main.js 引入 ai-bags.js 並初始化
- 新增設計文檔:三個錦囊設計.md
- 新增欄位對照文檔:表單欄位清單.md、更新欄位名稱.md、ID重命名對照表.md

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-06 01:19:54 +08:00

629 lines
22 KiB
JavaScript
Raw Permalink 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 Bags - 三個錦囊功能
* 提供可自定義 prompt 的 AI 生成按鈕
*/
import { callClaudeAPI } from './api.js';
import { showToast, fillIfEmpty } from './utils.js';
import { getPositionFormData, getJobFormData, getJobDescFormData, getDeptFunctionFormData } from './ui.js';
// ==================== 預設 Prompt 模板 ====================
const DEFAULT_PROMPTS = {
positionBasic: {
bag1: {
title: '簡化版',
subtitle: '僅必填欄位',
prompt: `你是專業人資顧問,熟悉半導體製造業。請生成崗位基礎資料(僅必填欄位)。
已填寫的資料:{existingData}
需要生成的欄位positionCode, positionName
請用繁體中文,返回 JSON 格式,不要有任何其他文字。`
},
bag2: {
title: '標準版',
subtitle: '常用欄位',
prompt: `你是專業人資顧問,熟悉半導體製造業。請生成崗位基礎資料(標準版)。
已填寫的資料:{existingData}
需要生成的欄位positionCode, positionName, positionCategory, positionLevel, headcount
欄位說明:
- positionCode: 崗位編號(格式如 ENG-001
- positionName: 崗位名稱
- positionCategory: 崗位類別代碼01=技術職, 02=管理職, 03=業務職, 04=行政職)
- positionLevel: 崗位級別L1-L7
- headcount: 編制人數1-10
請用繁體中文,返回 JSON 格式,不要有任何其他文字。`
},
bag3: {
title: '詳細版',
subtitle: '所有欄位',
prompt: `你是專業人資顧問,熟悉半導體製造業的人資所有流程。請生成完整的崗位基礎資料。
已填寫的資料:{existingData}
需要生成的欄位positionCode, positionName, positionCategory, positionNature, headcount, positionLevel, positionDesc, positionRemark
欄位說明:
- positionCode: 崗位編號(格式如 ENG-001, MGR-002
- positionName: 崗位名稱
- positionCategory: 崗位類別代碼01=技術職, 02=管理職, 03=業務職, 04=行政職)
- positionNature: 崗位性質代碼FT=全職, PT=兼職, CT=約聘, IN=實習)
- headcount: 編制人數1-10之間的數字字串
- positionLevel: 崗位級別L1到L7
- positionDesc: 崗位描述(條列式,用換行分隔)
- positionRemark: 崗位備注(條列式,用換行分隔)
請用繁體中文,返回 JSON 格式,不要有任何其他文字。`
}
},
positionRecruit: {
bag1: {
title: '基本需求',
subtitle: '核心要求',
prompt: `請生成「{positionName}」的基本招聘要求。
已填寫的資料:{existingData}
需要生成的欄位minEducation, workExperience, jobType, jobTitle
請用繁體中文,返回 JSON 格式,不要有任何其他文字。`
},
bag2: {
title: '標準需求',
subtitle: '完整資訊',
prompt: `請生成「{positionName}」的標準招聘要求。
已填寫的資料:{existingData}
需要生成的欄位minEducation, salaryRange, workExperience, jobType, recruitPosition, jobTitle, positionReq, skillReq
欄位說明:
- minEducation: 最低學歷代碼HS=高中職, JC=專科, BA=大學, MA=碩士, PHD=博士)
- salaryRange: 薪酬范圍代碼A=30000以下, B=30000-50000, C=50000-80000, D=80000-120000, E=120000以上, N=面議)
- workExperience: 工作經驗年數0=不限, 1, 3, 5, 10
- jobType: 工作性質代碼FT=全職, PT=兼職, CT=約聘, DP=派遣)
- recruitPosition: 招聘職位代碼ENG=工程師, MGR=經理, AST=助理, OP=作業員, SAL=業務)
- positionReq: 崗位要求(條列式,用換行分隔)
- skillReq: 技能要求(條列式,用換行分隔)
請用繁體中文,返回 JSON 格式,不要有任何其他文字。`
},
bag3: {
title: '完整需求',
subtitle: '18個欄位',
prompt: `請生成「{positionName}」的完整招聘要求資料。
已填寫的資料:{existingData}
需要生成所有空白欄位。
欄位說明:
- minEducation: 最低學歷代碼HS=高中職, JC=專科, BA=大學, MA=碩士, PHD=博士)
- requiredGender: 性別要求代碼M=限男性, F=限女性, N=不限)
- salaryRange: 薪酬范圍代碼A=30000以下, B=30000-50000, C=50000-80000, D=80000-120000, E=120000以上, N=面議)
- workExperience: 工作經驗年數0=不限, 1, 3, 5, 10
- minAge: 最低年齡18-65
- maxAge: 最高年齡18-65
- jobType: 工作性質代碼FT=全職, PT=兼職, CT=約聘, DP=派遣)
- recruitPosition: 招聘職位代碼ENG=工程師, MGR=經理, AST=助理, OP=作業員, SAL=業務)
- jobTitle: 職位名稱
- jobDesc: 工作內容(條列式,用換行分隔)
- positionReq: 崗位要求(條列式,用換行分隔)
- titleReq: 職稱要求(條列式,用換行分隔)
- majorReq: 科系要求(多個科系用逗號分隔)
- skillReq: 技能要求(條列式,用換行分隔)
- langReq: 語言要求(條列式,用換行分隔)
- otherReq: 其他要求(條列式,用換行分隔)
- superiorPosition: 直屬主管職位
- recruitRemark: 招聘備注
請用繁體中文,返回 JSON 格式,不要有任何其他文字。`
}
},
jobBasic: {
bag1: {
title: '簡化版',
subtitle: '核心欄位',
prompt: `請生成職務基礎資料(簡化版)。
已填寫的資料:{existingData}
需要生成的欄位jobCode, jobName, jobCategoryCode
請用繁體中文,返回 JSON 格式,不要有任何其他文字。`
},
bag2: {
title: '標準版',
subtitle: '常用欄位',
prompt: `請生成職務基礎資料(標準版)。
已填寫的資料:{existingData}
需要生成的欄位jobCode, jobName, jobNameEn, jobCategoryCode, jobLevel, jobHeadcount
欄位說明:
- jobCode: 職務編號(格式如 J001
- jobName: 職務名稱(繁體中文)
- jobNameEn: 職務名稱英文
- jobCategoryCode: 職務類別代碼01=技術類, 02=管理類, 03=業務類, 04=行政類)
- jobLevel: 職務級別J1-J7
- jobHeadcount: 職務人數1-100
請用繁體中文,返回 JSON 格式,不要有任何其他文字。`
},
bag3: {
title: '完整版',
subtitle: '所有欄位',
prompt: `請生成完整的職務基礎資料。
已填寫的資料:{existingData}
需要生成所有空白欄位。
欄位說明:
- jobCode: 職務編號(格式如 J001
- jobName: 職務名稱(繁體中文)
- jobNameEn: 職務名稱英文
- jobCategoryCode: 職務類別代碼01=技術類, 02=管理類, 03=業務類, 04=行政類)
- jobLevel: 職務級別J1-J7
- jobHeadcount: 職務人數1-100
- jobEffectiveDate: 生效日期YYYY-MM-DD
- jobSortOrder: 排序順序1-999
- hasAttendanceBonus: 是否有全勤獎金true/false
- hasHousingAllowance: 是否有住宿津貼true/false
- jobRemark: 職務備註
請用繁體中文,返回 JSON 格式,不要有任何其他文字。`
}
},
deptFunction: {
bag1: {
title: '基本版',
subtitle: '核心資訊',
prompt: `請生成部門職責資料(基本版)。
已填寫的資料:{existingData}
需要生成的欄位deptFunctionCode, deptFunctionName, deptFunctionBU, deptFunctionDept
請用繁體中文,返回 JSON 格式,不要有任何其他文字。`
},
bag2: {
title: '標準版',
subtitle: '含職責描述',
prompt: `請生成部門職責資料(標準版)。
已填寫的資料:{existingData}
需要生成的欄位deptFunctionCode, deptFunctionName, deptFunctionBU, deptFunctionDept, deptManager, deptMission, deptCoreFunctions
欄位說明:
- deptFunctionCode: 部門職責編號(格式如 DF001
- deptFunctionName: 部門職責名稱
- deptFunctionBU: 事業單位代碼BU1-BU5
- deptFunctionDept: 部門代碼DEPT1-DEPT20
- deptManager: 部門主管
- deptMission: 部門使命(簡短描述)
- deptCoreFunctions: 核心職能(條列式,用換行分隔)
請用繁體中文,返回 JSON 格式,不要有任何其他文字。`
},
bag3: {
title: '完整版',
subtitle: '含KPI指標',
prompt: `請生成完整的部門職責資料。
已填寫的資料:{existingData}
需要生成所有空白欄位。
欄位說明:
- deptFunctionCode: 部門職責編號(格式如 DF001
- deptFunctionName: 部門職責名稱
- deptFunctionBU: 事業單位代碼BU1-BU5
- deptFunctionDept: 部門代碼DEPT1-DEPT20
- deptManager: 部門主管
- deptMission: 部門使命(簡短描述)
- deptVision: 部門願景(條列式,用換行分隔)
- deptCoreFunctions: 核心職能(條列式,用換行分隔)
- deptKPIs: KPI 指標(條列式,用換行分隔)
請用繁體中文,返回 JSON 格式,不要有任何其他文字。`
}
},
jobDesc: {
bag1: {
title: '基本版',
subtitle: '核心資訊',
prompt: `請生成崗位描述資料(基本版)。
已填寫的資料:{existingData}
需要生成的欄位positionName, department, positionPurpose
請用繁體中文,返回 JSON 格式,不要有任何其他文字。`
},
bag2: {
title: '標準版',
subtitle: '含職責說明',
prompt: `請生成崗位描述資料(標準版)。
已填寫的資料:{existingData}
需要生成的欄位positionName, department, directSupervisor, positionPurpose, mainResponsibilities, education, basicSkills
欄位說明:
- positionName: 崗位名稱
- department: 所屬部門
- directSupervisor: 直屬主管
- positionPurpose: 崗位目的(簡短描述)
- mainResponsibilities: 主要職責(條列式,用換行分隔)
- education: 學歷要求
- basicSkills: 基本技能(條列式,用換行分隔)
請用繁體中文,返回 JSON 格式,不要有任何其他文字。`
},
bag3: {
title: '完整版',
subtitle: '33個欄位',
prompt: `請生成完整的崗位描述資料。
已填寫的資料:{existingData}
需要生成所有空白欄位。
包含以下區塊:
1. 基本資訊empNo, empName, positionCode, versionDate
2. 崗位資訊positionName, department, positionEffectiveDate, directSupervisor, positionGradeJob, reportTo, directReports, workLocation, empAttribute
3. 職責與目的positionPurpose, mainResponsibilities
4. 任職要求education, basicSkills, professionalKnowledge, workExperienceReq, otherRequirements
請用繁體中文,返回 JSON 格式,不要有任何其他文字。`
}
}
};
// ==================== LocalStorage 管理 ====================
/**
* 獲取模組的 prompts優先使用自定義否則使用預設
* @param {string} module - 模組名稱
* @returns {Object} - 包含三個 bag 的 prompt 物件
*/
function getModulePrompts(module) {
try {
const saved = localStorage.getItem('aiPrompts');
const prompts = saved ? JSON.parse(saved) : {};
// 如果沒有保存的 prompts使用預設值
if (!prompts[module]) {
return DEFAULT_PROMPTS[module] || {};
}
return prompts[module];
} catch (e) {
console.error('讀取 prompts 失敗:', e);
return DEFAULT_PROMPTS[module] || {};
}
}
/**
* 保存模組的 prompts
* @param {string} module - 模組名稱
* @param {number} bagNumber - 錦囊編號 (1-3)
* @param {Object} bagData - 包含 title, subtitle, prompt 的物件
*/
function saveModulePrompt(module, bagNumber, bagData) {
try {
const saved = localStorage.getItem('aiPrompts');
const prompts = saved ? JSON.parse(saved) : {};
if (!prompts[module]) {
prompts[module] = {};
}
prompts[module][`bag${bagNumber}`] = bagData;
localStorage.setItem('aiPrompts', JSON.stringify(prompts));
return true;
} catch (e) {
console.error('保存 prompt 失敗:', e);
return false;
}
}
/**
* 初始化所有模組的預設 prompts如果尚未設定
*/
function initializeDefaultPrompts() {
const saved = localStorage.getItem('aiPrompts');
if (!saved) {
localStorage.setItem('aiPrompts', JSON.stringify(DEFAULT_PROMPTS));
}
}
// ==================== 執行 AI 錦囊 ====================
/**
* 執行 AI 錦囊
* @param {string} module - 模組名稱
* @param {number} bagNumber - 錦囊編號 (1-3)
*/
export async function executeAIBag(module, bagNumber) {
const bagElement = document.querySelector(`.ai-bag[data-module="${module}"][data-bag="${bagNumber}"]`);
if (!bagElement) return;
// 防止重複點擊
if (bagElement.classList.contains('loading')) return;
try {
// 顯示載入狀態
bagElement.classList.add('loading');
const icon = bagElement.querySelector('.bag-icon');
const originalIcon = icon.textContent;
icon.innerHTML = '<div class="spinner"></div>';
// 獲取當前表單資料
let existingData = {};
if (module === 'positionBasic' || module === 'positionRecruit') {
const positionData = getPositionFormData();
existingData = module === 'positionBasic' ? positionData.basicInfo : positionData.recruitInfo;
} else if (module === 'jobBasic') {
existingData = getJobFormData();
} else if (module === 'jobDesc') {
existingData = getJobDescFormData();
} else if (module === 'deptFunction') {
existingData = getDeptFunctionFormData();
}
// 獲取 prompt 模板
const prompts = getModulePrompts(module);
const bagPrompt = prompts[`bag${bagNumber}`];
if (!bagPrompt || !bagPrompt.prompt) {
throw new Error('Prompt 模板不存在');
}
// 替換 prompt 中的變數
let finalPrompt = bagPrompt.prompt
.replace('{existingData}', JSON.stringify(existingData, null, 2))
.replace('{positionName}', existingData.positionName || '此崗位');
// 調用 AI API
const result = await callClaudeAPI(finalPrompt);
// 填充表單
fillFormWithAIResult(module, result);
showToast('✨ AI 生成成功!');
} catch (error) {
console.error('AI 錦囊執行失敗:', error);
showToast('❌ AI 生成失敗: ' + error.message);
} finally {
// 恢復正常狀態
bagElement.classList.remove('loading');
const icon = bagElement.querySelector('.bag-icon');
icon.textContent = '🎁';
}
}
/**
* 根據 AI 結果填充表單
* @param {string} module - 模組名稱
* @param {Object} result - AI 返回的 JSON 結果
*/
function fillFormWithAIResult(module, result) {
if (module === 'positionBasic') {
// 崗位基礎資料 - 基礎資料頁籤
Object.entries(result).forEach(([key, value]) => {
fillIfEmpty(key, value);
});
} else if (module === 'positionRecruit') {
// 崗位基礎資料 - 招聘要求頁籤
Object.entries(result).forEach(([key, value]) => {
fillIfEmpty(key, value);
});
} else if (module === 'jobBasic') {
// 職務基礎資料
Object.entries(result).forEach(([key, value]) => {
if (key === 'hasAttendanceBonus' || key === 'hasHousingAllowance') {
const checkbox = document.getElementById(key);
if (checkbox) checkbox.checked = value;
} else {
fillIfEmpty(key, value);
}
});
} else if (module === 'deptFunction') {
// 部門職責
Object.entries(result).forEach(([key, value]) => {
fillIfEmpty('df_' + key, value);
});
} else if (module === 'jobDesc') {
// 崗位描述
if (result.basicInfo) {
Object.entries(result.basicInfo).forEach(([key, value]) => {
fillIfEmpty('jd_' + key, value);
});
}
if (result.positionInfo) {
Object.entries(result.positionInfo).forEach(([key, value]) => {
fillIfEmpty('jd_' + key, value);
});
}
if (result.responsibilities) {
Object.entries(result.responsibilities).forEach(([key, value]) => {
fillIfEmpty('jd_' + key, value);
});
}
if (result.requirements) {
Object.entries(result.requirements).forEach(([key, value]) => {
fillIfEmpty('jd_' + key, value);
});
}
}
}
// ==================== 編輯 Prompt ====================
/**
* 編輯錦囊 Prompt
* @param {Event} event - 點擊事件
* @param {string} module - 模組名稱
* @param {number} bagNumber - 錦囊編號 (1-3)
*/
export function editBagPrompt(event, module, bagNumber) {
event.stopPropagation(); // 防止觸發父元素的 click 事件
const prompts = getModulePrompts(module);
const bagPrompt = prompts[`bag${bagNumber}`];
if (!bagPrompt) {
showToast('❌ Prompt 不存在');
return;
}
// 顯示編輯對話框
showPromptEditModal(module, bagNumber, bagPrompt);
}
/**
* 顯示 Prompt 編輯對話框
* @param {string} module - 模組名稱
* @param {number} bagNumber - 錦囊編號 (1-3)
* @param {Object} bagData - 包含 title, subtitle, prompt 的物件
*/
function showPromptEditModal(module, bagNumber, bagData) {
const modal = document.getElementById('promptEditModal');
if (!modal) {
console.error('找不到編輯對話框');
return;
}
// 填充對話框內容
document.getElementById('promptModalTitle').textContent = `編輯錦囊 ${bagNumber} - ${bagData.title}`;
document.getElementById('promptTitle').value = bagData.title || '';
document.getElementById('promptSubtitle').value = bagData.subtitle || '';
document.getElementById('promptContent').value = bagData.prompt || '';
// 保存當前編輯的模組和錦囊編號
modal.dataset.module = module;
modal.dataset.bagNumber = bagNumber;
// 顯示對話框
modal.classList.add('show');
}
/**
* 保存編輯的 Prompt
*/
export function savePromptEdit() {
const modal = document.getElementById('promptEditModal');
const module = modal.dataset.module;
const bagNumber = parseInt(modal.dataset.bagNumber);
const title = document.getElementById('promptTitle').value.trim();
const subtitle = document.getElementById('promptSubtitle').value.trim();
const prompt = document.getElementById('promptContent').value.trim();
if (!title || !prompt) {
showToast('⚠️ 標題和 Prompt 內容不能為空');
return;
}
// 保存到 LocalStorage
const success = saveModulePrompt(module, bagNumber, { title, subtitle, prompt });
if (success) {
// 更新頁面上的錦囊標題
const bagElement = document.querySelector(`.ai-bag[data-module="${module}"][data-bag="${bagNumber}"]`);
if (bagElement) {
const titleElement = bagElement.querySelector('.bag-title');
const subtitleElement = bagElement.querySelector('.bag-subtitle');
if (titleElement) titleElement.textContent = title;
if (subtitleElement) {
if (subtitle) {
if (!subtitleElement.classList) {
const newSubtitle = document.createElement('div');
newSubtitle.className = 'bag-subtitle';
newSubtitle.textContent = subtitle;
bagElement.querySelector('.bag-title').after(newSubtitle);
} else {
subtitleElement.textContent = subtitle;
}
}
}
}
closePromptEditModal();
showToast('✅ Prompt 已保存');
} else {
showToast('❌ 保存失敗');
}
}
/**
* 關閉編輯對話框
*/
export function closePromptEditModal() {
const modal = document.getElementById('promptEditModal');
if (modal) {
modal.classList.remove('show');
}
}
/**
* 重置為預設 Prompt
*/
export function resetToDefaultPrompt() {
const modal = document.getElementById('promptEditModal');
const module = modal.dataset.module;
const bagNumber = parseInt(modal.dataset.bagNumber);
const defaultPrompt = DEFAULT_PROMPTS[module]?.[`bag${bagNumber}`];
if (!defaultPrompt) {
showToast('❌ 找不到預設 Prompt');
return;
}
if (confirm('確定要重置為預設 Prompt 嗎?')) {
document.getElementById('promptTitle').value = defaultPrompt.title || '';
document.getElementById('promptSubtitle').value = defaultPrompt.subtitle || '';
document.getElementById('promptContent').value = defaultPrompt.prompt || '';
showToast('✅ 已重置為預設值');
}
}
// ==================== 初始化 ====================
/**
* 初始化錦囊標題
*/
export function initializeBagTitles() {
document.querySelectorAll('.ai-bag').forEach(bag => {
const module = bag.dataset.module;
const bagNumber = parseInt(bag.dataset.bag);
const prompts = getModulePrompts(module);
const bagPrompt = prompts[`bag${bagNumber}`];
if (bagPrompt) {
const titleElement = bag.querySelector('.bag-title');
const subtitleElement = bag.querySelector('.bag-subtitle');
if (titleElement) titleElement.textContent = bagPrompt.title || `錦囊${bagNumber}`;
if (subtitleElement && bagPrompt.subtitle) {
subtitleElement.textContent = bagPrompt.subtitle;
}
}
});
}
// 頁面載入時初始化
document.addEventListener('DOMContentLoaded', () => {
initializeDefaultPrompts();
initializeBagTitles();
});
// ==================== 掛載到 window ====================
if (typeof window !== 'undefined') {
window.executeAIBag = executeAIBag;
window.editBagPrompt = editBagPrompt;
window.savePromptEdit = savePromptEdit;
window.closePromptEditModal = closePromptEditModal;
window.resetToDefaultPrompt = resetToDefaultPrompt;
}