diff --git a/app/static/futures_analysis.js b/app/static/futures_analysis.js index b5eec5c..379b8ae 100644 --- a/app/static/futures_analysis.js +++ b/app/static/futures_analysis.js @@ -635,6 +635,7 @@ let trReflTags = []; let trReflExperiences = []; let trReflTagState = new Set(); let trReflCurrentPairId = null; +let trReflCurrentReflectionId = null; let trReflCurrentAIResult = null; function trSwitchSubtab(tab) { @@ -673,6 +674,14 @@ function trReflInit() { trReflSaveDaily(); }); + const saveAndAiBtn = document.getElementById('tr-refl-save-and-ai'); + if (saveAndAiBtn) { + saveAndAiBtn.addEventListener('click', e => { + e.preventDefault(); + trReflSaveAndAnalyze(); + }); + } + document.querySelectorAll('#tr-refl-discipline-score .tr-form-star, #tr-refl-daily-discipline-score .tr-form-star').forEach(star => { star.addEventListener('click', () => { const container = star.parentElement; @@ -751,10 +760,10 @@ function trReflRender() { document.getElementById('tr-refl-emotion').className = 'tr-refl-daily-value ' + (daily.emotion_state ? '' : 'empty'); document.getElementById('tr-refl-discipline').textContent = daily.discipline_score ? daily.discipline_score + '/5' : '未填写'; document.getElementById('tr-refl-discipline').className = 'tr-refl-daily-value ' + (daily.discipline_score ? '' : 'empty'); - document.getElementById('tr-refl-overall').textContent = trReflTextMap.overall[daily.overall_evaluation] || daily.overall_evaluation || '未填写'; - document.getElementById('tr-refl-overall').className = 'tr-refl-daily-value ' + (daily.overall_evaluation ? '' : 'empty'); - document.getElementById('tr-refl-market').textContent = trReflTextMap.market[daily.market_judgement] || daily.market_judgement || '未填写'; - document.getElementById('tr-refl-market').className = 'tr-refl-daily-value ' + (daily.market_judgement ? '' : 'empty'); + document.getElementById('tr-refl-overall').textContent = trReflTextMap.overall[daily.overall_rating] || daily.overall_rating || '未填写'; + document.getElementById('tr-refl-overall').className = 'tr-refl-daily-value ' + (daily.overall_rating ? '' : 'empty'); + document.getElementById('tr-refl-market').textContent = trReflTextMap.market[daily.market_judgment] || daily.market_judgment || '未填写'; + document.getElementById('tr-refl-market').className = 'tr-refl-daily-value ' + (daily.market_judgment ? '' : 'empty'); const pairsContainer = document.getElementById('tr-refl-pairs'); const pairs = data.pairs || []; @@ -792,8 +801,8 @@ function trReflRenderPairCard(p) { const pnlCls = pnl >= 0 ? 'profit' : 'loss'; const dirCls = p.direction === '多' ? 'long' : 'short'; const dirText = p.direction === '多' ? '做多' : '做空'; - const tags = (p.tags || []).map(t => `${t}`).join(''); - const hasReflection = p.reflection && (p.reflection.entry_reason || p.reflection.free_reflection); + const tags = (p.tags || []).map(t => `${typeof t === 'string' ? t : t.name}`).join(''); + const hasReflection = p.reflection_status === 'done'; return `
@@ -835,9 +844,30 @@ async function trReflOpenEditModal(pairId) { const pair = data.pairs.find(p => p.id === pairId); if (!pair) return; trReflCurrentPairId = pairId; - const reflection = pair.reflection || {}; + trReflCurrentReflectionId = null; document.getElementById('tr-refl-edit-pair-id').value = pairId; + + let reflection = {}; + let selectedTagNames = new Set(); + try { + const [reflRes, tagsRes] = await Promise.all([ + fetch(`${TR_API_BASE}/reflections?trade_pair_id=${pairId}`), + fetch(`${TR_API_BASE}/trade-pairs/${pairId}/tags`), + ]); + const reflJson = await reflRes.json(); + const tagsJson = await tagsRes.json(); + if (reflJson.success && reflJson.data && reflJson.data.length) { + reflection = reflJson.data[0]; + trReflCurrentReflectionId = reflection.id || null; + } + if (tagsJson.success && tagsJson.data) { + selectedTagNames = new Set(tagsJson.data.map(t => typeof t === 'string' ? t : t.name)); + } + } catch (e) { + console.error('加载反思/标签失败', e); + } + document.getElementById('tr-refl-entry-reason').value = reflection.entry_reason || ''; document.getElementById('tr-refl-exit-reason').value = reflection.exit_reason || ''; document.getElementById('tr-refl-entry-timing').value = reflection.entry_timing || ''; @@ -852,10 +882,9 @@ async function trReflOpenEditModal(pairId) { // 渲染标签选择 const tagPool = document.getElementById('tr-refl-edit-tags'); - const selected = new Set(reflection.tags || pair.tags || []); tagPool.innerHTML = trReflTags.map(t => { const name = typeof t === 'string' ? t : t.name; - const active = selected.has(name) ? 'selected' : ''; + const active = selectedTagNames.has(name) ? 'selected' : ''; return `${name}`; }).join(''); @@ -865,15 +894,16 @@ async function trReflOpenEditModal(pairId) { function trReflCloseEditModal() { document.getElementById('tr-refl-edit-modal').classList.remove('show'); trReflCurrentPairId = null; + trReflCurrentReflectionId = null; } -async function trReflSaveEdit() { +async function trReflSaveEdit(andAnalyze = false) { const pairId = parseInt(document.getElementById('tr-refl-edit-pair-id').value); if (!pairId) return; - const selectedTags = Array.from(document.querySelectorAll('#tr-refl-edit-tags .tr-tag-chip.selected')).map(el => el.textContent); const body = { trade_pair_id: pairId, + trade_date: trReflCurrentDate, entry_reason: document.getElementById('tr-refl-entry-reason').value, exit_reason: document.getElementById('tr-refl-exit-reason').value, entry_timing: document.getElementById('tr-refl-entry-timing').value, @@ -881,36 +911,108 @@ async function trReflSaveEdit() { position_management: document.getElementById('tr-refl-position').value, discipline_score: parseInt(document.getElementById('tr-refl-discipline-score').dataset.value || '0'), free_reflection: document.getElementById('tr-refl-free').value, - tags: selectedTags, }; try { - const res = await fetch(`${TR_API_BASE}/reflections`, { - method: 'POST', + const url = trReflCurrentReflectionId + ? `${TR_API_BASE}/reflections/${trReflCurrentReflectionId}` + : `${TR_API_BASE}/reflections`; + const method = trReflCurrentReflectionId ? 'PUT' : 'POST'; + const res = await fetch(url, { + method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); const json = await res.json(); - if (json.success) { - showToast('success', '保存成功', '反思已保存'); - trReflCloseEditModal(); - trReflLoadData(); - } else { + if (!json.success) { showToast('error', '保存失败', json.message || '请重试'); + return; } + + // 同步标签(编辑 Modal 中的标签选择) + const selectedTagNames = Array.from(document.querySelectorAll('#tr-refl-edit-tags .tr-tag-chip.selected')).map(el => el.textContent); + try { + await trReflSyncPairTags(pairId, selectedTagNames); + } catch (e) { + console.error('标签同步失败', e); + } + + if (andAnalyze) { + try { + const reRes = await fetch(`${TR_API_BASE}/reanalyze`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ trade_pair_id: pairId }), + }); + const reJson = await reRes.json(); + if (reJson.success && reJson.data) { + // 重新分析接口不返回 id,需要再取一次 latest 获取完整记录 + const latestRes = await fetch(`${TR_API_BASE}/reanalyze/${pairId}/latest`); + const latestJson = await latestRes.json(); + if (latestJson.success && latestJson.data) { + trReflCurrentAIResult = latestJson.data; + } else { + trReflCurrentAIResult = reJson.data; + } + trReflCloseEditModal(); + trReflOpenAIModal(pairId, false); + return; + } + showToast('error', 'AI分析失败', reJson.message || '请重试'); + return; + } catch (e) { + showToast('error', 'AI分析失败', e.message); + return; + } + } + + showToast('success', '保存成功', '反思已保存'); + trReflCloseEditModal(); + trReflLoadData(); } catch (e) { showToast('error', '保存失败', e.message); } } +async function trReflSaveAndAnalyze() { + await trReflSaveEdit(true); +} + +async function trReflSyncPairTags(pairId, selectedNames) { + const res = await fetch(`${TR_API_BASE}/trade-pairs/${pairId}/tags`); + const json = await res.json(); + const currentTags = (json.success ? json.data : []) || []; + const currentNames = new Set(currentTags.map(t => typeof t === 'string' ? t : t.name)); + + // 新增标签 + for (const name of selectedNames) { + if (currentNames.has(name)) continue; + const tag = trReflTags.find(t => (typeof t === 'string' ? t : t.name) === name); + if (!tag || typeof tag === 'string') continue; + await fetch(`${TR_API_BASE}/trade-pairs/${pairId}/tags?tag_id=${tag.id}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + }); + } + + // 移除标签 + for (const tag of currentTags) { + const name = typeof tag === 'string' ? tag : tag.name; + if (selectedNames.includes(name)) continue; + const tagId = typeof tag === 'string' ? null : tag.id; + if (!tagId) continue; + await fetch(`${TR_API_BASE}/trade-pairs/${pairId}/tags/${tagId}`, { method: 'DELETE' }); + } +} + // ===== 当日反思 Modal ===== function trReflOpenDailyModal() { const daily = (trReflData && trReflData.daily_reflection) || {}; document.getElementById('tr-refl-daily-emotion').value = daily.emotion_state || ''; - document.getElementById('tr-refl-daily-overall').value = daily.overall_evaluation || ''; - document.getElementById('tr-refl-daily-market').value = daily.market_judgement || ''; + document.getElementById('tr-refl-daily-overall').value = daily.overall_rating || ''; + document.getElementById('tr-refl-daily-market').value = daily.market_judgment || ''; document.getElementById('tr-refl-daily-summary').value = daily.summary || ''; - document.getElementById('tr-refl-daily-improvement').value = daily.improvement || ''; + document.getElementById('tr-refl-daily-improvement').value = daily.improvements || ''; const score = daily.discipline_score || 0; const container = document.getElementById('tr-refl-daily-discipline-score'); @@ -926,13 +1028,13 @@ function trReflCloseDailyModal() { async function trReflSaveDaily() { const body = { - trade_date: trReflCurrentDate, + reflection_date: trReflCurrentDate, emotion_state: document.getElementById('tr-refl-daily-emotion').value, - overall_evaluation: document.getElementById('tr-refl-daily-overall').value, - market_judgement: document.getElementById('tr-refl-daily-market').value, + overall_rating: document.getElementById('tr-refl-daily-overall').value, + market_judgment: document.getElementById('tr-refl-daily-market').value, discipline_score: parseInt(document.getElementById('tr-refl-daily-discipline-score').dataset.value || '0'), summary: document.getElementById('tr-refl-daily-summary').value, - improvement: document.getElementById('tr-refl-daily-improvement').value, + improvements: document.getElementById('tr-refl-daily-improvement').value, }; try { @@ -960,7 +1062,15 @@ async function trReflOpenTagsModal(pairId) { const pair = data.pairs.find(p => p.id === pairId); if (!pair) return; trReflCurrentPairId = pairId; - trReflTagState = new Set((pair.tags || []).map(t => typeof t === 'string' ? t : t.name)); + trReflTagState = new Set(); + + try { + const res = await fetch(`${TR_API_BASE}/trade-pairs/${pairId}/tags`); + const json = await res.json(); + if (json.success && json.data) { + trReflTagState = new Set(json.data.map(t => typeof t === 'string' ? t : t.name)); + } + } catch (e) { console.error('加载交易标签失败', e); } document.getElementById('tr-refl-tags-pair-id').value = pairId; trReflRenderTagPreset(); @@ -1009,36 +1119,58 @@ function trReflAddCustomTag() { trReflRenderTagPreset(); } +async function trReflResolveTagId(name) { + const existing = trReflTags.find(t => (typeof t === 'string' ? t : t.name) === name); + if (existing && typeof existing === 'object' && existing.id) return existing.id; + + // 对自定义标签创建后再使用 + try { + const res = await fetch(`${TR_API_BASE}/tags`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name, category: 'custom' }), + }); + const json = await res.json(); + if (json.success && json.data && json.data.id) { + trReflTags.push({ id: json.data.id, name, category: 'custom', is_preset: false }); + return json.data.id; + } + } catch (e) { console.error('创建标签失败', e); } + return null; +} + async function trReflSaveTags() { const pairId = parseInt(document.getElementById('tr-refl-tags-pair-id').value); if (!pairId) return; - const currentTags = Array.from(trReflTagState); - const data = trReflData || { pairs: [] }; - const pair = data.pairs.find(p => p.id === pairId); - const oldTags = (pair && pair.tags) || []; - const oldNames = new Set(oldTags.map(t => typeof t === 'string' ? t : t.name)); + const selectedNames = Array.from(trReflTagState); try { - for (const tag of currentTags) { - if (!oldNames.has(tag)) { - await fetch(`${TR_API_BASE}/trade-pairs/${pairId}/tags`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ tag }), - }); - } + const res = await fetch(`${TR_API_BASE}/trade-pairs/${pairId}/tags`); + const json = await res.json(); + const currentTags = (json.success ? json.data : []) || []; + const currentMap = new Map(currentTags.map(t => [ + typeof t === 'string' ? t : t.name, + typeof t === 'string' ? null : t.id, + ])); + + // 新增标签 + for (const name of selectedNames) { + if (currentMap.has(name)) continue; + const tagId = await trReflResolveTagId(name); + if (!tagId) continue; + await fetch(`${TR_API_BASE}/trade-pairs/${pairId}/tags?tag_id=${tagId}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + }); } - for (const tag of oldTags) { - const name = typeof tag === 'string' ? tag : tag.name; - if (!trReflTagState.has(name)) { - await fetch(`${TR_API_BASE}/trade-pairs/${pairId}/tags`, { - method: 'DELETE', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ tag: name }), - }); - } + + // 移除标签 + for (const [name, tagId] of currentMap) { + if (selectedNames.includes(name) || !tagId) continue; + await fetch(`${TR_API_BASE}/trade-pairs/${pairId}/tags/${tagId}`, { method: 'DELETE' }); } + showToast('success', '保存成功', '标签已更新'); trReflCloseTagsModal(); trReflLoadData(); @@ -1053,43 +1185,50 @@ function trReflCloseTagsModal() { } // ===== AI 分析结果 Modal ===== -async function trReflOpenAIModal(pairId) { +async function trReflOpenAIModal(pairId, fetchLatest = true) { const data = trReflData || { pairs: [] }; const pair = data.pairs.find(p => p.id === pairId); if (!pair) return; trReflCurrentPairId = pairId; - trReflCurrentAIResult = pair.ai_analysis || null; - if (!trReflCurrentAIResult) { - // 如果没有缓存的 AI 结果,尝试请求分析接口 + if (fetchLatest) { + trReflCurrentAIResult = null; try { - const res = await fetch(`${TR_API_BASE}/analyze-trade`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - symbol: pair.symbol, - open_date: pair.open_date, - open_time: pair.open_time, - close_date: pair.close_date, - close_time: pair.close_time, - direction: pair.direction, - open_price: pair.open_price, - close_price: pair.close_price, - }), - }); + const res = await fetch(`${TR_API_BASE}/reanalyze/${pairId}/latest`); const json = await res.json(); if (json.success && json.data) { trReflCurrentAIResult = json.data; } - } catch (e) { console.error('AI分析请求失败', e); } + } catch (e) { console.error('获取最新AI分析失败', e); } + + // 没有最新结果时,自动基于反思重新分析 + if (!trReflCurrentAIResult) { + try { + const reRes = await fetch(`${TR_API_BASE}/reanalyze`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ trade_pair_id: pairId }), + }); + const reJson = await reRes.json(); + if (reJson.success && reJson.data) { + const latestRes = await fetch(`${TR_API_BASE}/reanalyze/${pairId}/latest`); + const latestJson = await latestRes.json(); + if (latestJson.success && latestJson.data) { + trReflCurrentAIResult = latestJson.data; + } else { + trReflCurrentAIResult = reJson.data; + } + } + } catch (e) { console.error('AI重新分析失败', e); } + } } const result = trReflCurrentAIResult || {}; document.getElementById('tr-refl-ai-pair-id').value = pairId; - document.getElementById('tr-refl-ai-overall').textContent = result.overall_evaluation || result.analysis || '暂无 AI 分析结果'; + document.getElementById('tr-refl-ai-overall').textContent = result.overall_evaluation || result.analysis || '暂无 AI 分析结果(请先保存反思)'; document.getElementById('tr-refl-ai-strengths').innerHTML = (result.strengths || []).map(s => `
  • ${s}
  • `).join('') || '
  • -
  • '; document.getElementById('tr-refl-ai-weaknesses').innerHTML = (result.weaknesses || []).map(s => `
  • ${s}
  • `).join('') || '
  • -
  • '; - document.getElementById('tr-refl-ai-suggestion').textContent = result.suggestion || result.experience_suggestion || '暂无建议'; + document.getElementById('tr-refl-ai-suggestion').textContent = result.experience_suggestion || result.suggestion || '暂无建议'; document.getElementById('tr-refl-ai-modal').classList.add('show'); } @@ -1101,19 +1240,20 @@ async function trReflSaveExperience() { if (!pair || !trReflCurrentAIResult) return; const result = trReflCurrentAIResult; + const analysisId = result.id; + if (!analysisId) { + showToast('error', '保存失败', '缺少分析记录ID'); + return; + } + const body = { title: `${pair.symbol_name || pair.variety || pair.symbol} ${pair.direction === '多' ? '做多' : '做空'} 经验`, - content: result.overall_evaluation || result.analysis || '', - type: pair.net_pnl >= 0 ? 'insight' : 'lesson', - tags: ['AI分析', ...(pair.tags || [])].slice(0, 5), - source_trade_pair_id: pairId, - strengths: result.strengths || [], - weaknesses: result.weaknesses || [], - suggestion: result.suggestion || result.experience_suggestion || '', + content: result.overall_evaluation || result.analysis || result.experience_suggestion || '', + tags: ['AI分析'], }; try { - const res = await fetch(`${TR_API_BASE}/experiences`, { + const res = await fetch(`${TR_API_BASE}/ai-analysis/${analysisId}/save-experience`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),