| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086 |
- // 特征工程 JavaScript
- // 将API密钥存储在会话存储中
- let apiKey = sessionStorage.getItem('deepseekApiKey');
- let currentStep = parseInt(sessionStorage.getItem('featureEngCurrentStep')) || 1;
- let pipelineSteps = JSON.parse(sessionStorage.getItem('featureEngPipelineSteps')) || [];
- let currentOptions = JSON.parse(sessionStorage.getItem('featureEngCurrentOptions')) || [];
- let currentDataState = sessionStorage.getItem('featureEngCurrentDataState') || '原始数据';
- let conversationHistory = JSON.parse(sessionStorage.getItem('featureEngConversationHistory')) || [];
- let customSystemPrompt = sessionStorage.getItem('customSystemPrompt') || null;
- // DOM元素
- const apiKeyInput = document.getElementById('apiKey');
- const saveApiKeyBtn = document.getElementById('saveApiKey');
- const loadQuestionTemplateBtn = document.getElementById('loadQuestionTemplate');
- const editSystemPromptBtn = document.getElementById('editSystemPrompt');
- const questionTemplateInput = document.getElementById('questionTemplate');
- const startPipelineBtn = document.getElementById('startPipeline');
- const systemPromptModal = document.getElementById('systemPromptModal');
- const systemPromptTextarea = document.getElementById('systemPromptTextarea');
- const loadDefaultPromptBtn = document.getElementById('loadDefaultPrompt');
- const initialSetupSection = document.getElementById('initialSetup');
- const optionsSection = document.getElementById('optionsSection');
- const optionsContainer = document.getElementById('optionsContainer');
- const clearOptionsBtn = document.getElementById('clearOptions');
- const exportPipelineBtn = document.getElementById('exportPipeline');
- const pipelineStatus = document.getElementById('pipelineStatus');
- const pipelineStepsDiv = document.getElementById('pipelineSteps');
- const modalOverlay = document.getElementById('modalOverlay');
- const categoryPopup = document.getElementById('categoryPopup');
- const categoryPopupTitle = document.getElementById('categoryPopupTitle');
- const categoryPopupDescription = document.getElementById('categoryPopupDescription');
- const categoryPopupOperators = document.getElementById('categoryPopupOperators');
- const categoryPopupOperatorsTitle = document.getElementById('categoryPopupOperatorsTitle');
- // 如果存在API密钥则初始化
- if (apiKey) {
- apiKeyInput.value = apiKey;
- }
- // 页面加载时加载现有对话状态
- window.addEventListener('DOMContentLoaded', () => {
- console.log('正在加载对话状态...');
- console.log('对话历史:', conversationHistory);
- console.log('当前步骤:', currentStep);
- console.log('流水线步骤:', pipelineSteps);
- console.log('当前选项:', currentOptions);
- // 如果有对话历史,显示当前选项
- if (conversationHistory.length > 0 && currentOptions.length > 0) {
- console.log('正在恢复对话状态...');
- initialSetupSection.style.display = 'none';
- optionsSection.style.display = 'block';
- displayOptions();
- updatePipelineStatus();
- } else {
- // 确保从干净状态开始
- console.log('从干净状态开始...');
- initialSetupSection.style.display = 'block';
- optionsSection.style.display = 'none';
- }
- });
- // 点击遮罩层时关闭模态框
- modalOverlay.addEventListener('click', (e) => {
- if (e.target === modalOverlay) {
- modalOverlay.classList.remove('active');
- // 查找正在编辑的卡片并取消编辑
- const editingCard = document.querySelector('.option-card.editing');
- if (editingCard) {
- const index = parseInt(editingCard.dataset.optionIndex);
- cancelEdit(index);
- }
- }
- });
- // 保存API密钥并测试连接
- saveApiKeyBtn.addEventListener('click', async () => {
- const newApiKey = apiKeyInput.value.trim();
- if (!newApiKey) {
- showNotification('请输入有效的API密钥', 'error');
- return;
- }
- try {
- showLoading('正在测试API连接...');
- const response = await fetch('/feature-engineering/api/test-deepseek', {
- method: 'POST',
- headers: {
- 'X-API-Key': newApiKey,
- 'Content-Type': 'application/json'
- }
- });
- const data = await response.json();
- if (response.ok && data.success) {
- sessionStorage.setItem('deepseekApiKey', newApiKey);
- apiKey = newApiKey;
- showNotification('API连接成功', 'success');
- } else {
- showNotification(`API错误: ${data.error || '未知错误'}`, 'error');
- console.error('API错误详情:', data);
- }
- } catch (error) {
- showNotification('测试API连接时出错: ' + error.message, 'error');
- console.error('API测试错误:', error);
- } finally {
- hideLoading();
- }
- });
- // 加载问题模板
- loadQuestionTemplateBtn.addEventListener('click', () => {
- const template = `当前步骤: 0
- 当前数据字段: modify_your_input
- 当前数据字段描述: input_datafield_description
- 初始EDA观察: input_datafield_eda_observation
- 先前使用的步骤和类别: 无
- 当前数据状态: 这是第一步原始数据`;
- questionTemplateInput.value = template;
- showNotification('问题模板已加载', 'success');
- });
- // 编辑系统提示
- editSystemPromptBtn.addEventListener('click', () => {
- // 加载当前系统提示或默认提示
- if (customSystemPrompt) {
- systemPromptTextarea.value = customSystemPrompt;
- } else {
- loadDefaultSystemPrompt();
- }
- systemPromptModal.style.display = 'block';
- });
- // 加载默认系统提示
- loadDefaultPromptBtn.addEventListener('click', loadDefaultSystemPrompt);
- // 点击外部时隐藏类别弹出窗口
- document.addEventListener('click', (e) => {
- if (!categoryPopup.contains(e.target) && !e.target.classList.contains('clickable-category')) {
- hideCategoryPopup();
- }
- });
- async function loadDefaultSystemPrompt() {
- try {
- showLoading('正在加载默认系统提示...');
- const response = await fetch('/feature-engineering/api/get-default-system-prompt', {
- method: 'GET',
- headers: {
- 'Content-Type': 'application/json'
- }
- });
- const data = await response.json();
- if (response.ok && data.success) {
- systemPromptTextarea.value = data.default_system_prompt;
- showNotification('默认系统提示已从后端加载', 'success');
- } else {
- showNotification(`加载默认提示时出错: ${data.error || '未知错误'}`, 'error');
- console.error('加载默认提示时出错:', data);
- }
- } catch (error) {
- showNotification('加载默认系统提示时出错: ' + error.message, 'error');
- console.error('加载默认提示时出错:', error);
- } finally {
- hideLoading();
- }
- }
- // 关闭系统提示模态框
- function closeSystemPromptModal() {
- systemPromptModal.style.display = 'none';
- }
- // 保存系统提示
- function saveSystemPrompt() {
- const prompt = systemPromptTextarea.value.trim();
- if (!prompt) {
- showNotification('系统提示不能为空', 'error');
- return;
- }
- customSystemPrompt = prompt;
- sessionStorage.setItem('customSystemPrompt', prompt);
- systemPromptModal.style.display = 'none';
- showNotification('系统提示保存成功', 'success');
- }
- // 启动特征工程流水线
- startPipelineBtn.addEventListener('click', async () => {
- if (!apiKey) {
- showNotification('请先配置您的Deepseek API密钥', 'error');
- return;
- }
- const questionTemplate = questionTemplateInput.value.trim();
- if (!questionTemplate) {
- showNotification('请加载或输入问题模板', 'error');
- return;
- }
- try {
- showLoading('正在获取AI推荐...');
- console.log('=== 启动新流水线 ===');
- console.log('开始前的当前对话历史:', conversationHistory);
- console.log('对话历史长度:', conversationHistory.length);
- const response = await fetch('/feature-engineering/api/continue-conversation', {
- method: 'POST',
- headers: {
- 'X-API-Key': apiKey,
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- conversation_history: [],
- user_message: questionTemplate,
- custom_system_prompt: customSystemPrompt
- })
- });
- const data = await response.json();
- console.log('=== 初始提示 ===');
- console.log('用户消息:', questionTemplate);
- console.log('=== AI响应 ===');
- console.log('AI响应:', data.response);
- console.log('==================');
- if (response.ok && data.success) {
- // 清除对话历史并为新流水线重置状态
- conversationHistory = [];
- currentStep = 1;
- pipelineSteps = [];
- currentDataState = '原始数据';
- // 添加到对话历史
- conversationHistory.push({
- role: 'user',
- content: questionTemplate
- });
- conversationHistory.push({
- role: 'assistant',
- content: data.response
- });
- console.log('初始后的对话历史:', conversationHistory);
- console.log('对话历史长度:', conversationHistory.length);
- // 解析AI响应以提取选项
- parseAIResponse(data.response);
- // 保存对话状态
- saveConversationState();
- // 显示选项部分并隐藏初始设置
- initialSetupSection.style.display = 'none';
- optionsSection.style.display = 'block';
- updatePipelineStatus();
- showNotification('AI推荐加载成功', 'success');
- } else {
- showNotification(`错误: ${data.error || '未知错误'}`, 'error');
- console.error('API错误详情:', data);
- }
- } catch (error) {
- showNotification('获取推荐时出错: ' + error.message, 'error');
- console.error('流水线启动错误:', error);
- } finally {
- hideLoading();
- }
- });
- // 解析AI响应以提取选项
- function parseAIResponse(response) {
- console.log('=== 解析AI响应 ===');
- console.log('原始响应:', response);
- currentOptions = [];
- // 动态内容清理 - 移除各种摘要部分
- let cleanResponse = response;
- const summaryPatterns = [
- /### \*\*最佳选择\?\*\*[\s\S]*$/i,
- /### \*\*推荐下一步:\*\*[\s\S]*$/i,
- /最推荐的选择:[\s\S]*$/i,
- /理由:[\s\S]*$/i,
- /这保持了[\s\S]*$/i,
- /您想继续吗[\s\S]*$/i,
- /\*要创建的特征示例:\*[\s\S]*$/i
- ];
- summaryPatterns.forEach(pattern => {
- cleanResponse = cleanResponse.replace(pattern, '');
- });
- console.log('清理后的响应:', cleanResponse);
- // 动态提取顶级上下文
- let globalContext = null;
- const contextPatterns = [
- /\*\*上下文:\*\*\s*([\s\S]*?)(?=###|####|\*\*选项|\*\*选择|选项\s+\d+|$)/i,
- /上下文:\s*([\s\S]*?)(?=###|####|\*\*选项|\*\*选择|选项\s+\d+|$)/i
- ];
- for (const pattern of contextPatterns) {
- const match = cleanResponse.match(pattern);
- if (match) {
- globalContext = match[1].trim();
- console.log('找到全局上下文:', globalContext);
- break;
- }
- }
- // 动态选项模式匹配
- const optionPatterns = [
- /(?:####\s*)?(?:\*\*)?选项\s+(\d+)\s+用于\s+步骤\s+(\d+):?\*?\*?\s*([\s\S]*?)(?=(?:####\s*)?(?:\*\*)?选项\s+\d+\s+用于\s+步骤\s+\d+:|最推荐|理由:|这保持了|$)/gi,
- /(?:####\s*)?(?:\*\*)?option\s+(\d+)\s+for\s+Step\s+(\d+):\s*([\s\S]*?)(?=(?:####\s*)?(?:\*\*)?option\s+\d+\s+for\s+Step\s+\d+:|最推荐|理由:|这保持了|$)/gi
- ];
- let optionsFound = false;
- for (const optionPattern of optionPatterns) {
- let match;
- const tempOptions = [];
- while ((match = optionPattern.exec(cleanResponse)) !== null) {
- const optionNumber = match[1];
- const stepNumber = match[2];
- const content = match[3].trim();
- console.log(`找到步骤 ${stepNumber} 的选项 ${optionNumber}:`, content);
- const parsedOption = parseOptionContent(content, globalContext, parseInt(optionNumber), parseInt(stepNumber));
- if (parsedOption) {
- tempOptions.push(parsedOption);
- }
- }
- if (tempOptions.length > 0) {
- currentOptions = tempOptions;
- optionsFound = true;
- break;
- }
- // 为下一个模式重置regex lastIndex
- optionPattern.lastIndex = 0;
- }
- if (!optionsFound) {
- console.log('标准模式未找到选项,尝试备用解析...');
- // 备用方案:尝试查找任何编号选项
- const fallbackPattern = /(\d+)[.)]\s*([\s\S]*?)(?=\d+[.)]|$)/g;
- let match;
- while ((match = fallbackPattern.exec(cleanResponse)) !== null) {
- const optionNumber = match[1];
- const content = match[2].trim();
- console.log(`备用方案找到选项 ${optionNumber}:`, content);
- const parsedOption = parseOptionContent(content, globalContext, parseInt(optionNumber), currentStep);
- if (parsedOption) {
- currentOptions.push(parsedOption);
- }
- }
- }
- // 确保所有选项具有相同的上下文(如果需要从第一个复制)
- if (currentOptions.length > 0 && currentOptions[0].context) {
- const sharedContext = currentOptions[0].context;
- currentOptions.forEach(option => {
- if (!option.context || option.context.includes('同上')) {
- option.context = sharedContext;
- }
- });
- }
- console.log('解析的选项总数:', currentOptions.length);
- console.log('当前选项:', currentOptions);
- console.log('========================');
- displayOptions();
- // 保存当前选项状态
- saveConversationState();
- }
- // 辅助函数解析单个选项内容
- function parseOptionContent(content, globalContext, optionNumber, stepNumber) {
- console.log('=== 解析选项内容 ===');
- console.log('原始内容:', content);
- // 更精确的模式用于确切格式
- const contextPatterns = [
- /上下文:\s*([\s\S]*?)(?=\s+选择下一步:)/i,
- /\*\*上下文:\*\*\s*([\s\S]*?)(?=\s+\*\*选择下一步:\*\*)/i,
- /上下文:\s*([\s\S]*?)(?=\s+\*\*选择下一步:\*\*)/i
- ];
- // 多个模式用于下一步提取
- const nextStepPatterns = [
- /选择下一步:\s*([^\n\r]+?)(?=\s+理由:)/i,
- /\*\*选择下一步:\*\*\s*([^\n\r]+?)(?=\s+\*\*理由:\*\*)/i,
- /选择下一步:\s*([^\n\r]+?)(?=\s+\*\*理由:\*\*)/i
- ];
- // 多个模式用于理由提取
- const reasonPatterns = [
- /理由:\s*([\s\S]*?)(?=最推荐|理由:|这保持了|$)/i,
- /\*\*理由:\*\*\s*([\s\S]*?)(?=最推荐|理由:|这保持了|$)/i
- ];
- let contextMatch = null;
- let nextStepMatch = null;
- let reasonMatch = null;
- // 尝试上下文模式
- for (const pattern of contextPatterns) {
- contextMatch = content.match(pattern);
- if (contextMatch) {
- console.log('上下文模式匹配:', pattern);
- console.log('上下文匹配:', contextMatch[1].trim());
- break;
- }
- }
- // 尝试下一步模式
- for (const pattern of nextStepPatterns) {
- nextStepMatch = content.match(pattern);
- if (nextStepMatch) {
- console.log('下一步模式匹配:', pattern);
- console.log('下一步匹配:', nextStepMatch[1].trim());
- break;
- }
- }
- // 尝试理由模式
- for (const pattern of reasonPatterns) {
- reasonMatch = content.match(pattern);
- if (reasonMatch) {
- console.log('理由模式匹配:', pattern);
- console.log('理由匹配:', reasonMatch[1].trim());
- break;
- }
- }
- console.log('解析结果:', {
- contextMatch: contextMatch ? contextMatch[1].trim() : null,
- nextStepMatch: nextStepMatch ? nextStepMatch[1].trim() : null,
- reasonMatch: reasonMatch ? reasonMatch[1].trim() : null,
- globalContext: globalContext ? '可用' : '不可用'
- });
- // 确定要使用的上下文 - 优先使用单个选项上下文而非全局上下文
- let context = null;
- if (contextMatch) {
- context = contextMatch[1].trim().replace(/同上/gi, '').trim();
- console.log('使用单个选项上下文:', context);
- } else if (globalContext) {
- context = globalContext;
- console.log('使用全局上下文:', context);
- }
- if ((context || contextMatch) && nextStepMatch && reasonMatch) {
- const result = {
- optionNumber: optionNumber,
- stepNumber: stepNumber,
- context: context,
- nextStep: nextStepMatch[1].trim().replace(/\*\*/g, ''),
- reason: reasonMatch[1].trim(),
- originalContent: content
- };
- result.reason = "我在这一步使用了xxxxxxx操作符" + ",目的是\n" + result.reason;
- console.log('成功解析选项:', result);
- console.log('最终存储的上下文:', result.context);
- console.log('===============================');
- return result;
- } else {
- console.log('解析选项内容失败:', {
- hasContext: !!(context || contextMatch),
- hasNextStep: !!nextStepMatch,
- hasReason: !!reasonMatch
- });
- console.log('===============================');
- return null;
- }
- }
- // 将选项显示为卡片
- function displayOptions() {
- optionsContainer.innerHTML = '';
- currentOptions.forEach((option, index) => {
- const card = createOptionCard(option, index);
- optionsContainer.appendChild(card);
- });
- }
- // 创建选项卡片
- function createOptionCard(option, index) {
- console.log('=== 创建选项卡片 ===');
- console.log('选项索引:', index);
- console.log('显示的选项上下文:', option.context);
- console.log('选项下一步:', option.nextStep);
- console.log('选项理由:', option.reason);
- console.log('============================');
- const card = document.createElement('div');
- card.className = 'option-card';
- card.dataset.optionIndex = index;
- card.innerHTML = `
- <div class="option-header">
- <span class="option-number">选项 ${option.optionNumber}</span>
- <div class="option-actions">
- <button class="select-btn" onclick="selectAndEdit(${index})">选择并编辑</button>
- </div>
- </div>
- <div class="option-content">
- <div class="option-field readonly">
- <label>上下文:</label>
- <textarea readonly class="auto-resize-textarea">${option.context}</textarea>
- </div>
- <div class="option-field readonly">
- <label>下一步:</label>
- <input type="text" readonly value="${option.nextStep}" style="display: none;">
- <div class="readonly-display">
- <span class="clickable-category" onclick="showCategoryPopup('${option.nextStep.replace(/'/g, "\\'")}', event)">${option.nextStep}</span>
- </div>
- </div>
- <div class="option-field readonly">
- <label>理由:</label>
- <textarea readonly class="auto-resize-textarea">${option.reason}</textarea>
- </div>
- </div>
- `;
- // 创建卡片后自动调整文本区域大小
- setTimeout(() => {
- const textareas = card.querySelectorAll('.auto-resize-textarea');
- textareas.forEach(autoResizeTextarea);
- }, 0);
- return card;
- }
- // 自动调整文本区域大小函数
- function autoResizeTextarea(textarea) {
- textarea.style.height = 'auto';
- textarea.style.height = Math.max(textarea.scrollHeight, 60) + 'px';
- }
- // 选择并编辑选项
- function selectAndEdit(index) {
- const card = document.querySelector(`[data-option-index="${index}"]`);
- const fields = card.querySelectorAll('.option-field');
- // 移除只读类并使字段可编辑
- fields.forEach(field => {
- field.classList.remove('readonly');
- const input = field.querySelector('input, textarea');
- const readonlyDisplay = field.querySelector('.readonly-display');
- if (input) {
- input.removeAttribute('readonly');
- // 对于下一步字段,显示输入框并隐藏只读显示
- if (readonlyDisplay) {
- input.style.display = 'block';
- readonlyDisplay.style.display = 'none';
- }
- }
- // 为文本区域添加自动调整大小功能
- if (input && input.tagName === 'TEXTAREA') {
- input.addEventListener('input', () => autoResizeTextarea(input));
- autoResizeTextarea(input); // 初始调整大小
- }
- });
- // 更新卡片状态并显示模态遮罩层
- card.classList.add('editing');
- modalOverlay.classList.add('active');
- // 更新操作按钮
- const actionsDiv = card.querySelector('.option-actions');
- actionsDiv.innerHTML = `
- <button class="save-btn" onclick="saveOption(${index})">保存更改</button>
- <button class="cancel-btn" onclick="cancelEdit(${index})">取消</button>
- <button class="send-continue-btn" onclick="sendAndContinue(${index})">发送并继续</button>
- `;
- }
- // 保存选项
- function saveOption(index) {
- const card = document.querySelector(`[data-option-index="${index}"]`);
- const contextTextarea = card.querySelector('.option-field:nth-child(1) textarea');
- const nextStepInput = card.querySelector('.option-field:nth-child(2) input');
- const reasonTextarea = card.querySelector('.option-field:nth-child(3) textarea');
- // 更新选项数据
- currentOptions[index].context = contextTextarea.value;
- currentOptions[index].nextStep = nextStepInput.value;
- currentOptions[index].reason = reasonTextarea.value;
- // 保存更新的选项状态
- saveConversationState();
- // 再次使字段变为只读
- const fields = card.querySelectorAll('.option-field');
- fields.forEach(field => {
- field.classList.add('readonly');
- const input = field.querySelector('input, textarea');
- const readonlyDisplay = field.querySelector('.readonly-display');
- if (input) {
- input.setAttribute('readonly', 'readonly');
- // 对于下一步字段,隐藏输入框并显示只读显示
- if (readonlyDisplay) {
- input.style.display = 'none';
- readonlyDisplay.style.display = 'block';
- // 更新可点击类别文本
- const categorySpan = readonlyDisplay.querySelector('.clickable-category');
- if (categorySpan) {
- categorySpan.textContent = input.value;
- categorySpan.setAttribute('onclick', `showCategoryPopup('${input.value.replace(/'/g, "\\'")}', event)`);
- }
- }
- }
- });
- // 更新卡片状态并隐藏模态遮罩层
- card.classList.remove('editing');
- modalOverlay.classList.remove('active');
- // 更新操作按钮
- const actionsDiv = card.querySelector('.option-actions');
- actionsDiv.innerHTML = `
- <button class="select-btn" onclick="selectAndEdit(${index})">选择并编辑</button>
- `;
- showNotification('选项保存成功', 'success');
- }
- // 取消编辑
- function cancelEdit(index) {
- // 隐藏模态遮罩层
- modalOverlay.classList.remove('active');
- // 使用原始数据刷新卡片
- const card = createOptionCard(currentOptions[index], index);
- const oldCard = document.querySelector(`[data-option-index="${index}"]`);
- oldCard.parentNode.replaceChild(card, oldCard);
- }
- // 从类别获取数据状态
- function getDataStateFromCategory(category) {
- const stateMap = {
- '基础算术和数学运算': '数学变换',
- '逻辑和条件运算': '条件过滤',
- '时间序列:变化检测和值比较': '变化分析',
- '时间序列:统计特征工程': '统计工程',
- '时间序列:排名、缩放和归一化': '排名和归一化',
- '时间序列:衰减、平滑和周转控制': '平滑和控制',
- '时间序列:极值和位置识别': '极值识别',
- '横截面:排名、缩放和归一化': '横截面归一化',
- '横截面:回归和中性化': '中性化',
- '横截面:分布变换和截断': '分布变换',
- '变换和过滤操作': '变换和过滤',
- '分组聚合和统计摘要': '聚合',
- '分组排名、缩放和归一化': '分组归一化',
- '分组回归和中性化': '分组中性化',
- '分组插补和回填': '插补和回填'
- };
- return stateMap[category] || '已处理';
- }
- // 清除选项并重新开始
- clearOptionsBtn.addEventListener('click', () => {
- if (confirm('确定要清除所有进度并重新开始吗?')) {
- // 清除所有状态
- conversationHistory = [];
- currentStep = 1;
- pipelineSteps = [];
- currentOptions = [];
- currentDataState = '原始数据';
- // 清除会话存储
- sessionStorage.removeItem('featureEngConversationHistory');
- sessionStorage.removeItem('featureEngCurrentStep');
- sessionStorage.removeItem('featureEngPipelineSteps');
- sessionStorage.removeItem('featureEngCurrentOptions');
- sessionStorage.removeItem('featureEngCurrentDataState');
- // 重置UI
- optionsSection.style.display = 'none';
- initialSetupSection.style.display = 'block';
- questionTemplateInput.value = '';
- // 更新流水线状态以反映清除状态
- updatePipelineStatus();
- showNotification('流水线已清除。您可以开始新的对话。', 'success');
- }
- });
- // 导出流水线
- exportPipelineBtn.addEventListener('click', () => {
- const exportData = {
- timestamp: new Date().toISOString(),
- currentStep: currentStep,
- pipelineSteps: pipelineSteps,
- currentOptions: currentOptions,
- conversationHistory: conversationHistory
- };
- const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = `feature_engineering_pipeline_${new Date().toISOString().split('T')[0]}.json`;
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- URL.revokeObjectURL(url);
- showNotification('流水线导出成功', 'success');
- });
- // 发送编辑后的选项并继续
- function sendAndContinue(index) {
- const card = document.querySelector(`[data-option-index="${index}"]`);
- const contextTextarea = card.querySelector('.option-field:nth-child(1) textarea');
- const nextStepInput = card.querySelector('.option-field:nth-child(2) input');
- const reasonTextarea = card.querySelector('.option-field:nth-child(3) textarea');
- // 获取编辑后的值
- const context = contextTextarea.value;
- const chosenStep = nextStepInput.value;
- const reason = reasonTextarea.value;
- console.log('=== 发送并继续调试 ===');
- console.log('选中的选项索引:', index);
- console.log('当前选项:', currentOptions[index]);
- console.log('上下文:', context);
- console.log('选择的步骤:', chosenStep);
- console.log('理由:', reason);
- console.log('更新前的当前步骤:', currentStep);
- console.log('更新前的流水线步骤:', pipelineSteps);
- // 隐藏模态遮罩层
- modalOverlay.classList.remove('active');
- // 添加到流水线步骤 - 修复:使用currentStep而不是选项中的stepNumber
- pipelineSteps.push(`步骤 ${currentStep}: ${chosenStep}`);
- currentStep = currentStep + 1; // 从当前步骤递增
- currentDataState = getDataStateFromCategory(chosenStep);
- console.log('更新后的当前步骤:', currentStep);
- console.log('更新后的流水线步骤:', pipelineSteps);
- console.log('当前数据状态:', currentDataState);
- // 更新流水线状态
- updatePipelineStatus();
- // 保存流水线状态
- saveConversationState();
- // 为AI系统提示准备适当格式的消息
- // 构建先前步骤列表
- const previousStepsText = pipelineSteps.length > 0 ? pipelineSteps.join(', ') : '无';
- // 获取所选步骤的类别描述
- const categoryData = operatorsData.find(cat => cat.name === chosenStep);
- const stepDescription = categoryData ? categoryData.description : '无描述可用';
- const userMessage = `
- 我选择的下一步: ${chosenStep}
- 步骤描述: ${stepDescription}
- 选择理由: ${reason}
- 基于我的选择和信息,请推荐一些进一步的选项`;
- console.log('=== 为AI构建的消息 ===');
- console.log('发送的用户消息:', userMessage);
- console.log('当前步骤:', currentStep);
- console.log('先前步骤:', previousStepsText);
- console.log('当前数据状态:', currentDataState);
- console.log('步骤描述:', stepDescription);
- console.log('选择的下一步:', chosenStep);
- console.log('选择理由:', reason);
- console.log('=================================');
- // 获取下一个推荐
- showLoading('正在获取下一个推荐...');
- fetch('/feature-engineering/api/continue-conversation', {
- method: 'POST',
- headers: {
- 'X-API-Key': apiKey,
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- conversation_history: conversationHistory,
- user_message: userMessage,
- custom_system_prompt: customSystemPrompt
- })
- })
- .then(response => response.json())
- .then(data => {
- console.log('=== 发送并继续提示 ===');
- console.log('用户消息:', userMessage);
- console.log('之前的当前步骤:', currentStep);
- console.log('之前的流水线步骤:', pipelineSteps);
- console.log('发送的对话历史:', conversationHistory);
- console.log('=== AI响应 ===');
- console.log('AI响应:', data.response);
- console.log('==================');
- if (data.success) {
- // 添加到对话历史
- conversationHistory.push({
- role: 'user',
- content: userMessage
- });
- conversationHistory.push({
- role: 'assistant',
- content: data.response
- });
- console.log('更新后的对话历史:', conversationHistory);
- // 解析新的AI响应
- parseAIResponse(data.response);
- // 保存对话状态
- saveConversationState();
- showNotification(`编辑的选项发送成功。下一步推荐已加载。`, 'success');
- } else {
- showNotification(`错误: ${data.error || '未知错误'}`, 'error');
- console.error('API错误详情:', data);
- }
- })
- .catch(error => {
- showNotification('获取下一个推荐时出错: ' + error.message, 'error');
- console.error('下一步错误:', error);
- })
- .finally(() => {
- hideLoading();
- });
- }
- // 使函数在onclick处理程序中全局可用
- window.selectAndEdit = selectAndEdit;
- window.saveOption = saveOption;
- window.cancelEdit = cancelEdit;
- window.sendAndContinue = sendAndContinue;
- window.closeSystemPromptModal = closeSystemPromptModal;
- window.saveSystemPrompt = saveSystemPrompt;
- // 更新流水线状态
- function updatePipelineStatus() {
- console.log('=== 更新流水线状态 ===');
- console.log('流水线步骤:', pipelineSteps);
- console.log('当前步骤:', currentStep);
- console.log('当前数据状态:', currentDataState);
- if (pipelineSteps.length === 0) {
- pipelineStatus.style.display = 'none';
- return;
- }
- pipelineStatus.style.display = 'block';
- pipelineStepsDiv.innerHTML = pipelineSteps.map(step =>
- `<div class="pipeline-step"><strong>${step}</strong></div>`
- ).join('');
- // 添加当前状态
- const statusDiv = document.createElement('div');
- statusDiv.className = 'pipeline-step';
- statusDiv.style.backgroundColor = '#e8f5e8';
- statusDiv.innerHTML = `<strong>当前步骤:</strong> ${currentStep} | <strong>数据状态:</strong> ${currentDataState}`;
- pipelineStepsDiv.appendChild(statusDiv);
- console.log('流水线状态已更新');
- console.log('==============================');
- }
- // 工具函数
- function showNotification(message, type) {
- const notification = document.createElement('div');
- notification.className = `notification ${type}`;
- notification.textContent = message;
- document.body.appendChild(notification);
- setTimeout(() => {
- notification.remove();
- }, 8000);
- }
- let loadingElement = null;
- function showLoading(message) {
- loadingElement = document.createElement('div');
- loadingElement.className = 'loading-overlay';
- loadingElement.innerHTML = `
- <div class="loading-spinner"></div>
- <div class="loading-message">${message}</div>
- `;
- document.body.appendChild(loadingElement);
- }
- function hideLoading() {
- if (loadingElement) {
- loadingElement.remove();
- loadingElement = null;
- }
- }
- // 操作符参考数据
- const operatorsData = [
- {
- id: 1,
- name: "基础算术和数学运算",
- description: "核心数学和逐元素运算(例如,加、减、乘、对数、指数、绝对值、幂等)",
- operators: ["add", "subtract", "multiply", "divide", "exp", "log", "abs", "power", "sqrt", "round", "round_down", "floor", "ceiling", "inverse", "negate", "signed_power", "sign", "arc_sin", "arc_cos", "arc_tan", "tanh", "sigmoid", "s_log_1p", "fraction", "max", "min", "densify", "pasteurize", "purify", "to_nan", "nan_out", "replace", "reverse"]
- },
- {
- id: 2,
- name: "逻辑和条件运算",
- description: "布尔逻辑、比较和条件分支(例如,与、或、非、如果否则、等于、大于、小于等)",
- operators: ["and", "or", "not", "if_else", "equal", "not_equal", "less", "less_equal", "greater", "greater_equal", "is_nan", "is_not_nan", "is_finite", "is_not_finite", "nan_mask"]
- },
- {
- id: 3,
- name: "时间序列:变化检测和值比较",
- description: "比较随时间变化的值,计算差异,检测变化,或计算自上次变化以来的天数(例如,ts_delta、ts_returns、days_from_last_change、last_diff_value等)",
- operators: ["ts_delta", "ts_returns", "days_from_last_change", "last_diff_value", "ts_delta_limit", "ts_backfill"]
- },
- {
- id: 4,
- name: "时间序列:统计特征工程",
- description: "计算随时间滚动的统计属性(例如,ts_mean、ts_std_dev、ts_skewness、ts_kurtosis、ts_entropy、ts_moment、ts_covariance、ts_corr、ts_co_skewness、ts_co_kurtosis等)",
- operators: ["ts_mean", "ts_std_dev", "ts_skewness", "ts_kurtosis", "ts_entropy", "ts_moment", "ts_covariance", "ts_corr", "ts_partial_corr", "ts_triple_corr", "ts_ir", "ts_sum", "ts_product", "ts_median", "ts_count_nans", "ts_av_diff", "ts_regression", "ts_poly_regression", "ts_vector_neut", "ts_vector_proj", "ts_co_skewness", "ts_co_kurtosis", "ts_theilsen", "ts_zscore", "ts_rank_gmean_amean_diff", "ts_step", "ts_delay", "inst_tvr", "generate_stats"]
- },
- {
- id: 5,
- name: "时间序列:排名、缩放和归一化",
- description: "在滚动窗口内对时间序列数据进行排名、缩放或归一化(例如,ts_rank、ts_scale、ts_percentage、ts_quantile等)",
- operators: ["ts_rank", "ts_scale", "ts_percentage", "ts_quantile", "ts_rank_gmean_amean_diff", "ts_zscore"]
- },
- {
- id: 6,
- name: "时间序列:衰减、平滑和周转控制",
- description: "在时间序列中应用衰减(线性、指数、加权)、平滑或控制周转(例如,ts_decay_exp_window、ts_decay_linear、ts_weighted_decay、ts_target_tvr_decay、hump、jump_decay等)",
- operators: ["ts_decay_exp_window", "ts_decay_linear", "ts_weighted_decay", "ts_target_tvr_decay", "hump", "jump_decay", "ts_target_tvr_delta_limit", "ts_target_tvr_hump", "hump_decay"]
- },
- {
- id: 7,
- name: "时间序列:极值和位置识别",
- description: "识别最小/最大值、它们的差异或窗口内极值的位置(索引)(例如,ts_min、ts_max、ts_min_diff、ts_max_diff、ts_arg_min、ts_arg_max、ts_min_max_diff等)",
- operators: ["ts_min", "ts_max", "ts_min_diff", "ts_max_diff", "ts_arg_min", "ts_arg_max", "ts_min_max_diff", "ts_min_max_cps", "kth_element"]
- },
- {
- id: 8,
- name: "横截面:排名、缩放和归一化",
- description: "在单个时间点跨工具对数据进行排名、缩放、归一化或标准化(例如,rank、zscore、scale_down、normalize、rank_by_side等)",
- operators: ["rank", "zscore", "scale_down", "scale", "normalize", "rank_by_side", "generalized_rank", "one_side", "rank_gmean_amean_diff"]
- },
- {
- id: 9,
- name: "横截面:回归和中性化",
- description: "移除其他变量的影响,执行横截面回归,或将一个向量相对于另一个向量正交化(例如,regression_neut、vector_neut、regression_proj、vector_proj、multi_regression等)",
- operators: ["regression_neut", "vector_neut", "regression_proj", "vector_proj", "multi_regression"]
- },
- {
- id: 10,
- name: "横截面:分布变换和截断",
- description: "跨工具变换分布或截断异常值(例如,quantile、winsorize、truncate、bucket、generalized_rank等)",
- operators: ["quantile", "winsorize", "truncate", "bucket", "right_tail", "left_tail", "tail"]
- },
- {
- id: 11,
- name: "变换和过滤操作",
- description: "通用数据变换、过滤、钳制、掩码或条件值分配(例如,filter、clamp、keep、tail、left_tail、right_tail、trade_when等)",
- operators: ["filter", "clamp", "keep", "tail", "left_tail", "right_tail", "trade_when"]
- },
- {
- id: 12,
- name: "分组聚合和统计摘要",
- description: "在每个组内(如行业、部门、国家)聚合或摘要(例如,均值、总和、标准差、最小值、最大值、中位数)。每个股票根据其组成员资格接收组级值。",
- operators: ["group_mean", "group_sum", "group_std_dev", "group_min", "group_max", "group_median", "group_count", "group_percentage", "group_extra"]
- },
- {
- id: 13,
- name: "分组排名、缩放和归一化",
- description: "在每个组内进行排名、缩放或归一化(例如,每个股票的行业排名,在部门内缩放值)。每个股票在其组内同行中进行排名或缩放。",
- operators: ["group_rank", "group_scale", "group_zscore", "group_normalize"]
- },
- {
- id: 14,
- name: "分组回归和中性化",
- description: "移除组级影响,在每个组内执行回归或正交化(例如,行业中性化,分组回归)。每个组独立处理。",
- operators: ["group_vector_neut", "group_vector_proj", "group_neutralize", "group_multi_regression"]
- },
- {
- id: 15,
- name: "分组插补和回填",
- description: "使用同一组中其他股票的数据插补缺失值或回填(例如,用组均值或中位数填充NaN,group_backfill)。",
- operators: ["group_backfill"]
- }
- ];
- // 显示类别弹出窗口
- function showCategoryPopup(categoryName, event) {
- event.stopPropagation();
- // 查找类别数据
- const categoryData = operatorsData.find(cat => cat.name === categoryName);
- if (!categoryData) {
- console.log('未找到类别:', categoryName);
- return;
- }
- // 填充弹出窗口内容
- categoryPopupTitle.textContent = categoryData.name;
- categoryPopupDescription.textContent = categoryData.description;
- categoryPopupOperatorsTitle.textContent = `可用操作符 (${categoryData.operators.length}):`;
- const operatorsHtml = categoryData.operators.map(op =>
- `<span class="popup-operator-tag">${op}</span>`
- ).join('');
- categoryPopupOperators.innerHTML = operatorsHtml;
- // 将弹出窗口定位在点击元素附近
- const rect = event.target.getBoundingClientRect();
- const popup = categoryPopup;
- popup.style.display = 'block';
- // 计算位置
- let left = rect.left + window.scrollX;
- let top = rect.bottom + window.scrollY + 5;
- // 如果弹出窗口会超出屏幕则调整
- const popupRect = popup.getBoundingClientRect();
- if (left + popupRect.width > window.innerWidth) {
- left = window.innerWidth - popupRect.width - 20;
- }
- if (top + popupRect.height > window.innerHeight + window.scrollY) {
- top = rect.top + window.scrollY - popupRect.height - 5;
- }
- popup.style.left = left + 'px';
- popup.style.top = top + 'px';
- }
- // 隐藏类别弹出窗口
- function hideCategoryPopup() {
- categoryPopup.style.display = 'none';
- }
- // 使函数在onclick处理程序中全局可用
- window.showCategoryPopup = showCategoryPopup;
- window.hideCategoryPopup = hideCategoryPopup;
- // 保存对话状态的函数
- function saveConversationState() {
- sessionStorage.setItem('featureEngConversationHistory', JSON.stringify(conversationHistory));
- sessionStorage.setItem('featureEngCurrentStep', currentStep.toString());
- sessionStorage.setItem('featureEngPipelineSteps', JSON.stringify(pipelineSteps));
- sessionStorage.setItem('featureEngCurrentOptions', JSON.stringify(currentOptions));
- sessionStorage.setItem('featureEngCurrentDataState', currentDataState);
- console.log('对话状态已保存到sessionStorage');
- }
|