feature_engineering.js 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086
  1. // 特征工程 JavaScript
  2. // 将API密钥存储在会话存储中
  3. let apiKey = sessionStorage.getItem('deepseekApiKey');
  4. let currentStep = parseInt(sessionStorage.getItem('featureEngCurrentStep')) || 1;
  5. let pipelineSteps = JSON.parse(sessionStorage.getItem('featureEngPipelineSteps')) || [];
  6. let currentOptions = JSON.parse(sessionStorage.getItem('featureEngCurrentOptions')) || [];
  7. let currentDataState = sessionStorage.getItem('featureEngCurrentDataState') || '原始数据';
  8. let conversationHistory = JSON.parse(sessionStorage.getItem('featureEngConversationHistory')) || [];
  9. let customSystemPrompt = sessionStorage.getItem('customSystemPrompt') || null;
  10. // DOM元素
  11. const apiKeyInput = document.getElementById('apiKey');
  12. const saveApiKeyBtn = document.getElementById('saveApiKey');
  13. const loadQuestionTemplateBtn = document.getElementById('loadQuestionTemplate');
  14. const editSystemPromptBtn = document.getElementById('editSystemPrompt');
  15. const questionTemplateInput = document.getElementById('questionTemplate');
  16. const startPipelineBtn = document.getElementById('startPipeline');
  17. const systemPromptModal = document.getElementById('systemPromptModal');
  18. const systemPromptTextarea = document.getElementById('systemPromptTextarea');
  19. const loadDefaultPromptBtn = document.getElementById('loadDefaultPrompt');
  20. const initialSetupSection = document.getElementById('initialSetup');
  21. const optionsSection = document.getElementById('optionsSection');
  22. const optionsContainer = document.getElementById('optionsContainer');
  23. const clearOptionsBtn = document.getElementById('clearOptions');
  24. const exportPipelineBtn = document.getElementById('exportPipeline');
  25. const pipelineStatus = document.getElementById('pipelineStatus');
  26. const pipelineStepsDiv = document.getElementById('pipelineSteps');
  27. const modalOverlay = document.getElementById('modalOverlay');
  28. const categoryPopup = document.getElementById('categoryPopup');
  29. const categoryPopupTitle = document.getElementById('categoryPopupTitle');
  30. const categoryPopupDescription = document.getElementById('categoryPopupDescription');
  31. const categoryPopupOperators = document.getElementById('categoryPopupOperators');
  32. const categoryPopupOperatorsTitle = document.getElementById('categoryPopupOperatorsTitle');
  33. // 如果存在API密钥则初始化
  34. if (apiKey) {
  35. apiKeyInput.value = apiKey;
  36. }
  37. // 页面加载时加载现有对话状态
  38. window.addEventListener('DOMContentLoaded', () => {
  39. console.log('正在加载对话状态...');
  40. console.log('对话历史:', conversationHistory);
  41. console.log('当前步骤:', currentStep);
  42. console.log('流水线步骤:', pipelineSteps);
  43. console.log('当前选项:', currentOptions);
  44. // 如果有对话历史,显示当前选项
  45. if (conversationHistory.length > 0 && currentOptions.length > 0) {
  46. console.log('正在恢复对话状态...');
  47. initialSetupSection.style.display = 'none';
  48. optionsSection.style.display = 'block';
  49. displayOptions();
  50. updatePipelineStatus();
  51. } else {
  52. // 确保从干净状态开始
  53. console.log('从干净状态开始...');
  54. initialSetupSection.style.display = 'block';
  55. optionsSection.style.display = 'none';
  56. }
  57. });
  58. // 点击遮罩层时关闭模态框
  59. modalOverlay.addEventListener('click', (e) => {
  60. if (e.target === modalOverlay) {
  61. modalOverlay.classList.remove('active');
  62. // 查找正在编辑的卡片并取消编辑
  63. const editingCard = document.querySelector('.option-card.editing');
  64. if (editingCard) {
  65. const index = parseInt(editingCard.dataset.optionIndex);
  66. cancelEdit(index);
  67. }
  68. }
  69. });
  70. // 保存API密钥并测试连接
  71. saveApiKeyBtn.addEventListener('click', async () => {
  72. const newApiKey = apiKeyInput.value.trim();
  73. if (!newApiKey) {
  74. showNotification('请输入有效的API密钥', 'error');
  75. return;
  76. }
  77. try {
  78. showLoading('正在测试API连接...');
  79. const response = await fetch('/feature-engineering/api/test-deepseek', {
  80. method: 'POST',
  81. headers: {
  82. 'X-API-Key': newApiKey,
  83. 'Content-Type': 'application/json'
  84. }
  85. });
  86. const data = await response.json();
  87. if (response.ok && data.success) {
  88. sessionStorage.setItem('deepseekApiKey', newApiKey);
  89. apiKey = newApiKey;
  90. showNotification('API连接成功', 'success');
  91. } else {
  92. showNotification(`API错误: ${data.error || '未知错误'}`, 'error');
  93. console.error('API错误详情:', data);
  94. }
  95. } catch (error) {
  96. showNotification('测试API连接时出错: ' + error.message, 'error');
  97. console.error('API测试错误:', error);
  98. } finally {
  99. hideLoading();
  100. }
  101. });
  102. // 加载问题模板
  103. loadQuestionTemplateBtn.addEventListener('click', () => {
  104. const template = `当前步骤: 0
  105. 当前数据字段: modify_your_input
  106. 当前数据字段描述: input_datafield_description
  107. 初始EDA观察: input_datafield_eda_observation
  108. 先前使用的步骤和类别: 无
  109. 当前数据状态: 这是第一步原始数据`;
  110. questionTemplateInput.value = template;
  111. showNotification('问题模板已加载', 'success');
  112. });
  113. // 编辑系统提示
  114. editSystemPromptBtn.addEventListener('click', () => {
  115. // 加载当前系统提示或默认提示
  116. if (customSystemPrompt) {
  117. systemPromptTextarea.value = customSystemPrompt;
  118. } else {
  119. loadDefaultSystemPrompt();
  120. }
  121. systemPromptModal.style.display = 'block';
  122. });
  123. // 加载默认系统提示
  124. loadDefaultPromptBtn.addEventListener('click', loadDefaultSystemPrompt);
  125. // 点击外部时隐藏类别弹出窗口
  126. document.addEventListener('click', (e) => {
  127. if (!categoryPopup.contains(e.target) && !e.target.classList.contains('clickable-category')) {
  128. hideCategoryPopup();
  129. }
  130. });
  131. async function loadDefaultSystemPrompt() {
  132. try {
  133. showLoading('正在加载默认系统提示...');
  134. const response = await fetch('/feature-engineering/api/get-default-system-prompt', {
  135. method: 'GET',
  136. headers: {
  137. 'Content-Type': 'application/json'
  138. }
  139. });
  140. const data = await response.json();
  141. if (response.ok && data.success) {
  142. systemPromptTextarea.value = data.default_system_prompt;
  143. showNotification('默认系统提示已从后端加载', 'success');
  144. } else {
  145. showNotification(`加载默认提示时出错: ${data.error || '未知错误'}`, 'error');
  146. console.error('加载默认提示时出错:', data);
  147. }
  148. } catch (error) {
  149. showNotification('加载默认系统提示时出错: ' + error.message, 'error');
  150. console.error('加载默认提示时出错:', error);
  151. } finally {
  152. hideLoading();
  153. }
  154. }
  155. // 关闭系统提示模态框
  156. function closeSystemPromptModal() {
  157. systemPromptModal.style.display = 'none';
  158. }
  159. // 保存系统提示
  160. function saveSystemPrompt() {
  161. const prompt = systemPromptTextarea.value.trim();
  162. if (!prompt) {
  163. showNotification('系统提示不能为空', 'error');
  164. return;
  165. }
  166. customSystemPrompt = prompt;
  167. sessionStorage.setItem('customSystemPrompt', prompt);
  168. systemPromptModal.style.display = 'none';
  169. showNotification('系统提示保存成功', 'success');
  170. }
  171. // 启动特征工程流水线
  172. startPipelineBtn.addEventListener('click', async () => {
  173. if (!apiKey) {
  174. showNotification('请先配置您的Deepseek API密钥', 'error');
  175. return;
  176. }
  177. const questionTemplate = questionTemplateInput.value.trim();
  178. if (!questionTemplate) {
  179. showNotification('请加载或输入问题模板', 'error');
  180. return;
  181. }
  182. try {
  183. showLoading('正在获取AI推荐...');
  184. console.log('=== 启动新流水线 ===');
  185. console.log('开始前的当前对话历史:', conversationHistory);
  186. console.log('对话历史长度:', conversationHistory.length);
  187. const response = await fetch('/feature-engineering/api/continue-conversation', {
  188. method: 'POST',
  189. headers: {
  190. 'X-API-Key': apiKey,
  191. 'Content-Type': 'application/json'
  192. },
  193. body: JSON.stringify({
  194. conversation_history: [],
  195. user_message: questionTemplate,
  196. custom_system_prompt: customSystemPrompt
  197. })
  198. });
  199. const data = await response.json();
  200. console.log('=== 初始提示 ===');
  201. console.log('用户消息:', questionTemplate);
  202. console.log('=== AI响应 ===');
  203. console.log('AI响应:', data.response);
  204. console.log('==================');
  205. if (response.ok && data.success) {
  206. // 清除对话历史并为新流水线重置状态
  207. conversationHistory = [];
  208. currentStep = 1;
  209. pipelineSteps = [];
  210. currentDataState = '原始数据';
  211. // 添加到对话历史
  212. conversationHistory.push({
  213. role: 'user',
  214. content: questionTemplate
  215. });
  216. conversationHistory.push({
  217. role: 'assistant',
  218. content: data.response
  219. });
  220. console.log('初始后的对话历史:', conversationHistory);
  221. console.log('对话历史长度:', conversationHistory.length);
  222. // 解析AI响应以提取选项
  223. parseAIResponse(data.response);
  224. // 保存对话状态
  225. saveConversationState();
  226. // 显示选项部分并隐藏初始设置
  227. initialSetupSection.style.display = 'none';
  228. optionsSection.style.display = 'block';
  229. updatePipelineStatus();
  230. showNotification('AI推荐加载成功', 'success');
  231. } else {
  232. showNotification(`错误: ${data.error || '未知错误'}`, 'error');
  233. console.error('API错误详情:', data);
  234. }
  235. } catch (error) {
  236. showNotification('获取推荐时出错: ' + error.message, 'error');
  237. console.error('流水线启动错误:', error);
  238. } finally {
  239. hideLoading();
  240. }
  241. });
  242. // 解析AI响应以提取选项
  243. function parseAIResponse(response) {
  244. console.log('=== 解析AI响应 ===');
  245. console.log('原始响应:', response);
  246. currentOptions = [];
  247. // 动态内容清理 - 移除各种摘要部分
  248. let cleanResponse = response;
  249. const summaryPatterns = [
  250. /### \*\*最佳选择\?\*\*[\s\S]*$/i,
  251. /### \*\*推荐下一步:\*\*[\s\S]*$/i,
  252. /最推荐的选择:[\s\S]*$/i,
  253. /理由:[\s\S]*$/i,
  254. /这保持了[\s\S]*$/i,
  255. /您想继续吗[\s\S]*$/i,
  256. /\*要创建的特征示例:\*[\s\S]*$/i
  257. ];
  258. summaryPatterns.forEach(pattern => {
  259. cleanResponse = cleanResponse.replace(pattern, '');
  260. });
  261. console.log('清理后的响应:', cleanResponse);
  262. // 动态提取顶级上下文
  263. let globalContext = null;
  264. const contextPatterns = [
  265. /\*\*上下文:\*\*\s*([\s\S]*?)(?=###|####|\*\*选项|\*\*选择|选项\s+\d+|$)/i,
  266. /上下文:\s*([\s\S]*?)(?=###|####|\*\*选项|\*\*选择|选项\s+\d+|$)/i
  267. ];
  268. for (const pattern of contextPatterns) {
  269. const match = cleanResponse.match(pattern);
  270. if (match) {
  271. globalContext = match[1].trim();
  272. console.log('找到全局上下文:', globalContext);
  273. break;
  274. }
  275. }
  276. // 动态选项模式匹配
  277. const optionPatterns = [
  278. /(?:####\s*)?(?:\*\*)?选项\s+(\d+)\s+用于\s+步骤\s+(\d+):?\*?\*?\s*([\s\S]*?)(?=(?:####\s*)?(?:\*\*)?选项\s+\d+\s+用于\s+步骤\s+\d+:|最推荐|理由:|这保持了|$)/gi,
  279. /(?:####\s*)?(?:\*\*)?option\s+(\d+)\s+for\s+Step\s+(\d+):\s*([\s\S]*?)(?=(?:####\s*)?(?:\*\*)?option\s+\d+\s+for\s+Step\s+\d+:|最推荐|理由:|这保持了|$)/gi
  280. ];
  281. let optionsFound = false;
  282. for (const optionPattern of optionPatterns) {
  283. let match;
  284. const tempOptions = [];
  285. while ((match = optionPattern.exec(cleanResponse)) !== null) {
  286. const optionNumber = match[1];
  287. const stepNumber = match[2];
  288. const content = match[3].trim();
  289. console.log(`找到步骤 ${stepNumber} 的选项 ${optionNumber}:`, content);
  290. const parsedOption = parseOptionContent(content, globalContext, parseInt(optionNumber), parseInt(stepNumber));
  291. if (parsedOption) {
  292. tempOptions.push(parsedOption);
  293. }
  294. }
  295. if (tempOptions.length > 0) {
  296. currentOptions = tempOptions;
  297. optionsFound = true;
  298. break;
  299. }
  300. // 为下一个模式重置regex lastIndex
  301. optionPattern.lastIndex = 0;
  302. }
  303. if (!optionsFound) {
  304. console.log('标准模式未找到选项,尝试备用解析...');
  305. // 备用方案:尝试查找任何编号选项
  306. const fallbackPattern = /(\d+)[.)]\s*([\s\S]*?)(?=\d+[.)]|$)/g;
  307. let match;
  308. while ((match = fallbackPattern.exec(cleanResponse)) !== null) {
  309. const optionNumber = match[1];
  310. const content = match[2].trim();
  311. console.log(`备用方案找到选项 ${optionNumber}:`, content);
  312. const parsedOption = parseOptionContent(content, globalContext, parseInt(optionNumber), currentStep);
  313. if (parsedOption) {
  314. currentOptions.push(parsedOption);
  315. }
  316. }
  317. }
  318. // 确保所有选项具有相同的上下文(如果需要从第一个复制)
  319. if (currentOptions.length > 0 && currentOptions[0].context) {
  320. const sharedContext = currentOptions[0].context;
  321. currentOptions.forEach(option => {
  322. if (!option.context || option.context.includes('同上')) {
  323. option.context = sharedContext;
  324. }
  325. });
  326. }
  327. console.log('解析的选项总数:', currentOptions.length);
  328. console.log('当前选项:', currentOptions);
  329. console.log('========================');
  330. displayOptions();
  331. // 保存当前选项状态
  332. saveConversationState();
  333. }
  334. // 辅助函数解析单个选项内容
  335. function parseOptionContent(content, globalContext, optionNumber, stepNumber) {
  336. console.log('=== 解析选项内容 ===');
  337. console.log('原始内容:', content);
  338. // 更精确的模式用于确切格式
  339. const contextPatterns = [
  340. /上下文:\s*([\s\S]*?)(?=\s+选择下一步:)/i,
  341. /\*\*上下文:\*\*\s*([\s\S]*?)(?=\s+\*\*选择下一步:\*\*)/i,
  342. /上下文:\s*([\s\S]*?)(?=\s+\*\*选择下一步:\*\*)/i
  343. ];
  344. // 多个模式用于下一步提取
  345. const nextStepPatterns = [
  346. /选择下一步:\s*([^\n\r]+?)(?=\s+理由:)/i,
  347. /\*\*选择下一步:\*\*\s*([^\n\r]+?)(?=\s+\*\*理由:\*\*)/i,
  348. /选择下一步:\s*([^\n\r]+?)(?=\s+\*\*理由:\*\*)/i
  349. ];
  350. // 多个模式用于理由提取
  351. const reasonPatterns = [
  352. /理由:\s*([\s\S]*?)(?=最推荐|理由:|这保持了|$)/i,
  353. /\*\*理由:\*\*\s*([\s\S]*?)(?=最推荐|理由:|这保持了|$)/i
  354. ];
  355. let contextMatch = null;
  356. let nextStepMatch = null;
  357. let reasonMatch = null;
  358. // 尝试上下文模式
  359. for (const pattern of contextPatterns) {
  360. contextMatch = content.match(pattern);
  361. if (contextMatch) {
  362. console.log('上下文模式匹配:', pattern);
  363. console.log('上下文匹配:', contextMatch[1].trim());
  364. break;
  365. }
  366. }
  367. // 尝试下一步模式
  368. for (const pattern of nextStepPatterns) {
  369. nextStepMatch = content.match(pattern);
  370. if (nextStepMatch) {
  371. console.log('下一步模式匹配:', pattern);
  372. console.log('下一步匹配:', nextStepMatch[1].trim());
  373. break;
  374. }
  375. }
  376. // 尝试理由模式
  377. for (const pattern of reasonPatterns) {
  378. reasonMatch = content.match(pattern);
  379. if (reasonMatch) {
  380. console.log('理由模式匹配:', pattern);
  381. console.log('理由匹配:', reasonMatch[1].trim());
  382. break;
  383. }
  384. }
  385. console.log('解析结果:', {
  386. contextMatch: contextMatch ? contextMatch[1].trim() : null,
  387. nextStepMatch: nextStepMatch ? nextStepMatch[1].trim() : null,
  388. reasonMatch: reasonMatch ? reasonMatch[1].trim() : null,
  389. globalContext: globalContext ? '可用' : '不可用'
  390. });
  391. // 确定要使用的上下文 - 优先使用单个选项上下文而非全局上下文
  392. let context = null;
  393. if (contextMatch) {
  394. context = contextMatch[1].trim().replace(/同上/gi, '').trim();
  395. console.log('使用单个选项上下文:', context);
  396. } else if (globalContext) {
  397. context = globalContext;
  398. console.log('使用全局上下文:', context);
  399. }
  400. if ((context || contextMatch) && nextStepMatch && reasonMatch) {
  401. const result = {
  402. optionNumber: optionNumber,
  403. stepNumber: stepNumber,
  404. context: context,
  405. nextStep: nextStepMatch[1].trim().replace(/\*\*/g, ''),
  406. reason: reasonMatch[1].trim(),
  407. originalContent: content
  408. };
  409. result.reason = "我在这一步使用了xxxxxxx操作符" + ",目的是\n" + result.reason;
  410. console.log('成功解析选项:', result);
  411. console.log('最终存储的上下文:', result.context);
  412. console.log('===============================');
  413. return result;
  414. } else {
  415. console.log('解析选项内容失败:', {
  416. hasContext: !!(context || contextMatch),
  417. hasNextStep: !!nextStepMatch,
  418. hasReason: !!reasonMatch
  419. });
  420. console.log('===============================');
  421. return null;
  422. }
  423. }
  424. // 将选项显示为卡片
  425. function displayOptions() {
  426. optionsContainer.innerHTML = '';
  427. currentOptions.forEach((option, index) => {
  428. const card = createOptionCard(option, index);
  429. optionsContainer.appendChild(card);
  430. });
  431. }
  432. // 创建选项卡片
  433. function createOptionCard(option, index) {
  434. console.log('=== 创建选项卡片 ===');
  435. console.log('选项索引:', index);
  436. console.log('显示的选项上下文:', option.context);
  437. console.log('选项下一步:', option.nextStep);
  438. console.log('选项理由:', option.reason);
  439. console.log('============================');
  440. const card = document.createElement('div');
  441. card.className = 'option-card';
  442. card.dataset.optionIndex = index;
  443. card.innerHTML = `
  444. <div class="option-header">
  445. <span class="option-number">选项 ${option.optionNumber}</span>
  446. <div class="option-actions">
  447. <button class="select-btn" onclick="selectAndEdit(${index})">选择并编辑</button>
  448. </div>
  449. </div>
  450. <div class="option-content">
  451. <div class="option-field readonly">
  452. <label>上下文:</label>
  453. <textarea readonly class="auto-resize-textarea">${option.context}</textarea>
  454. </div>
  455. <div class="option-field readonly">
  456. <label>下一步:</label>
  457. <input type="text" readonly value="${option.nextStep}" style="display: none;">
  458. <div class="readonly-display">
  459. <span class="clickable-category" onclick="showCategoryPopup('${option.nextStep.replace(/'/g, "\\'")}', event)">${option.nextStep}</span>
  460. </div>
  461. </div>
  462. <div class="option-field readonly">
  463. <label>理由:</label>
  464. <textarea readonly class="auto-resize-textarea">${option.reason}</textarea>
  465. </div>
  466. </div>
  467. `;
  468. // 创建卡片后自动调整文本区域大小
  469. setTimeout(() => {
  470. const textareas = card.querySelectorAll('.auto-resize-textarea');
  471. textareas.forEach(autoResizeTextarea);
  472. }, 0);
  473. return card;
  474. }
  475. // 自动调整文本区域大小函数
  476. function autoResizeTextarea(textarea) {
  477. textarea.style.height = 'auto';
  478. textarea.style.height = Math.max(textarea.scrollHeight, 60) + 'px';
  479. }
  480. // 选择并编辑选项
  481. function selectAndEdit(index) {
  482. const card = document.querySelector(`[data-option-index="${index}"]`);
  483. const fields = card.querySelectorAll('.option-field');
  484. // 移除只读类并使字段可编辑
  485. fields.forEach(field => {
  486. field.classList.remove('readonly');
  487. const input = field.querySelector('input, textarea');
  488. const readonlyDisplay = field.querySelector('.readonly-display');
  489. if (input) {
  490. input.removeAttribute('readonly');
  491. // 对于下一步字段,显示输入框并隐藏只读显示
  492. if (readonlyDisplay) {
  493. input.style.display = 'block';
  494. readonlyDisplay.style.display = 'none';
  495. }
  496. }
  497. // 为文本区域添加自动调整大小功能
  498. if (input && input.tagName === 'TEXTAREA') {
  499. input.addEventListener('input', () => autoResizeTextarea(input));
  500. autoResizeTextarea(input); // 初始调整大小
  501. }
  502. });
  503. // 更新卡片状态并显示模态遮罩层
  504. card.classList.add('editing');
  505. modalOverlay.classList.add('active');
  506. // 更新操作按钮
  507. const actionsDiv = card.querySelector('.option-actions');
  508. actionsDiv.innerHTML = `
  509. <button class="save-btn" onclick="saveOption(${index})">保存更改</button>
  510. <button class="cancel-btn" onclick="cancelEdit(${index})">取消</button>
  511. <button class="send-continue-btn" onclick="sendAndContinue(${index})">发送并继续</button>
  512. `;
  513. }
  514. // 保存选项
  515. function saveOption(index) {
  516. const card = document.querySelector(`[data-option-index="${index}"]`);
  517. const contextTextarea = card.querySelector('.option-field:nth-child(1) textarea');
  518. const nextStepInput = card.querySelector('.option-field:nth-child(2) input');
  519. const reasonTextarea = card.querySelector('.option-field:nth-child(3) textarea');
  520. // 更新选项数据
  521. currentOptions[index].context = contextTextarea.value;
  522. currentOptions[index].nextStep = nextStepInput.value;
  523. currentOptions[index].reason = reasonTextarea.value;
  524. // 保存更新的选项状态
  525. saveConversationState();
  526. // 再次使字段变为只读
  527. const fields = card.querySelectorAll('.option-field');
  528. fields.forEach(field => {
  529. field.classList.add('readonly');
  530. const input = field.querySelector('input, textarea');
  531. const readonlyDisplay = field.querySelector('.readonly-display');
  532. if (input) {
  533. input.setAttribute('readonly', 'readonly');
  534. // 对于下一步字段,隐藏输入框并显示只读显示
  535. if (readonlyDisplay) {
  536. input.style.display = 'none';
  537. readonlyDisplay.style.display = 'block';
  538. // 更新可点击类别文本
  539. const categorySpan = readonlyDisplay.querySelector('.clickable-category');
  540. if (categorySpan) {
  541. categorySpan.textContent = input.value;
  542. categorySpan.setAttribute('onclick', `showCategoryPopup('${input.value.replace(/'/g, "\\'")}', event)`);
  543. }
  544. }
  545. }
  546. });
  547. // 更新卡片状态并隐藏模态遮罩层
  548. card.classList.remove('editing');
  549. modalOverlay.classList.remove('active');
  550. // 更新操作按钮
  551. const actionsDiv = card.querySelector('.option-actions');
  552. actionsDiv.innerHTML = `
  553. <button class="select-btn" onclick="selectAndEdit(${index})">选择并编辑</button>
  554. `;
  555. showNotification('选项保存成功', 'success');
  556. }
  557. // 取消编辑
  558. function cancelEdit(index) {
  559. // 隐藏模态遮罩层
  560. modalOverlay.classList.remove('active');
  561. // 使用原始数据刷新卡片
  562. const card = createOptionCard(currentOptions[index], index);
  563. const oldCard = document.querySelector(`[data-option-index="${index}"]`);
  564. oldCard.parentNode.replaceChild(card, oldCard);
  565. }
  566. // 从类别获取数据状态
  567. function getDataStateFromCategory(category) {
  568. const stateMap = {
  569. '基础算术和数学运算': '数学变换',
  570. '逻辑和条件运算': '条件过滤',
  571. '时间序列:变化检测和值比较': '变化分析',
  572. '时间序列:统计特征工程': '统计工程',
  573. '时间序列:排名、缩放和归一化': '排名和归一化',
  574. '时间序列:衰减、平滑和周转控制': '平滑和控制',
  575. '时间序列:极值和位置识别': '极值识别',
  576. '横截面:排名、缩放和归一化': '横截面归一化',
  577. '横截面:回归和中性化': '中性化',
  578. '横截面:分布变换和截断': '分布变换',
  579. '变换和过滤操作': '变换和过滤',
  580. '分组聚合和统计摘要': '聚合',
  581. '分组排名、缩放和归一化': '分组归一化',
  582. '分组回归和中性化': '分组中性化',
  583. '分组插补和回填': '插补和回填'
  584. };
  585. return stateMap[category] || '已处理';
  586. }
  587. // 清除选项并重新开始
  588. clearOptionsBtn.addEventListener('click', () => {
  589. if (confirm('确定要清除所有进度并重新开始吗?')) {
  590. // 清除所有状态
  591. conversationHistory = [];
  592. currentStep = 1;
  593. pipelineSteps = [];
  594. currentOptions = [];
  595. currentDataState = '原始数据';
  596. // 清除会话存储
  597. sessionStorage.removeItem('featureEngConversationHistory');
  598. sessionStorage.removeItem('featureEngCurrentStep');
  599. sessionStorage.removeItem('featureEngPipelineSteps');
  600. sessionStorage.removeItem('featureEngCurrentOptions');
  601. sessionStorage.removeItem('featureEngCurrentDataState');
  602. // 重置UI
  603. optionsSection.style.display = 'none';
  604. initialSetupSection.style.display = 'block';
  605. questionTemplateInput.value = '';
  606. // 更新流水线状态以反映清除状态
  607. updatePipelineStatus();
  608. showNotification('流水线已清除。您可以开始新的对话。', 'success');
  609. }
  610. });
  611. // 导出流水线
  612. exportPipelineBtn.addEventListener('click', () => {
  613. const exportData = {
  614. timestamp: new Date().toISOString(),
  615. currentStep: currentStep,
  616. pipelineSteps: pipelineSteps,
  617. currentOptions: currentOptions,
  618. conversationHistory: conversationHistory
  619. };
  620. const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
  621. const url = URL.createObjectURL(blob);
  622. const a = document.createElement('a');
  623. a.href = url;
  624. a.download = `feature_engineering_pipeline_${new Date().toISOString().split('T')[0]}.json`;
  625. document.body.appendChild(a);
  626. a.click();
  627. document.body.removeChild(a);
  628. URL.revokeObjectURL(url);
  629. showNotification('流水线导出成功', 'success');
  630. });
  631. // 发送编辑后的选项并继续
  632. function sendAndContinue(index) {
  633. const card = document.querySelector(`[data-option-index="${index}"]`);
  634. const contextTextarea = card.querySelector('.option-field:nth-child(1) textarea');
  635. const nextStepInput = card.querySelector('.option-field:nth-child(2) input');
  636. const reasonTextarea = card.querySelector('.option-field:nth-child(3) textarea');
  637. // 获取编辑后的值
  638. const context = contextTextarea.value;
  639. const chosenStep = nextStepInput.value;
  640. const reason = reasonTextarea.value;
  641. console.log('=== 发送并继续调试 ===');
  642. console.log('选中的选项索引:', index);
  643. console.log('当前选项:', currentOptions[index]);
  644. console.log('上下文:', context);
  645. console.log('选择的步骤:', chosenStep);
  646. console.log('理由:', reason);
  647. console.log('更新前的当前步骤:', currentStep);
  648. console.log('更新前的流水线步骤:', pipelineSteps);
  649. // 隐藏模态遮罩层
  650. modalOverlay.classList.remove('active');
  651. // 添加到流水线步骤 - 修复:使用currentStep而不是选项中的stepNumber
  652. pipelineSteps.push(`步骤 ${currentStep}: ${chosenStep}`);
  653. currentStep = currentStep + 1; // 从当前步骤递增
  654. currentDataState = getDataStateFromCategory(chosenStep);
  655. console.log('更新后的当前步骤:', currentStep);
  656. console.log('更新后的流水线步骤:', pipelineSteps);
  657. console.log('当前数据状态:', currentDataState);
  658. // 更新流水线状态
  659. updatePipelineStatus();
  660. // 保存流水线状态
  661. saveConversationState();
  662. // 为AI系统提示准备适当格式的消息
  663. // 构建先前步骤列表
  664. const previousStepsText = pipelineSteps.length > 0 ? pipelineSteps.join(', ') : '无';
  665. // 获取所选步骤的类别描述
  666. const categoryData = operatorsData.find(cat => cat.name === chosenStep);
  667. const stepDescription = categoryData ? categoryData.description : '无描述可用';
  668. const userMessage = `
  669. 我选择的下一步: ${chosenStep}
  670. 步骤描述: ${stepDescription}
  671. 选择理由: ${reason}
  672. 基于我的选择和信息,请推荐一些进一步的选项`;
  673. console.log('=== 为AI构建的消息 ===');
  674. console.log('发送的用户消息:', userMessage);
  675. console.log('当前步骤:', currentStep);
  676. console.log('先前步骤:', previousStepsText);
  677. console.log('当前数据状态:', currentDataState);
  678. console.log('步骤描述:', stepDescription);
  679. console.log('选择的下一步:', chosenStep);
  680. console.log('选择理由:', reason);
  681. console.log('=================================');
  682. // 获取下一个推荐
  683. showLoading('正在获取下一个推荐...');
  684. fetch('/feature-engineering/api/continue-conversation', {
  685. method: 'POST',
  686. headers: {
  687. 'X-API-Key': apiKey,
  688. 'Content-Type': 'application/json'
  689. },
  690. body: JSON.stringify({
  691. conversation_history: conversationHistory,
  692. user_message: userMessage,
  693. custom_system_prompt: customSystemPrompt
  694. })
  695. })
  696. .then(response => response.json())
  697. .then(data => {
  698. console.log('=== 发送并继续提示 ===');
  699. console.log('用户消息:', userMessage);
  700. console.log('之前的当前步骤:', currentStep);
  701. console.log('之前的流水线步骤:', pipelineSteps);
  702. console.log('发送的对话历史:', conversationHistory);
  703. console.log('=== AI响应 ===');
  704. console.log('AI响应:', data.response);
  705. console.log('==================');
  706. if (data.success) {
  707. // 添加到对话历史
  708. conversationHistory.push({
  709. role: 'user',
  710. content: userMessage
  711. });
  712. conversationHistory.push({
  713. role: 'assistant',
  714. content: data.response
  715. });
  716. console.log('更新后的对话历史:', conversationHistory);
  717. // 解析新的AI响应
  718. parseAIResponse(data.response);
  719. // 保存对话状态
  720. saveConversationState();
  721. showNotification(`编辑的选项发送成功。下一步推荐已加载。`, 'success');
  722. } else {
  723. showNotification(`错误: ${data.error || '未知错误'}`, 'error');
  724. console.error('API错误详情:', data);
  725. }
  726. })
  727. .catch(error => {
  728. showNotification('获取下一个推荐时出错: ' + error.message, 'error');
  729. console.error('下一步错误:', error);
  730. })
  731. .finally(() => {
  732. hideLoading();
  733. });
  734. }
  735. // 使函数在onclick处理程序中全局可用
  736. window.selectAndEdit = selectAndEdit;
  737. window.saveOption = saveOption;
  738. window.cancelEdit = cancelEdit;
  739. window.sendAndContinue = sendAndContinue;
  740. window.closeSystemPromptModal = closeSystemPromptModal;
  741. window.saveSystemPrompt = saveSystemPrompt;
  742. // 更新流水线状态
  743. function updatePipelineStatus() {
  744. console.log('=== 更新流水线状态 ===');
  745. console.log('流水线步骤:', pipelineSteps);
  746. console.log('当前步骤:', currentStep);
  747. console.log('当前数据状态:', currentDataState);
  748. if (pipelineSteps.length === 0) {
  749. pipelineStatus.style.display = 'none';
  750. return;
  751. }
  752. pipelineStatus.style.display = 'block';
  753. pipelineStepsDiv.innerHTML = pipelineSteps.map(step =>
  754. `<div class="pipeline-step"><strong>${step}</strong></div>`
  755. ).join('');
  756. // 添加当前状态
  757. const statusDiv = document.createElement('div');
  758. statusDiv.className = 'pipeline-step';
  759. statusDiv.style.backgroundColor = '#e8f5e8';
  760. statusDiv.innerHTML = `<strong>当前步骤:</strong> ${currentStep} | <strong>数据状态:</strong> ${currentDataState}`;
  761. pipelineStepsDiv.appendChild(statusDiv);
  762. console.log('流水线状态已更新');
  763. console.log('==============================');
  764. }
  765. // 工具函数
  766. function showNotification(message, type) {
  767. const notification = document.createElement('div');
  768. notification.className = `notification ${type}`;
  769. notification.textContent = message;
  770. document.body.appendChild(notification);
  771. setTimeout(() => {
  772. notification.remove();
  773. }, 8000);
  774. }
  775. let loadingElement = null;
  776. function showLoading(message) {
  777. loadingElement = document.createElement('div');
  778. loadingElement.className = 'loading-overlay';
  779. loadingElement.innerHTML = `
  780. <div class="loading-spinner"></div>
  781. <div class="loading-message">${message}</div>
  782. `;
  783. document.body.appendChild(loadingElement);
  784. }
  785. function hideLoading() {
  786. if (loadingElement) {
  787. loadingElement.remove();
  788. loadingElement = null;
  789. }
  790. }
  791. // 操作符参考数据
  792. const operatorsData = [
  793. {
  794. id: 1,
  795. name: "基础算术和数学运算",
  796. description: "核心数学和逐元素运算(例如,加、减、乘、对数、指数、绝对值、幂等)",
  797. 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"]
  798. },
  799. {
  800. id: 2,
  801. name: "逻辑和条件运算",
  802. description: "布尔逻辑、比较和条件分支(例如,与、或、非、如果否则、等于、大于、小于等)",
  803. 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"]
  804. },
  805. {
  806. id: 3,
  807. name: "时间序列:变化检测和值比较",
  808. description: "比较随时间变化的值,计算差异,检测变化,或计算自上次变化以来的天数(例如,ts_delta、ts_returns、days_from_last_change、last_diff_value等)",
  809. operators: ["ts_delta", "ts_returns", "days_from_last_change", "last_diff_value", "ts_delta_limit", "ts_backfill"]
  810. },
  811. {
  812. id: 4,
  813. name: "时间序列:统计特征工程",
  814. description: "计算随时间滚动的统计属性(例如,ts_mean、ts_std_dev、ts_skewness、ts_kurtosis、ts_entropy、ts_moment、ts_covariance、ts_corr、ts_co_skewness、ts_co_kurtosis等)",
  815. 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"]
  816. },
  817. {
  818. id: 5,
  819. name: "时间序列:排名、缩放和归一化",
  820. description: "在滚动窗口内对时间序列数据进行排名、缩放或归一化(例如,ts_rank、ts_scale、ts_percentage、ts_quantile等)",
  821. operators: ["ts_rank", "ts_scale", "ts_percentage", "ts_quantile", "ts_rank_gmean_amean_diff", "ts_zscore"]
  822. },
  823. {
  824. id: 6,
  825. name: "时间序列:衰减、平滑和周转控制",
  826. description: "在时间序列中应用衰减(线性、指数、加权)、平滑或控制周转(例如,ts_decay_exp_window、ts_decay_linear、ts_weighted_decay、ts_target_tvr_decay、hump、jump_decay等)",
  827. 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"]
  828. },
  829. {
  830. id: 7,
  831. name: "时间序列:极值和位置识别",
  832. description: "识别最小/最大值、它们的差异或窗口内极值的位置(索引)(例如,ts_min、ts_max、ts_min_diff、ts_max_diff、ts_arg_min、ts_arg_max、ts_min_max_diff等)",
  833. 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"]
  834. },
  835. {
  836. id: 8,
  837. name: "横截面:排名、缩放和归一化",
  838. description: "在单个时间点跨工具对数据进行排名、缩放、归一化或标准化(例如,rank、zscore、scale_down、normalize、rank_by_side等)",
  839. operators: ["rank", "zscore", "scale_down", "scale", "normalize", "rank_by_side", "generalized_rank", "one_side", "rank_gmean_amean_diff"]
  840. },
  841. {
  842. id: 9,
  843. name: "横截面:回归和中性化",
  844. description: "移除其他变量的影响,执行横截面回归,或将一个向量相对于另一个向量正交化(例如,regression_neut、vector_neut、regression_proj、vector_proj、multi_regression等)",
  845. operators: ["regression_neut", "vector_neut", "regression_proj", "vector_proj", "multi_regression"]
  846. },
  847. {
  848. id: 10,
  849. name: "横截面:分布变换和截断",
  850. description: "跨工具变换分布或截断异常值(例如,quantile、winsorize、truncate、bucket、generalized_rank等)",
  851. operators: ["quantile", "winsorize", "truncate", "bucket", "right_tail", "left_tail", "tail"]
  852. },
  853. {
  854. id: 11,
  855. name: "变换和过滤操作",
  856. description: "通用数据变换、过滤、钳制、掩码或条件值分配(例如,filter、clamp、keep、tail、left_tail、right_tail、trade_when等)",
  857. operators: ["filter", "clamp", "keep", "tail", "left_tail", "right_tail", "trade_when"]
  858. },
  859. {
  860. id: 12,
  861. name: "分组聚合和统计摘要",
  862. description: "在每个组内(如行业、部门、国家)聚合或摘要(例如,均值、总和、标准差、最小值、最大值、中位数)。每个股票根据其组成员资格接收组级值。",
  863. operators: ["group_mean", "group_sum", "group_std_dev", "group_min", "group_max", "group_median", "group_count", "group_percentage", "group_extra"]
  864. },
  865. {
  866. id: 13,
  867. name: "分组排名、缩放和归一化",
  868. description: "在每个组内进行排名、缩放或归一化(例如,每个股票的行业排名,在部门内缩放值)。每个股票在其组内同行中进行排名或缩放。",
  869. operators: ["group_rank", "group_scale", "group_zscore", "group_normalize"]
  870. },
  871. {
  872. id: 14,
  873. name: "分组回归和中性化",
  874. description: "移除组级影响,在每个组内执行回归或正交化(例如,行业中性化,分组回归)。每个组独立处理。",
  875. operators: ["group_vector_neut", "group_vector_proj", "group_neutralize", "group_multi_regression"]
  876. },
  877. {
  878. id: 15,
  879. name: "分组插补和回填",
  880. description: "使用同一组中其他股票的数据插补缺失值或回填(例如,用组均值或中位数填充NaN,group_backfill)。",
  881. operators: ["group_backfill"]
  882. }
  883. ];
  884. // 显示类别弹出窗口
  885. function showCategoryPopup(categoryName, event) {
  886. event.stopPropagation();
  887. // 查找类别数据
  888. const categoryData = operatorsData.find(cat => cat.name === categoryName);
  889. if (!categoryData) {
  890. console.log('未找到类别:', categoryName);
  891. return;
  892. }
  893. // 填充弹出窗口内容
  894. categoryPopupTitle.textContent = categoryData.name;
  895. categoryPopupDescription.textContent = categoryData.description;
  896. categoryPopupOperatorsTitle.textContent = `可用操作符 (${categoryData.operators.length}):`;
  897. const operatorsHtml = categoryData.operators.map(op =>
  898. `<span class="popup-operator-tag">${op}</span>`
  899. ).join('');
  900. categoryPopupOperators.innerHTML = operatorsHtml;
  901. // 将弹出窗口定位在点击元素附近
  902. const rect = event.target.getBoundingClientRect();
  903. const popup = categoryPopup;
  904. popup.style.display = 'block';
  905. // 计算位置
  906. let left = rect.left + window.scrollX;
  907. let top = rect.bottom + window.scrollY + 5;
  908. // 如果弹出窗口会超出屏幕则调整
  909. const popupRect = popup.getBoundingClientRect();
  910. if (left + popupRect.width > window.innerWidth) {
  911. left = window.innerWidth - popupRect.width - 20;
  912. }
  913. if (top + popupRect.height > window.innerHeight + window.scrollY) {
  914. top = rect.top + window.scrollY - popupRect.height - 5;
  915. }
  916. popup.style.left = left + 'px';
  917. popup.style.top = top + 'px';
  918. }
  919. // 隐藏类别弹出窗口
  920. function hideCategoryPopup() {
  921. categoryPopup.style.display = 'none';
  922. }
  923. // 使函数在onclick处理程序中全局可用
  924. window.showCategoryPopup = showCategoryPopup;
  925. window.hideCategoryPopup = hideCategoryPopup;
  926. // 保存对话状态的函数
  927. function saveConversationState() {
  928. sessionStorage.setItem('featureEngConversationHistory', JSON.stringify(conversationHistory));
  929. sessionStorage.setItem('featureEngCurrentStep', currentStep.toString());
  930. sessionStorage.setItem('featureEngPipelineSteps', JSON.stringify(pipelineSteps));
  931. sessionStorage.setItem('featureEngCurrentOptions', JSON.stringify(currentOptions));
  932. sessionStorage.setItem('featureEngCurrentDataState', currentDataState);
  933. console.log('对话状态已保存到sessionStorage');
  934. }