刘立冬 3 hafta önce
ebeveyn
işleme
6ecca466a0
1 değiştirilmiş dosya ile 434 ekleme ve 0 silme
  1. 434 0
      visualize_stage78_with_deconstruction.py

+ 434 - 0
visualize_stage78_with_deconstruction.py

@@ -508,6 +508,251 @@ def generate_html(data: List[Dict[str, Any]], stats: Dict[str, Any],
             margin-top: 4px;
         }}
 
+        /* P值显示样式 */
+        .p-score-container {{
+            display: flex;
+            align-items: center;
+            gap: 8px;
+            margin-top: 6px;
+            padding: 6px 8px;
+            background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
+            border-radius: 6px;
+            border-left: 3px solid #0ea5e9;
+        }}
+
+        .p-score-label {{
+            font-size: 11px;
+            font-weight: 600;
+            color: #0369a1;
+        }}
+
+        .p-score-value {{
+            font-size: 13px;
+            font-weight: 700;
+            color: #0284c7;
+        }}
+
+        .p-score-info-icon {{
+            cursor: pointer;
+            font-size: 14px;
+            color: #0ea5e9;
+            transition: all 0.2s;
+            padding: 2px 6px;
+            border-radius: 50%;
+            background: white;
+        }}
+
+        .p-score-info-icon:hover {{
+            background: #0ea5e9;
+            color: white;
+            transform: scale(1.1);
+        }}
+
+        .p-score-arrow {{
+            display: flex;
+            align-items: center;
+            gap: 6px;
+            font-size: 11px;
+            color: #64748b;
+            margin-top: 4px;
+        }}
+
+        .p-score-arrow-symbol {{
+            color: #0ea5e9;
+            font-weight: bold;
+        }}
+
+        /* P值详情模态窗口 */
+        .p-detail-modal {{
+            display: none;
+            position: fixed;
+            top: 0;
+            left: 0;
+            right: 0;
+            bottom: 0;
+            background: rgba(0, 0, 0, 0.7);
+            z-index: 10000;
+            animation: fadeIn 0.3s ease;
+        }}
+
+        .p-detail-modal.active {{
+            display: flex;
+            align-items: center;
+            justify-content: center;
+        }}
+
+        .p-detail-window {{
+            background: white;
+            border-radius: 12px;
+            width: 90%;
+            max-width: 800px;
+            max-height: 85vh;
+            overflow: hidden;
+            box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
+            animation: slideUp 0.3s ease;
+            display: flex;
+            flex-direction: column;
+        }}
+
+        .p-detail-header {{
+            padding: 20px 25px;
+            background: linear-gradient(135deg, #0ea5e9 0%, #0284c7 100%);
+            color: white;
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+            flex-shrink: 0;
+        }}
+
+        .p-detail-title {{
+            font-size: 18px;
+            font-weight: 600;
+        }}
+
+        .p-detail-close-btn {{
+            background: rgba(255, 255, 255, 0.2);
+            border: none;
+            color: white;
+            width: 36px;
+            height: 36px;
+            border-radius: 50%;
+            cursor: pointer;
+            font-size: 20px;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            transition: all 0.2s;
+        }}
+
+        .p-detail-close-btn:hover {{
+            background: rgba(255, 255, 255, 0.3);
+            transform: scale(1.1);
+        }}
+
+        .p-detail-body {{
+            flex: 1;
+            overflow-y: auto;
+            padding: 25px;
+        }}
+
+        .p-formula-section {{
+            background: #f8fafc;
+            border: 2px solid #e2e8f0;
+            border-radius: 8px;
+            padding: 16px;
+            margin-bottom: 20px;
+        }}
+
+        .p-formula-title {{
+            font-size: 14px;
+            font-weight: 600;
+            color: #0f172a;
+            margin-bottom: 12px;
+        }}
+
+        .p-formula {{
+            font-family: 'Courier New', monospace;
+            font-size: 16px;
+            color: #0284c7;
+            background: white;
+            padding: 12px;
+            border-radius: 6px;
+            text-align: center;
+            margin-bottom: 8px;
+        }}
+
+        .p-summary {{
+            display: grid;
+            grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
+            gap: 12px;
+            margin-bottom: 20px;
+        }}
+
+        .p-summary-item {{
+            background: white;
+            border: 2px solid #e2e8f0;
+            border-radius: 8px;
+            padding: 12px;
+            text-align: center;
+        }}
+
+        .p-summary-label {{
+            font-size: 12px;
+            color: #64748b;
+            margin-bottom: 4px;
+        }}
+
+        .p-summary-value {{
+            font-size: 20px;
+            font-weight: 700;
+            color: #0f172a;
+        }}
+
+        .p-summary-value.highlight {{
+            color: #0284c7;
+        }}
+
+        .p-matches-section {{
+            margin-top: 20px;
+        }}
+
+        .p-matches-title {{
+            font-size: 15px;
+            font-weight: 600;
+            color: #0f172a;
+            margin-bottom: 12px;
+        }}
+
+        .p-match-item {{
+            background: white;
+            border: 2px solid #e2e8f0;
+            border-radius: 8px;
+            padding: 12px;
+            margin-bottom: 10px;
+            transition: all 0.2s;
+        }}
+
+        .p-match-item:hover {{
+            border-color: #0ea5e9;
+            box-shadow: 0 4px 12px rgba(14, 165, 233, 0.1);
+        }}
+
+        .p-match-header {{
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+            margin-bottom: 8px;
+        }}
+
+        .p-match-title {{
+            font-size: 14px;
+            font-weight: 600;
+            color: #0f172a;
+            flex: 1;
+        }}
+
+        .p-match-contribution {{
+            font-size: 16px;
+            font-weight: 700;
+            color: #0284c7;
+        }}
+
+        .p-match-details {{
+            display: flex;
+            gap: 16px;
+            font-size: 12px;
+            color: #64748b;
+        }}
+
+        .p-match-detail-item {{
+            display: flex;
+            gap: 4px;
+        }}
+
+        .p-match-detail-label {{
+            font-weight: 600;
+        }}
+
         /* 右侧结果区 */
         .right-content {{
             flex: 1;
@@ -1301,6 +1546,17 @@ def generate_html(data: List[Dict[str, Any]], stats: Dict[str, Any],
         </div>
     </div>
 
+    <!-- P值详情模态窗口 -->
+    <div class="p-detail-modal" id="pDetailModal">
+        <div class="p-detail-window">
+            <div class="p-detail-header">
+                <div class="p-detail-title">📊 综合得分P 计算详情</div>
+                <button class="p-detail-close-btn" onclick="closePDetailModal()">×</button>
+            </div>
+            <div class="p-detail-body" id="pDetailContent"></div>
+        </div>
+    </div>
+
     <script>
         // 数据
         const data = {data_json};
@@ -1386,6 +1642,7 @@ def generate_html(data: List[Dict[str, Any]], stats: Dict[str, Any],
                         const score = sw.score || 0;
                         const blockId = `block-${{featureIdx}}-${{groupIdx}}-${{swIdx}}`;
                         const sourceWord = sw.source_word || '';
+                        const searchWord = sw.search_word || '';
 
                         const evaluation = sw['evaluation_with_filter'];
                         let evalBadges = '';
@@ -1404,6 +1661,28 @@ def generate_html(data: List[Dict[str, Any]], stats: Dict[str, Any],
                             if (filtered > 0) evalBadges += `<span class="eval-badge eval-filtered">⚫${{filtered}}</span>`;
                         }}
 
+                        // P值显示
+                        let pScoreHtml = '';
+                        const pScore = sw.comprehensive_score;
+                        const pDetail = sw.comprehensive_score_detail;
+                        if (pScore !== undefined && pScore !== null) {{
+                            const originalFeature = feature['原始特征名称'] || '';
+                            pScoreHtml = `
+                                <div class="p-score-container" onclick="event.stopPropagation();">
+                                    <div class="p-score-arrow">
+                                        <span>${{sourceWord}}</span>
+                                        <span class="p-score-arrow-symbol">→</span>
+                                        <span>${{originalFeature}}</span>
+                                    </div>
+                                </div>
+                                <div class="p-score-container" onclick="event.stopPropagation(); openPDetailModal('${{searchWord}}', ${{featureIdx}}, ${{groupIdx}}, ${{swIdx}});">
+                                    <span class="p-score-label">综合得分P:</span>
+                                    <span class="p-score-value">${{pScore.toFixed(3)}}</span>
+                                    <span class="p-score-info-icon" title="点击查看计算详情">ℹ️</span>
+                                </div>
+                            `;
+                        }}
+
                         html += `
                             <div class="search-word-item" onclick="scrollToBlock('${{blockId}}')"
                                  id="sw-${{featureIdx}}-${{groupIdx}}-${{swIdx}}"
@@ -1415,6 +1694,7 @@ def generate_html(data: List[Dict[str, Any]], stats: Dict[str, Any],
                                     来源: ${{sourceWord}}
                                 </div>
                                 <div class="search-word-eval">${{evalBadges}}</div>
+                                ${{pScoreHtml}}
                             </div>
                         `;
                     }});
@@ -1698,8 +1978,162 @@ def generate_html(data: List[Dict[str, Any]], stats: Dict[str, Any],
             if (e.target === modal) {{
                 closeModal();
             }}
+
+            const pModal = document.getElementById('pDetailModal');
+            if (e.target === pModal) {{
+                closePDetailModal();
+            }}
         }});
 
