You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
wqb-server/static/feature_engineering.js

1086 lines
42 KiB

// 特征工程 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');
}