/** * 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 = '
'; // 獲取當前表單資料 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; }