+        // 打开P值详情模态窗口
+        function openPDetailModal(searchWord, featureIdx, groupIdx, swIdx) {{
+            console.log('🔧 [调试] openPDetailModal被调用, searchWord:', searchWord);
+
+            const modal = document.getElementById('pDetailModal');
+            const modalContent = document.getElementById('pDetailContent');
+
+            if (!modal || !modalContent) {{
+                console.error('❌ [错误] 无法找到P值详情模态窗口元素');
+                return;
+            }}
+
+            // 获取对应的搜索词数据
+            const feature = data[featureIdx];
+            const group = feature['组合评估结果_分组'][groupIdx];
+            const sw = group['top10_searches'][swIdx];
+
+            const pScore = sw.comprehensive_score;
+            const pDetail = sw.comprehensive_score_detail;
+
+            if (!pDetail) {{
+                console.warn('⚠️ [警告] 未找到P值详情数据');
+                modalContent.innerHTML = '<div style="padding: 30px; text-align: center; color: #6b7280;">暂无P值详情数据</div>';
+            }} else {{
+                try {{
+                    modalContent.innerHTML = renderPDetailContent(searchWord, feature['原始特征名称'], sw.source_word, pScore, pDetail);
+                    console.log('✅ [调试] P值详情内容渲染成功');
+                }} catch (error) {{
+                    console.error('❌ [错误] 渲染P值详情内容失败:', error);
+                    modalContent.innerHTML = `<div style="padding: 30px; text-align: center; color: red;">渲染错误: ${{error.message}}</div>`;
+                }}
+            }}
+
+            // 显示模态窗口
+            modal.classList.add('active');
+            document.body.style.overflow = 'hidden';
+            console.log('✅ [调试] P值详情模态窗口已显示');
+        }}
+
+        // 关闭P值详情模态窗口
+        function closePDetailModal() {{
+            console.log('🔧 [调试] closePDetailModal被调用');
+            const modal = document.getElementById('pDetailModal');
+            if (modal) {{
+                modal.classList.remove('active');
+                document.body.style.overflow = '';
+                console.log('✅ [调试] P值详情模态窗口已关闭');
+            }}
+        }}
+
+        // 渲染P值详情内容
+        function renderPDetailContent(searchWord, targetFeature, sourceWord, pScore, pDetail) {{
+            const N = pDetail.N || 0;
+            const M = pDetail.M || 0;
+            const totalContribution = pDetail.total_contribution || 0;
+            const matches = pDetail.complete_matches || [];
+
+            let html = `
+                <div class="p-formula-section">
+                    <div class="p-formula-title">📐 计算公式</div>
+                    <div class="p-formula">P = Σ(a × b) / N = ${{totalContribution.toFixed(3)}} / ${{N}} = ${{pScore.toFixed(3)}}</div>
+                    <div style="font-size: 12px; color: #64748b; margin-top: 8px;">
+                        <div>• a: 评估得分(综合得分)</div>
+                        <div>• b: 最高相似度(解构特征与目标特征的最高相似度)</div>
+                        <div>• N: 总帖子数</div>
+                        <div>• M: 完全匹配数(得分 ≥ 0.8)</div>
+                    </div>
+                </div>
+
+                <div class="p-summary">
+                    <div class="p-summary-item">
+                        <div class="p-summary-label">搜索词</div>
+                        <div class="p-summary-value" style="font-size: 16px;">${{searchWord}}</div>
+                    </div>
+                    <div class="p-summary-item">
+                        <div class="p-summary-label">来源</div>
+                        <div class="p-summary-value" style="font-size: 16px;">${{sourceWord}}</div>
+                    </div>
+                    <div class="p-summary-item">
+                        <div class="p-summary-label">目标特征</div>
+                        <div class="p-summary-value" style="font-size: 16px;">${{targetFeature}}</div>
+                    </div>
+                </div>
+
+                <div class="p-summary">
+                    <div class="p-summary-item">
+                        <div class="p-summary-label">总帖子数 (N)</div>
+                        <div class="p-summary-value">${{N}}</div>
+                    </div>
+                    <div class="p-summary-item">
+                        <div class="p-summary-label">完全匹配数 (M)</div>
+                        <div class="p-summary-value highlight">${{M}}</div>
+                    </div>
+                    <div class="p-summary-item">
+                        <div class="p-summary-label">总贡献值</div>
+                        <div class="p-summary-value highlight">${{totalContribution.toFixed(3)}}</div>
+                    </div>
+                    <div class="p-summary-item">
+                        <div class="p-summary-label">综合得分 (P)</div>
+                        <div class="p-summary-value highlight">${{pScore.toFixed(3)}}</div>
+                    </div>
+                </div>
+            `;
+
+            if (matches.length > 0) {{
+                html += `
+                    <div class="p-matches-section">
+                        <div class="p-matches-title">🎯 完全匹配帖子详情 (${{M}}个)</div>
+                `;
+
+                matches.forEach((match, idx) => {{
+                    html += `
+                        <div class="p-match-item">
+                            <div class="p-match-header">
+                                <div class="p-match-title">${{idx + 1}}. ${{match.note_title || '未知标题'}}</div>
+                                <div class="p-match-contribution">+${{match.contribution.toFixed(3)}}</div>
+                            </div>
+                            <div class="p-match-details">
+                                <div class="p-match-detail-item">
+                                    <span class="p-match-detail-label">评估得分 (a):</span>
+                                    <span>${{match.evaluation_score.toFixed(3)}}</span>
+                                </div>
+                                <div class="p-match-detail-item">
+                                    <span class="p-match-detail-label">最高相似度 (b):</span>
+                                    <span>${{match.max_similarity.toFixed(3)}}</span>
+                                </div>
+                                <div class="p-match-detail-item">
+                                    <span class="p-match-detail-label">贡献值 (a×b):</span>
+                                    <span>${{match.contribution.toFixed(3)}}</span>
+                                </div>
+                            </div>
+                        </div>
+                    `;
+                }});
+
+                html += `</div>`;
+            }} else {{
+                html += `
+                    <div class="p-matches-section">
+                        <div style="text-align: center; padding: 30px; color: #6b7280;">
+                            没有完全匹配的帖子(得分 ≥ 0.8)
+                        </div>
+                    </div>
+                `;
+            }}
+
+            return html;
+        }}
+
         // 渲染解构内容
         function renderDeconstructionContent(noteId) {{
             const stage8Info = stage8Data[noteId];