Ver Fonte

可视化tab1样式修改

jihuaqiang há 4 dias atrás
pai
commit
86ae83b6a8

+ 0 - 2
examples/html/visualize/style.css

@@ -1252,9 +1252,7 @@ body {
     background: white;
     border-radius: 12px;
     padding: 0;
-    max-width: 600px;
     width: 90%;
-    max-height: 80vh;
     overflow-y: auto;
     box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
     animation: slideUp 0.3s ease;

BIN
examples/static/visualize/__pycache__/tab1.cpython-313.pyc


BIN
examples/static/visualize/__pycache__/tab3.cpython-313.pyc


BIN
examples/static/visualize/__pycache__/tab5.cpython-313.pyc


+ 502 - 128
examples/static/visualize/tab1.py

@@ -3,12 +3,489 @@
 Tab1内容生成器 - 选题点(灵感点、目的点、关键点)
 """
 import html as html_module
-from typing import Dict, Any
+from typing import Dict, Any, List
+
+
+def render_inspiration_card(item: Dict[str, Any], idx: int) -> str:
+    """渲染灵感点卡片"""
+    point_text = item.get('灵感点', '')
+    description = item.get('描述', '')
+    category = item.get('分类', '')
+    features = item.get('提取的特征', [])
+    scoring = item.get('scoring', {})
+    reasoning = item.get('推理', '')
+    derivation = item.get('推导说明', '')
+    
+    card_id = f'inspiration-{idx}'
+    has_details = description or features or scoring or reasoning or derivation
+    
+    html = f'<div class="point-card inspiration-card" data-card-id="{card_id}">\n'
+    html += '<div class="point-card-header" onclick="toggleCardDetails(\'' + card_id + '\')">\n'
+    html += f'<span class="point-number">#{idx}</span>\n'
+    html += f'<span class="point-text">{html_module.escape(point_text)}</span>\n'
+    if category:
+        html += f'<span class="point-category">{html_module.escape(category)}</span>\n'
+    if has_details:
+        html += '<span class="toggle-icon">▼</span>\n'
+    html += '</div>\n'
+    
+    if has_details:
+        html += f'<div class="point-card-details" id="{card_id}-details">\n'
+        
+        if description:
+            html += '<div class="detail-section">\n'
+            html += '<strong>描述:</strong>\n'
+            html += f'<div class="detail-text">{html_module.escape(description)}</div>\n'
+            html += '</div>\n'
+        
+        if reasoning:
+            html += '<div class="detail-section">\n'
+            html += '<strong>推理:</strong>\n'
+            html += f'<div class="detail-text">{html_module.escape(reasoning)}</div>\n'
+            html += '</div>\n'
+        
+        if derivation:
+            html += '<div class="detail-section">\n'
+            html += '<strong>推导说明:</strong>\n'
+            html += f'<div class="detail-text">{html_module.escape(derivation)}</div>\n'
+            html += '</div>\n'
+        
+        if features:
+            html += '<div class="detail-section">\n'
+            html += '<strong>提取的特征:</strong>\n'
+            html += '<div class="feature-tags">\n'
+            for feature in features:
+                feature_name = feature.get('特征名称', '')
+                weight = feature.get('权重', 0)
+                dimension = feature.get('维度分类', '')
+                html += f'<span class="feature-tag">{html_module.escape(feature_name)} '
+                if dimension:
+                    html += f'<em class="feature-dimension">({html_module.escape(dimension)})</em> '
+                html += f'<strong class="feature-weight">权重:{weight}</strong></span>\n'
+            html += '</div>\n'
+            html += '</div>\n'
+        
+        if scoring:
+            html += '<div class="detail-section">\n'
+            html += '<strong>评分:</strong>\n'
+            html += '<div class="scoring-badges">\n'
+            if '人设契合度' in scoring:
+                html += f'<span class="scoring-badge">人设契合度: {scoring["人设契合度"]}/10</span>\n'
+            if '触发可能性' in scoring:
+                html += f'<span class="scoring-badge">触发可能性: {scoring["触发可能性"]}/10</span>\n'
+            if '内容解释力' in scoring:
+                html += f'<span class="scoring-badge">内容解释力: {scoring["内容解释力"]}/10</span>\n'
+            if '总分' in scoring:
+                html += f'<span class="scoring-badge total-score">总分: {scoring["总分"]}</span>\n'
+            if '评分说明' in scoring:
+                html += f'<div class="scoring-note">{html_module.escape(scoring["评分说明"])}</div>\n'
+            html += '</div>\n'
+            html += '</div>\n'
+        
+        html += '</div>\n'
+    
+    html += '</div>\n'
+    return html
+
+
+def render_purpose_card(item: Dict[str, Any], idx: int) -> str:
+    """渲染目的点卡片"""
+    point_text = item.get('目的点', '')
+    description = item.get('描述', '')
+    dimension = item.get('维度', {})
+    features = item.get('提取的特征', [])
+    reasoning = item.get('推理', '')
+    
+    card_id = f'purpose-{idx}'
+    has_details = description or features or reasoning
+    
+    html = f'<div class="point-card purpose-card" data-card-id="{card_id}">\n'
+    html += '<div class="point-card-header" onclick="toggleCardDetails(\'' + card_id + '\')">\n'
+    html += f'<span class="point-number">#{idx}</span>\n'
+    html += f'<span class="point-text">{html_module.escape(point_text)}</span>\n'
+    
+    # 显示维度标签
+    if dimension:
+        html += '<div class="dimension-tags">\n'
+        if '一级分类' in dimension:
+            html += f'<span class="dimension-tag level-1">{html_module.escape(dimension["一级分类"])}</span>\n'
+        if '二级分类' in dimension:
+            html += f'<span class="dimension-tag level-2">{html_module.escape(dimension["二级分类"])}</span>\n'
+        html += '</div>\n'
+    
+    if has_details:
+        html += '<span class="toggle-icon">▼</span>\n'
+    html += '</div>\n'
+    
+    if has_details:
+        html += f'<div class="point-card-details" id="{card_id}-details">\n'
+        
+        if description:
+            html += '<div class="detail-section">\n'
+            html += '<strong>描述:</strong>\n'
+            html += f'<div class="detail-text">{html_module.escape(description)}</div>\n'
+            html += '</div>\n'
+        
+        if reasoning:
+            html += '<div class="detail-section">\n'
+            html += '<strong>推理:</strong>\n'
+            html += f'<div class="detail-text">{html_module.escape(reasoning)}</div>\n'
+            html += '</div>\n'
+        
+        if features:
+            html += '<div class="detail-section">\n'
+            html += '<strong>提取的特征:</strong>\n'
+            html += '<div class="feature-tags">\n'
+            for feature in features:
+                feature_name = feature.get('特征名称', '')
+                weight = feature.get('权重', 0)
+                feature_class = feature.get('特征分类', '')
+                html += f'<span class="feature-tag">{html_module.escape(feature_name)} '
+                if feature_class:
+                    html += f'<em class="feature-dimension">({html_module.escape(feature_class)})</em> '
+                html += f'<strong class="feature-weight">权重:{weight}</strong></span>\n'
+            html += '</div>\n'
+            html += '</div>\n'
+        
+        html += '</div>\n'
+    
+    html += '</div>\n'
+    return html
+
+
+def render_keypoint_card(item: Dict[str, Any], idx: int, level: int = 0) -> str:
+    """渲染关键点卡片(支持嵌套)"""
+    point_text = item.get('关键点', '')
+    description = item.get('描述', '')
+    dimension = item.get('维度', '') or item.get('维度细分', '')
+    dimension_category = item.get('维度大类', '')
+    features = item.get('提取的特征', [])
+    children = item.get('children', [])
+    child_reason = item.get('作为子节点的原因', '')
+    
+    card_id = f'keypoint-{idx}-l{level}'
+    has_details = description or features or child_reason
+    has_children = len(children) > 0
+    
+    indent_class = f'keypoint-level-{level}' if level > 0 else ''
+    
+    html = f'<div class="point-card keypoint-card {indent_class}" data-card-id="{card_id}" data-level="{level}">\n'
+    html += '<div class="point-card-header" onclick="toggleCardDetails(\'' + card_id + '\')">\n'
+    html += f'<span class="point-number">#{item.get("候选编号", idx)}</span>\n'
+    html += f'<span class="point-text">{html_module.escape(point_text)}</span>\n'
+    
+    # 显示维度大类标签(实质类/形式类)
+    if dimension_category:
+        category_label = '形式类' if dimension_category == '形式' else '实质类'
+        category_class = 'dimension-category-form' if dimension_category == '形式' else 'dimension-category-substance'
+        html += f'<span class="dimension-category {category_class}">{html_module.escape(category_label)}</span>\n'
+    
+    # 显示维度标签(维度细分)
+    if dimension:
+        html += f'<span class="dimension-tag keypoint-dimension">{html_module.escape(dimension)}</span>\n'
+    
+    if has_details or has_children:
+        html += '<span class="toggle-icon">▼</span>\n'
+    html += '</div>\n'
+    
+    if has_details or has_children:
+        html += f'<div class="point-card-details" id="{card_id}-details">\n'
+        
+        if description:
+            html += '<div class="detail-section">\n'
+            html += '<strong>描述:</strong>\n'
+            html += f'<div class="detail-text">{html_module.escape(description)}</div>\n'
+            html += '</div>\n'
+        
+        if child_reason:
+            html += '<div class="detail-section">\n'
+            html += '<strong>作为子节点的原因:</strong>\n'
+            html += f'<div class="detail-text">{html_module.escape(child_reason)}</div>\n'
+            html += '</div>\n'
+        
+        if features:
+            html += '<div class="detail-section">\n'
+            html += '<strong>提取的特征:</strong>\n'
+            html += '<div class="feature-tags">\n'
+            for feature in features:
+                feature_name = feature.get('特征名称', '')
+                weight = feature.get('权重', 0)
+                feature_dimension = feature.get('维度', '')
+                html += f'<span class="feature-tag">{html_module.escape(feature_name)} '
+                if feature_dimension:
+                    html += f'<em class="feature-dimension">({html_module.escape(feature_dimension)})</em> '
+                html += f'<strong class="feature-weight">权重:{weight}</strong></span>\n'
+            html += '</div>\n'
+            html += '</div>\n'
+        
+        # 递归渲染子关键点
+        if has_children:
+            html += '<div class="detail-section keypoint-children">\n'
+            html += '<strong>子关键点:</strong>\n'
+            html += '<div class="keypoint-children-list">\n'
+            for child_idx, child in enumerate(children, 1):
+                html += render_keypoint_card(child, child_idx, level + 1)
+            html += '</div>\n'
+            html += '</div>\n'
+        
+        html += '</div>\n'
+    
+    html += '</div>\n'
+    return html
 
 
 def generate_tab1_content(data: Dict[str, Any]) -> str:
     """生成Tab1内容:选题、灵感点、目的点、关键点"""
     html = '<div class="tab-content" id="tab1">\n'
+    
+    # 添加CSS样式
+    html += '''
+    <style>
+    .point-card {
+        background: white;
+        border-radius: 8px;
+        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+        margin-bottom: 16px;
+        transition: all 0.3s ease;
+        overflow: hidden;
+    }
+    
+    .point-card:hover {
+        box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
+        transform: translateY(-2px);
+    }
+    
+    .point-card-header {
+        display: flex;
+        align-items: center;
+        gap: 12px;
+        padding: 16px 20px;
+        cursor: pointer;
+        user-select: none;
+        border-bottom: 1px solid #f0f0f0;
+    }
+    
+    .point-card-header:hover {
+        background-color: #f8f8f8;
+    }
+    
+    .point-number {
+        color: #667eea;
+        font-weight: 600;
+        font-size: 14px;
+        min-width: 40px;
+    }
+    
+    .point-text {
+        flex: 1;
+        font-size: 16px;
+        font-weight: 500;
+        color: #333;
+    }
+    
+    .point-category {
+        background-color: #e3f2fd;
+        color: #1976d2;
+        padding: 4px 12px;
+        border-radius: 12px;
+        font-size: 12px;
+        font-weight: 500;
+    }
+    
+    .dimension-tags {
+        display: flex;
+        gap: 8px;
+    }
+    
+    .dimension-tag {
+        background-color: #f3e5f5;
+        color: #7b1fa2;
+        padding: 4px 12px;
+        border-radius: 12px;
+        font-size: 12px;
+        font-weight: 500;
+    }
+    
+    .dimension-tag.level-1 {
+        background-color: #e1bee7;
+        color: #6a1b9a;
+    }
+    
+    .dimension-tag.level-2 {
+        background-color: #f3e5f5;
+        color: #7b1fa2;
+    }
+    
+    .dimension-category {
+        padding: 4px 12px;
+        border-radius: 12px;
+        font-size: 12px;
+        font-weight: 600;
+    }
+    
+    .dimension-category-form {
+        background-color: #fff3e0;
+        color: #e65100;
+    }
+    
+    .dimension-category-substance {
+        background-color: #e8f5e9;
+        color: #2e7d32;
+    }
+    
+    .toggle-icon {
+        color: #999;
+        font-size: 12px;
+        transition: transform 0.3s ease;
+    }
+    
+    .point-card.expanded .toggle-icon {
+        transform: rotate(180deg);
+    }
+    
+    .point-card-details {
+        display: none;
+        padding: 20px;
+        background-color: #fafafa;
+    }
+    
+    .point-card.expanded .point-card-details {
+        display: block;
+    }
+    
+    .detail-section {
+        margin-bottom: 20px;
+    }
+    
+    .detail-section:last-child {
+        margin-bottom: 0;
+    }
+    
+    .detail-section strong {
+        display: block;
+        color: #667eea;
+        font-size: 14px;
+        font-weight: 600;
+        margin-bottom: 8px;
+    }
+    
+    .detail-text {
+        background-color: white;
+        padding: 12px 16px;
+        border-radius: 6px;
+        line-height: 1.6;
+        color: #444;
+        font-size: 14px;
+        border-left: 3px solid #667eea;
+    }
+    
+    .feature-tags {
+        display: flex;
+        flex-wrap: wrap;
+        gap: 8px;
+        margin-top: 8px;
+    }
+    
+    .feature-tag {
+        background-color: #e8eaf6;
+        color: #3f51b5;
+        padding: 6px 12px;
+        border-radius: 15px;
+        font-size: 13px;
+    }
+    
+    .feature-dimension {
+        color: #7986cb;
+        font-style: italic;
+    }
+    
+    .feature-weight {
+        color: #1a237e;
+        margin-left: 4px;
+    }
+    
+    .scoring-badges {
+        display: flex;
+        flex-wrap: wrap;
+        gap: 8px;
+        margin-top: 8px;
+    }
+    
+    .scoring-badge {
+        background-color: #e0f2f1;
+        color: #00695c;
+        padding: 6px 12px;
+        border-radius: 15px;
+        font-size: 13px;
+        font-weight: 500;
+    }
+    
+    .scoring-badge.total-score {
+        background-color: #667eea;
+        color: white;
+        font-weight: 600;
+    }
+    
+    .scoring-note {
+        margin-top: 12px;
+        padding: 12px;
+        background-color: #fff3cd;
+        border-left: 3px solid #ffc107;
+        border-radius: 4px;
+        font-size: 13px;
+        color: #856404;
+        line-height: 1.6;
+    }
+    
+    /* 关键点嵌套样式 */
+    .keypoint-card.keypoint-level-1 {
+        margin-left: 24px;
+        border-left: 3px solid #4facfe;
+    }
+    
+    .keypoint-card.keypoint-level-2 {
+        margin-left: 48px;
+        border-left: 3px solid #43e97b;
+    }
+    
+    .keypoint-card.keypoint-level-3 {
+        margin-left: 72px;
+        border-left: 3px solid #fa709a;
+    }
+    
+    .keypoint-children {
+        margin-top: 16px;
+    }
+    
+    .keypoint-children-list {
+        margin-top: 12px;
+    }
+    
+    .inspiration-card {
+        border-left: 4px solid #667eea;
+    }
+    
+    .purpose-card {
+        border-left: 4px solid #f093fb;
+    }
+    
+    .keypoint-card {
+        border-left: 4px solid #4facfe;
+    }
+    </style>
+    '''
+    
+    # 添加JavaScript
+    html += '''
+    <script>
+    function toggleCardDetails(cardId) {
+        const card = document.querySelector(`[data-card-id="${cardId}"]`);
+        if (card) {
+            card.classList.toggle('expanded');
+        }
+    }
+    </script>
+    '''
 
     # 选题描述
     if '选题描述' in data:
@@ -26,157 +503,54 @@ def generate_tab1_content(data: Dict[str, Any]) -> str:
         inspiration = data['灵感点']
         html += '<div class="section">\n'
         html += '<h3>灵感点</h3>\n'
+        html += '<div class="point-cards-list">\n'
 
         if isinstance(inspiration, list):
             for idx, item in enumerate(inspiration, 1):
-                point_text = item.get('灵感点', '')
-                description = item.get('描述', '')
-                features = item.get('提取的特征', [])
-                scoring = item.get('scoring', {})
-
-                html += f'<div class="point-item inspiration-item">\n'
-                html += f'<div class="point-header">\n'
-                html += f'<span class="point-number">#{idx}</span>\n'
-                html += f'<span class="point-text">{html_module.escape(point_text)}</span>\n'
-                html += f'</div>\n'
-
-                if description:
-                    html += f'<div class="point-description">{html_module.escape(description)}</div>\n'
-
-                # 显示提取的特征
-                if features:
-                    html += f'<div class="point-features">\n'
-                    html += f'<strong>提取的特征:</strong>\n'
-                    html += f'<div class="feature-tags">\n'
-                    for feature in features:
-                        feature_name = feature.get('特征名称', '')
-                        weight = feature.get('权重', 0)
-                        dimension = feature.get('维度分类', '')
-                        html += f'<span class="feature-tag">{html_module.escape(feature_name)} '
-                        if dimension:
-                            html += f'<em class="feature-dimension">({html_module.escape(dimension)})</em> '
-                        html += f'<strong class="feature-weight">权重:{weight}</strong></span>\n'
-                    html += f'</div>\n'
-                    html += f'</div>\n'
-
-                # 显示评分信息
-                if scoring:
-                    html += f'<div class="point-scoring">\n'
-                    html += f'<strong>评分:</strong>\n'
-                    html += f'<div class="scoring-badges">\n'
-                    if '人设契合度' in scoring:
-                        html += f'<span class="scoring-badge">人设契合度: {scoring["人设契合度"]}/10</span>\n'
-                    if '触发可能性' in scoring:
-                        html += f'<span class="scoring-badge">触发可能性: {scoring["触发可能性"]}/10</span>\n'
-                    if '内容解释力' in scoring:
-                        html += f'<span class="scoring-badge">内容解释力: {scoring["内容解释力"]}/10</span>\n'
-                    if '总分' in scoring:
-                        html += f'<span class="scoring-badge total-score">总分: {scoring["总分"]}</span>\n'
-                    html += f'</div>\n'
-                    html += f'</div>\n'
-
-                html += f'</div>\n'
+                html += render_inspiration_card(item, idx)
 
         html += '</div>\n'
+        html += '</div>\n'
 
     # 目的点
     if '目的点' in data:
         purpose = data['目的点']
         html += '<div class="section">\n'
         html += '<h3>目的点</h3>\n'
+        html += '<div class="point-cards-list">\n'
 
-        if isinstance(purpose, list):
-            for idx, item in enumerate(purpose, 1):
-                point_text = item.get('目的点', '')
-                description = item.get('描述', '')
-                dimension = item.get('维度', {})
-                features = item.get('提取的特征', [])
+        if isinstance(purpose, dict) and 'purposes' in purpose:
+            purpose_list = purpose['purposes']
+        elif isinstance(purpose, list):
+            purpose_list = purpose
+        else:
+            purpose_list = []
 
-                html += f'<div class="point-item purpose-item">\n'
-                html += f'<div class="point-header">\n'
-                html += f'<span class="point-number">#{idx}</span>\n'
-                html += f'<span class="point-text">{html_module.escape(point_text)}</span>\n'
-
-                # 显示维度标签
-                if dimension:
-                    html += f'<div class="dimension-tags">\n'
-                    if '一级分类' in dimension:
-                        html += f'<span class="dimension-tag level-1">{html_module.escape(dimension["一级分类"])}</span>\n'
-                    if '二级分类' in dimension:
-                        html += f'<span class="dimension-tag level-2">{html_module.escape(dimension["二级分类"])}</span>\n'
-                    html += f'</div>\n'
-
-                html += f'</div>\n'
-
-                if description:
-                    html += f'<div class="point-description">{html_module.escape(description)}</div>\n'
-
-                # 显示提取的特征
-                if features:
-                    html += f'<div class="point-features">\n'
-                    html += f'<strong>提取的特征:</strong>\n'
-                    html += f'<div class="feature-tags">\n'
-                    for feature in features:
-                        feature_name = feature.get('特征名称', '')
-                        weight = feature.get('权重', 0)
-                        feature_class = feature.get('特征分类', '')
-                        html += f'<span class="feature-tag">{html_module.escape(feature_name)} '
-                        if feature_class:
-                            html += f'<em class="feature-dimension">({html_module.escape(feature_class)})</em> '
-                        html += f'<strong class="feature-weight">权重:{weight}</strong></span>\n'
-                    html += f'</div>\n'
-                    html += f'</div>\n'
-
-                html += f'</div>\n'
+        if isinstance(purpose_list, list):
+            for idx, item in enumerate(purpose_list, 1):
+                html += render_purpose_card(item, idx)
 
         html += '</div>\n'
+        html += '</div>\n'
 
     # 关键点
     if '关键点' in data:
         keypoint = data['关键点']
         html += '<div class="section">\n'
         html += '<h3>关键点</h3>\n'
+        html += '<div class="point-cards-list">\n'
 
-        if isinstance(keypoint, list):
-            for idx, item in enumerate(keypoint, 1):
-                point_text = item.get('关键点', '')
-                description = item.get('描述', '')
-                dimension = item.get('维度', '')
-                features = item.get('提取的特征', [])
-
-                html += f'<div class="point-item keypoint-item">\n'
-                html += f'<div class="point-header">\n'
-                html += f'<span class="point-number">#{idx}</span>\n'
-                html += f'<span class="point-text">{html_module.escape(point_text)}</span>\n'
-
-                # 显示维度标签
-                if dimension:
-                    html += f'<span class="dimension-tag keypoint-dimension">{html_module.escape(dimension)}</span>\n'
-
-                html += f'</div>\n'
-
-                if description:
-                    html += f'<div class="point-description">{html_module.escape(description)}</div>\n'
-
-                # 显示提取的特征
-                if features:
-                    html += f'<div class="point-features">\n'
-                    html += f'<strong>提取的特征:</strong>\n'
-                    html += f'<div class="feature-tags">\n'
-                    for feature in features:
-                        feature_name = feature.get('特征名称', '')
-                        weight = feature.get('权重', 0)
-                        feature_dimension = feature.get('维度', '')
-                        html += f'<span class="feature-tag">{html_module.escape(feature_name)} '
-                        if feature_dimension:
-                            html += f'<em class="feature-dimension">({html_module.escape(feature_dimension)})</em> '
-                        html += f'<strong class="feature-weight">权重:{weight}</strong></span>\n'
-                    html += f'</div>\n'
-                    html += f'</div>\n'
-
-                html += f'</div>\n'
+        # 处理关键点数据:可能是列表,也可能是包含key_points的对象
+        keypoint_list = keypoint
+        if isinstance(keypoint, dict) and 'key_points' in keypoint:
+            keypoint_list = keypoint['key_points']
+        
+        if isinstance(keypoint_list, list):
+            for idx, item in enumerate(keypoint_list, 1):
+                html += render_keypoint_card(item, idx, 0)
 
         html += '</div>\n'
+        html += '</div>\n'
 
     html += '</div>\n'
     return html

+ 973 - 106
examples/static/visualize/tab3.py

@@ -59,6 +59,46 @@ def calculate_intent_support_count(element: Dict[str, Any]) -> int:
     return total_support_count
 
 
+def get_element_coverage(element: Dict[str, Any]) -> float:
+    """
+    获取元素的段落覆盖率(兼容形式元素和实质元素)
+    
+    对于实质元素:从"共性分析"字段获取
+    对于形式元素:从"权重明细"中的"覆盖率分"反推
+    根据script_form_extraction_agent.py的逻辑:
+    - coverage_rate_base_score = coverage_rate * 50 (基础分0-50)
+    - coverage_rate_score = coverage_rate_base_score * 0.3 (加权后0-15)
+    所以:coverage_rate = coverage_rate_score / 15
+    
+    Args:
+        element: 元素数据
+        
+    Returns:
+        段落覆盖率(0.0-1.0)
+    """
+    dimension = element.get('维度', {})
+    is_form = isinstance(dimension, dict) and dimension.get('一级') == '形式'
+    
+    if is_form:
+        # 形式元素:从权重明细中反推覆盖率
+        weight_details = element.get('权重明细', {})
+        if weight_details:
+            coverage_score = float(weight_details.get('覆盖率分', 0) or 0)
+            # 覆盖率分 = 覆盖率 × 50 × 0.3 = 覆盖率 × 15
+            # 所以:覆盖率 = 覆盖率分 / 15
+            if coverage_score > 0:
+                coverage = coverage_score / 15.0
+                return min(1.0, max(0.0, coverage))
+        
+        # 如果权重明细中没有覆盖率分,返回0
+        return 0.0
+    else:
+        # 实质元素:从共性分析中获取
+        commonality = element.get('共性分析') or {}
+        coverage = float(commonality.get('段落覆盖率', 0.0) or 0.0)
+        return coverage
+
+
 def get_support_stats(element: Dict[str, Any]) -> Dict[str, int]:
     """
     获取元素的支撑统计信息(灵感点/目的点/关键点数量)
@@ -379,7 +419,7 @@ def group_elements_by_hierarchical_category(elements: List[Dict[str, Any]]) -> D
             continue
 
         # 计算统计指标
-        avg_coverage = sum((e.get('共性分析') or {}).get('段落覆盖率', 0.0) for e in all_elements) / len(all_elements)
+        avg_coverage = sum(get_element_coverage(e) for e in all_elements) / len(all_elements)
         avg_frequency = sum((e.get('共性分析') or {}).get('出现频次', 0) for e in all_elements) / len(all_elements)
         avg_intent_count = sum(calculate_intent_support_count(e) for e in all_elements) / len(all_elements)
 
@@ -406,7 +446,7 @@ def group_elements_by_hierarchical_category(elements: List[Dict[str, Any]]) -> D
                 level2_scores[level2_name] = (0.0, 0, 0.0)
                 continue
 
-            avg_coverage = sum((e.get('共性分析') or {}).get('段落覆盖率', 0.0) for e in level2_elements) / len(level2_elements)
+            avg_coverage = sum(get_element_coverage(e) for e in level2_elements) / len(level2_elements)
             avg_frequency = sum((e.get('共性分析') or {}).get('出现频次', 0) for e in level2_elements) / len(level2_elements)
             avg_intent_count = sum(calculate_intent_support_count(e) for e in level2_elements) / len(level2_elements)
 
@@ -427,7 +467,7 @@ def group_elements_by_hierarchical_category(elements: List[Dict[str, Any]]) -> D
 
 
 def render_element_item(element: Dict[str, Any], all_elements: List[Dict[str, Any]] = None) -> str:
-    """渲染单个元素项的HTML(支持详情展开,兼容新旧数据结构
+    """渲染单个元素项的HTML(卡片样式,详细信息在弹窗中
 
     Args:
         element: 元素数据
@@ -446,37 +486,15 @@ def render_element_item(element: Dict[str, Any], all_elements: List[Dict[str, An
         elem_type = element.get('类型', '')
         elem_type_level2 = ''
 
-    # 获取分类(兼容新旧结构)
-    category_data = element.get('分类', '')
-    if isinstance(category_data, dict):
-        category_level1 = category_data.get('一级分类', '')
-        category_level2 = category_data.get('二级分类', '')
-        category = get_element_category(element)
-    elif isinstance(category_data, list):
-        category_level1 = category_data[0] if len(category_data) > 0 else ''
-        category_level2 = category_data[1] if len(category_data) > 1 else ''
-        category = get_element_category(element)
-    else:
-        category = category_data
-        category_level1 = ''
-        category_level2 = ''
-
-    category_def = element.get('分类定义', '')
     # 获取共性分析(防止为None)
     commonality = element.get('共性分析') or {}
     coverage = commonality.get('段落覆盖率', 0.0)
     frequency = commonality.get('出现频次', 0)
-    paragraphs_list = commonality.get('出现段落列表', [])
-    source = element.get('来源', [])
     intent_count = calculate_intent_support_count(element)
-    intent_support = get_intent_support_data(element)
 
-    # 计算权重得分(如果有 weight_details)
+    # 计算权重得分
     weight_info = compute_weight_scores(element)
 
-    # 检查是否有详细信息
-    has_details = bool(elem_type or category or category_def or paragraphs_list or source or intent_support or weight_info.get('raw_total', 0) > 0)
-
     # 计算主导因素
     dominant_factor = 'coverage'  # 默认
     if all_elements:
@@ -485,91 +503,118 @@ def render_element_item(element: Dict[str, Any], all_elements: List[Dict[str, An
     # 根据主导因素确定边框颜色
     border_color_class = f'dominant-{dominant_factor}'
 
-    html = f'<li class="element-item {border_color_class}" data-elem-id="{elem_id}">\n'
-    html += '<div class="element-header" onclick="toggleElementDetails(this)">\n'
-
-    # 添加展开/收起图标
-    if has_details:
-        html += '<span class="element-toggle-icon">▶</span>\n'
-
-    # 显示ID和名称
-    if elem_id:
-        html += f'<span class="element-id">#{elem_id}</span>\n'
-    html += f'<span class="element-name">{html_module.escape(name)}</span>\n'
+    # 检查是否有详细信息
+    category_def = element.get('分类定义', '')
+    paragraphs_list = commonality.get('出现段落列表', [])
+    source = element.get('来源', [])
+    intent_support = get_intent_support_data(element)
+    has_details = bool(category_def or paragraphs_list or source or intent_support or weight_info.get('raw_total', 0) > 0)
 
-    # 显示统计指标(根据主导因素高亮)
     # 判断是否为形式元素
     is_form = isinstance(dimension, dict) and dimension.get('一级') == '形式'
 
-    html += '<div class="element-stats">\n'
+    # 卡片样式
+    html = f'<li class="element-card {border_color_class}" data-elem-id="{elem_id}">\n'
+    html += '<div class="element-card-body">\n'
+    
+    # 卡片头部
+    html += '<div class="element-card-header">\n'
+    if elem_id:
+        html += f'<span class="element-card-id">#{elem_id}</span>\n'
+    html += f'<h4 class="element-card-name">{html_module.escape(name)}</h4>\n'
+    html += '</div>\n'
 
-    # 显示权重分(如有)
+    # 统计指标
+    html += '<div class="element-card-stats">\n'
     if weight_info.get('raw_total', 0) > 0:
         html += f'<span class="stat-badge stat-weight">权重分: {weight_info["weight_score"]:.1f}</span>\n'
-
+    
     if is_form:
-        # 形式元素:显示支撑数量和权重分
         html += f'<span class="stat-badge stat-intent">支撑: {intent_count}</span>\n'
-        # 权重分已在上面显示
     else:
-        # 实质元素显示全部三个指标
         coverage_highlight = 'stat-highlight' if dominant_factor == 'coverage' else ''
         frequency_highlight = 'stat-highlight' if dominant_factor == 'frequency' else ''
         intent_highlight = 'stat-highlight' if dominant_factor == 'intent_support' else ''
-
+        
         html += f'<span class="stat-badge stat-coverage {coverage_highlight}">覆盖率: {coverage:.2%}</span>\n'
         html += f'<span class="stat-badge stat-frequency {frequency_highlight}">频次: {frequency}</span>\n'
         html += f'<span class="stat-badge stat-intent {intent_highlight}">意图支撑: {intent_count}</span>\n'
-
     html += '</div>\n'
 
+    # 描述
+    if description:
+        html += f'<div class="element-card-description">{html_module.escape(description)}</div>\n'
+
+    # 查看详情按钮
+    if has_details:
+        html += f'<button class="element-detail-btn" onclick="openElementModal(\'{elem_id}\')">查看详情</button>\n'
+
+    html += '</div>\n'  # element-card-body
+    html += '</li>\n'
+    return html
+
+
+def render_element_modal(element: Dict[str, Any]) -> str:
+    """渲染元素详情的弹窗内容
+
+    Args:
+        element: 元素数据
+    """
+    elem_id = element.get('id', '')
+    name = element.get('名称') or ''
+    description = element.get('描述') or ''
+
+    # 获取类型和维度
+    dimension = element.get('维度', {})
+    if isinstance(dimension, dict):
+        elem_type = dimension.get('一级', '')
+        elem_type_level2 = dimension.get('二级', '')
+    else:
+        elem_type = element.get('类型', '')
+        elem_type_level2 = ''
+
+    # 获取分类
+    category_data = element.get('分类', '')
+    category_def = element.get('分类定义', '')
+    
+    # 获取共性分析
+    commonality = element.get('共性分析') or {}
+    coverage = commonality.get('段落覆盖率', 0.0)
+    frequency = commonality.get('出现频次', 0)
+    paragraphs_list = commonality.get('出现段落列表', [])
+    source = element.get('来源', [])
+    intent_support = get_intent_support_data(element)
+    weight_info = compute_weight_scores(element)
+
+    html = f'<div class="element-modal-content" data-elem-id="{elem_id}">\n'
+    html += '<div class="modal-header">\n'
+    if elem_id:
+        html += f'<span class="modal-id">#{elem_id}</span>\n'
+    html += f'<h3 class="modal-title">{html_module.escape(name)}</h3>\n'
+    html += '<button class="modal-close" onclick="closeElementModal()">&times;</button>\n'
     html += '</div>\n'
 
-    # 描述(始终显示)
+    html += '<div class="modal-body">\n'
+
+    # 描述
     if description:
-        html += f'<div class="element-description">{html_module.escape(description)}</div>\n'
+        html += '<div class="detail-section">\n'
+        html += '<strong>描述:</strong>\n'
+        html += f'<div class="detail-text">{html_module.escape(description)}</div>\n'
+        html += '</div>\n'
 
-    # 详细信息(可展开)
-    if has_details:
-        html += '<div class="element-details collapsed">\n'
-
-        # 维度/类型 - 已移除,不再展示
-        # if elem_type:
-        #     html += '<div class="detail-section">\n'
-        #     html += '<strong>维度:</strong>\n'
-        #     html += '<div class="detail-content">\n'
-        #     html += f'<span class="detail-tag dimension-level1">{html_module.escape(elem_type)}</span>\n'
-        #     if elem_type_level2:
-        #         html += f'<span class="detail-tag dimension-level2">{html_module.escape(elem_type_level2)}</span>\n'
-        #     html += '</div>\n'
-        #     html += '</div>\n'
-
-        # 分类 - 已移除,不再展示
-        # if category_level1 or category:
-        #     html += '<div class="detail-section">\n'
-        #     html += '<strong>分类:</strong>\n'
-        #     html += '<div class="detail-content">\n'
-        #     if category_level1:
-        #         html += f'<span class="detail-tag category-level1">{html_module.escape(category_level1)}</span>\n'
-        #         if category_level2:
-        #             html += f'<span class="detail-tag category-level2">{html_module.escape(category_level2)}</span>\n'
-        #     else:
-        #         html += f'<span class="detail-tag">{html_module.escape(str(category))}</span>\n'
-        #     html += '</div>\n'
-        #     html += '</div>\n'
-
-        # 分类定义
-        if category_def:
-            html += '<div class="detail-section">\n'
-            html += '<strong>分类定义:</strong>\n'
-            html += f'<div class="detail-text">{html_module.escape(category_def)}</div>\n'
-            html += '</div>\n'
+    # 分类定义
+    if category_def:
+        html += '<div class="detail-section">\n'
+        html += '<strong>分类定义:</strong>\n'
+        html += f'<div class="detail-text">{html_module.escape(category_def)}</div>\n'
+        html += '</div>\n'
 
-        # 针对"形式"维度,显示"支撑"、"推理"和"支撑关系"
-        if isinstance(dimension, dict) and dimension.get('一级') == '形式':
-            # 支撑
-            zhicheng = element.get('支撑')
-            if zhicheng:
+    # 针对"形式"维度,显示"支撑"、"推理"和"支撑关系"
+    if isinstance(dimension, dict) and dimension.get('一级') == '形式':
+        # 支撑
+        zhicheng = element.get('支撑')
+        if zhicheng:
                 html += '<div class="detail-section">\n'
                 html += '<strong>支撑:</strong>\n'
                 html += '<div class="detail-content">\n'
@@ -600,22 +645,21 @@ def render_element_item(element: Dict[str, Any], all_elements: List[Dict[str, An
                 html += '</div>\n'
                 html += '</div>\n'
 
-            # 推理
-            tuili = element.get('推理')
-            if tuili:
+        # 推理
+        tuili = element.get('推理')
+        if tuili:
                 html += '<div class="detail-section">\n'
                 html += '<strong>推理:</strong>\n'
                 html += f'<div class="detail-text">{html_module.escape(tuili)}</div>\n'
                 html += '</div>\n'
 
-            # 支撑关系(形式元素专用,从意图支撑数据中显示支撑点)
-            # 优先使用"意图支撑"字段,如果没有则使用"多维度评分"字段
-            intent_support_data = get_intent_support_data(element)
-            if intent_support_data:
-                html += '<div class="detail-section">\n'
-                html += '<strong>支撑关系:</strong>\n'
-
-                for point_type in ['灵感点', '目的点', '关键点']:
+        # 支撑关系(形式元素专用,从意图支撑数据中显示支撑点)
+        # 优先使用"意图支撑"字段,如果没有则使用"多维度评分"字段
+        intent_support_data = get_intent_support_data(element)
+        if intent_support_data:
+            html += '<div class="detail-section">\n'
+            html += '<strong>支撑关系:</strong>\n'
+            for point_type in ['灵感点', '目的点', '关键点']:
                     support_points = intent_support_data.get(point_type) or []
                     if not support_points:
                         continue
@@ -651,8 +695,7 @@ def render_element_item(element: Dict[str, Any], all_elements: List[Dict[str, An
                         html += '</div>\n'
 
                     html += '</div>\n'  # end score-list
-
-                html += '</div>\n'  # end detail-section
+            html += '</div>\n'  # end detail-section
 
         # 针对"隐含概念",显示"来源"(声音特征、语气语调、BGM、音效等)和"时间范围"
         elem_type = element.get('类型', '')
@@ -888,11 +931,342 @@ def render_element_item(element: Dict[str, Any], all_elements: List[Dict[str, An
     return html
 
 
+def render_element_modal(element: Dict[str, Any]) -> str:
+    """渲染元素详情的弹窗内容
+
+    Args:
+        element: 元素数据
+    """
+    elem_id = element.get('id', '')
+    name = element.get('名称') or ''
+    description = element.get('描述') or ''
+
+    # 获取类型和维度
+    dimension = element.get('维度', {})
+    if isinstance(dimension, dict):
+        elem_type = dimension.get('一级', '')
+        elem_type_level2 = dimension.get('二级', '')
+    else:
+        elem_type = element.get('类型', '')
+        elem_type_level2 = ''
+
+    # 获取分类
+    category_data = element.get('分类', '')
+    category_def = element.get('分类定义', '')
+    
+    # 获取共性分析
+    commonality = element.get('共性分析') or {}
+    coverage = commonality.get('段落覆盖率', 0.0)
+    frequency = commonality.get('出现频次', 0)
+    paragraphs_list = commonality.get('出现段落列表', [])
+    source = element.get('来源', [])
+    intent_support = get_intent_support_data(element)
+    weight_info = compute_weight_scores(element)
+
+    html = f'<div class="element-modal-content" data-elem-id="{elem_id}">\n'
+    html += '<div class="modal-header">\n'
+    if elem_id:
+        html += f'<span class="modal-id">#{elem_id}</span>\n'
+    html += f'<h3 class="modal-title">{html_module.escape(name)}</h3>\n'
+    html += '<button class="modal-close" onclick="closeElementModal()">&times;</button>\n'
+    html += '</div>\n'
+
+    html += '<div class="modal-body">\n'
+
+    # 描述
+    if description:
+        html += '<div class="detail-section">\n'
+        html += '<strong>描述:</strong>\n'
+        html += f'<div class="detail-text">{html_module.escape(description)}</div>\n'
+        html += '</div>\n'
+
+    # 分类定义
+    if category_def:
+        html += '<div class="detail-section">\n'
+        html += '<strong>分类定义:</strong>\n'
+        html += f'<div class="detail-text">{html_module.escape(category_def)}</div>\n'
+        html += '</div>\n'
+
+    # 针对"形式"维度,显示"支撑"、"推理"和"支撑关系"
+    if isinstance(dimension, dict) and dimension.get('一级') == '形式':
+        # 支撑
+        zhicheng = element.get('支撑')
+        if zhicheng:
+            html += '<div class="detail-section">\n'
+            html += '<strong>支撑:</strong>\n'
+            html += '<div class="detail-content">\n'
+            if isinstance(zhicheng, list):
+                for item in zhicheng:
+                    if isinstance(item, dict):
+                        item_id = item.get('id', '')
+                        item_name = item.get('名称', '')
+                        html += f'<span class="detail-tag">{html_module.escape(f"{item_id}: {item_name}")}</span>\n'
+                    else:
+                        html += f'<span class="detail-tag">{html_module.escape(str(item))}</span>\n'
+            elif isinstance(zhicheng, dict):
+                for key, values in zhicheng.items():
+                    if isinstance(values, list):
+                        html += f'<div class="detail-tag category-level1">{html_module.escape(key)}</div>\n'
+                        for item in values:
+                            if isinstance(item, dict):
+                                item_id = item.get('id', '')
+                                item_name = item.get('名称', '')
+                                html += f'<span class="detail-tag">{html_module.escape(f"{item_id}: {item_name}")}</span>\n'
+                            else:
+                                html += f'<span class="detail-tag">{html_module.escape(str(item))}</span>\n'
+                    else:
+                        html += f'<span class="detail-tag">{html_module.escape(str(values))}</span>\n'
+            else:
+                html += f'<span class="detail-tag">{html_module.escape(str(zhicheng))}</span>\n'
+            html += '</div>\n'
+            html += '</div>\n'
+
+        # 推理
+        tuili = element.get('推理')
+        if tuili:
+            html += '<div class="detail-section">\n'
+            html += '<strong>推理:</strong>\n'
+            html += f'<div class="detail-text">{html_module.escape(tuili)}</div>\n'
+            html += '</div>\n'
+
+        # 支撑关系
+        intent_support_data = get_intent_support_data(element)
+        if intent_support_data:
+            html += '<div class="detail-section">\n'
+            html += '<strong>支撑关系:</strong>\n'
+            for point_type in ['灵感点', '目的点', '关键点']:
+                support_points = intent_support_data.get(point_type) or []
+                if not support_points:
+                    continue
+                html += f'<div class="score-type">{point_type}</div>\n'
+                html += '<div class="score-list">\n'
+                for support_point in support_points:
+                    if not isinstance(support_point, dict):
+                        continue
+                    point = support_point.get('点', '')
+                    point_intention = support_point.get('点的意图', '')
+                    support_reason = support_point.get('支撑理由', '')
+                    html += '<div class="score-item">\n'
+                    html += f'<div class="score-point">{html_module.escape(point)}</div>\n'
+                    if point_intention:
+                        html += '<div class="point-intention">\n'
+                        html += f'<strong style="color: #666;">点的意图:</strong>{html_module.escape(point_intention)}\n'
+                        html += '</div>\n'
+                    if support_reason:
+                        html += '<div class="score-reasons">\n'
+                        html += f'<strong style="color: #666;">支撑理由:</strong>\n'
+                        html += f'<div class="score-reason">{html_module.escape(support_reason)}</div>\n'
+                        html += '</div>\n'
+                    html += '</div>\n'
+                html += '</div>\n'
+            html += '</div>\n'
+
+    # 针对"隐含概念"
+    elem_type = element.get('类型', '')
+    if elem_type == '隐含概念':
+        laiyuan = element.get('来源')
+        if laiyuan and isinstance(laiyuan, dict):
+            html += '<div class="detail-section">\n'
+            html += '<strong>来源:</strong>\n'
+            html += '<div class="detail-content">\n'
+            for key, values in laiyuan.items():
+                if isinstance(values, list) and values:
+                    html += f'<div class="detail-tag category-level1">{html_module.escape(key)}</div>\n'
+                    for item in values:
+                        html += f'<span class="detail-tag">{html_module.escape(str(item))}</span>\n'
+                elif values:
+                    html += f'<span class="detail-tag">{html_module.escape(str(values))}</span>\n'
+            html += '</div>\n'
+            html += '</div>\n'
+
+        time_range = element.get('时间范围')
+        if time_range:
+            html += '<div class="detail-section">\n'
+            html += '<strong>时间范围:</strong>\n'
+            html += '<div class="detail-content">\n'
+            if isinstance(time_range, list):
+                for tr in time_range:
+                    html += f'<span class="detail-tag source-tag">{html_module.escape(str(tr))}</span>\n'
+            else:
+                html += f'<span class="detail-tag source-tag">{html_module.escape(str(time_range))}</span>\n'
+            html += '</div>\n'
+            html += '</div>\n'
+
+    # 针对"抽象概念"
+    elif isinstance(dimension, dict) and dimension.get('二级') == '抽象概念':
+        leixing = element.get('类型')
+        if leixing:
+            html += '<div class="detail-section">\n'
+            html += '<strong>类型:</strong>\n'
+            html += f'<div class="detail-text">{html_module.escape(str(leixing))}</div>\n'
+            html += '</div>\n'
+
+        laiyuan = element.get('来源')
+        if laiyuan and isinstance(laiyuan, dict):
+            html += '<div class="detail-section">\n'
+            html += '<strong>来源:</strong>\n'
+            html += '<div class="detail-content">\n'
+            for key, values in laiyuan.items():
+                if isinstance(values, list):
+                    html += f'<div class="detail-tag category-level1">{html_module.escape(key)}</div>\n'
+                    for item in values:
+                        if isinstance(item, dict):
+                            item_id = item.get('id', '')
+                            item_name = item.get('名称', '')
+                            html += f'<span class="detail-tag">{html_module.escape(f"{item_id}: {item_name}")}</span>\n'
+                        else:
+                            html += f'<span class="detail-tag">{html_module.escape(str(item))}</span>\n'
+                else:
+                    html += f'<span class="detail-tag">{html_module.escape(str(values))}</span>\n'
+            html += '</div>\n'
+            html += '</div>\n'
+
+        tuili_guocheng = element.get('推理过程')
+        if tuili_guocheng:
+            html += '<div class="detail-section">\n'
+            html += '<strong>推理过程:</strong>\n'
+            html += f'<div class="detail-text">{html_module.escape(tuili_guocheng)}</div>\n'
+            html += '</div>\n'
+
+    else:
+        if source:
+            html += '<div class="detail-section">\n'
+            html += '<strong>来源:</strong>\n'
+            html += '<div class="detail-content">\n'
+            for src in source:
+                html += f'<span class="detail-tag source-tag">{html_module.escape(str(src))}</span>\n'
+            html += '</div>\n'
+            html += '</div>\n'
+
+    # 上下文验证
+    context_verification = element.get('上下文验证')
+    if context_verification:
+        html += '<div class="detail-section">\n'
+        html += '<strong>上下文验证:</strong>\n'
+        html += '<div class="context-verification">\n'
+        original_position = context_verification.get('原文位置', '')
+        if original_position:
+            html += '<div class="context-item">\n'
+            html += '<strong>原文位置:</strong>\n'
+            html += f'<div class="context-text">{html_module.escape(original_position)}</div>\n'
+            html += '</div>\n'
+        grammar_component = context_verification.get('语法成分', '')
+        if grammar_component:
+            html += '<div class="context-item">\n'
+            html += '<strong>语法成分:</strong>\n'
+            html += f'<span class="grammar-tag">{html_module.escape(grammar_component)}</span>\n'
+            html += '</div>\n'
+        context_judgment = context_verification.get('语境判断', '')
+        if context_judgment:
+            html += '<div class="context-item">\n'
+            html += '<strong>语境判断:</strong>\n'
+            html += f'<div class="context-text">{html_module.escape(context_judgment)}</div>\n'
+            html += '</div>\n'
+        html += '</div>\n'
+        html += '</div>\n'
+
+    # 出现段落
+    if paragraphs_list:
+        html += '<div class="detail-section">\n'
+        html += '<strong>出现段落:</strong>\n'
+        html += '<div class="paragraphs-detail-list">\n'
+        for para in paragraphs_list:
+            if isinstance(para, dict):
+                para_id = para.get('段落ID', '')
+                how = para.get('如何体现', '')
+                html += '<div class="paragraph-detail-item">\n'
+                html += f'<span class="detail-tag para-id-tag">{html_module.escape(para_id)}</span>\n'
+                if how:
+                    html += f'<div class="para-how">{html_module.escape(how)}</div>\n'
+                html += '</div>\n'
+            else:
+                html += f'<span class="detail-tag">{html_module.escape(str(para))}</span>\n'
+        html += '</div>\n'
+        html += '</div>\n'
+
+    # 意图支撑
+    if intent_support:
+        html += '<div class="detail-section">\n'
+        html += '<strong>意图支撑:</strong>\n'
+        for point_type in ['灵感点', '目的点', '关键点']:
+            if point_type in intent_support and intent_support[point_type]:
+                html += f'<div class="score-type">{point_type}</div>\n'
+                html += '<div class="score-list">\n'
+                for item in intent_support[point_type]:
+                    point = item.get('点', '')
+                    point_intention = item.get('点的意图', '')
+                    support_reason = item.get('支撑理由', '')
+                    html += '<div class="score-item">\n'
+                    html += f'<div class="score-point">{html_module.escape(point)}</div>\n'
+                    if point_intention:
+                        html += '<div class="point-intention">\n'
+                        html += f'<strong style="color: #666;">点的意图:</strong>{html_module.escape(point_intention)}\n'
+                        html += '</div>\n'
+                    if support_reason:
+                        html += '<div class="score-reasons">\n'
+                        html += f'<strong style="color: #666;">支撑理由:</strong>\n'
+                        html += f'<div class="score-reason">{html_module.escape(support_reason)}</div>\n'
+                        html += '</div>\n'
+                    html += '</div>\n'
+                html += '</div>\n'
+        html += '</div>\n'
+
+    # 权重明细
+    if weight_info.get('raw_total', 0) > 0:
+        wd = weight_info['details']
+        ss = weight_info['support_stats']
+        is_form = isinstance(dimension, dict) and dimension.get('一级') == '形式'
+
+        html += '<div class="detail-section weight-detail-section">\n'
+        html += '<strong>权重得分明细:</strong>\n'
+        html += '<div class="weight-summary-header">\n'
+        if is_form:
+            html += f'权重分:{weight_info["weight_score"]:.1f}(形式元素使用新的权重计算逻辑)\n'
+        else:
+            html += f'原始总分:{weight_info["raw_total"]:.1f},权重分:{weight_info["weight_score"]:.1f}(min(100, 原始总分 × 100 / 110))\n'
+        html += '</div>\n'
+        html += '<div class="weight-summary-grid">\n'
+        
+        if is_form:
+            html += f'<span class="weight-chip">频次分 {wd.get("频次分", 0):.1f}</span>\n'
+            if "覆盖段落数分" in wd:
+                html += f'<span class="weight-chip">覆盖段落数分 {wd.get("覆盖段落数分", 0):.1f}</span>\n'
+            html += f'<span class="weight-chip">覆盖率分 {wd.get("覆盖率分", 0):.1f}</span>\n'
+        else:
+            html += f'<span class="weight-chip">频次分 {wd.get("频次分", 0):.1f} / 30</span>\n'
+            html += f'<span class="weight-chip">覆盖率分 {wd.get("覆盖率分", 0):.1f} / 30</span>\n'
+
+        inspiration_count = ss.get('灵感点数量', 0)
+        if is_form:
+            html += f'<span class="weight-chip">灵感点支撑分 {wd.get("灵感点支撑分", 0):.1f}({inspiration_count} 个)</span>\n'
+        else:
+            html += f'<span class="weight-chip">灵感点支撑分 {wd.get("灵感点支撑分", 0):.1f} / 25({inspiration_count} 个)</span>\n'
+
+        purpose_count = ss.get('目的点数量', 0)
+        if is_form:
+            html += f'<span class="weight-chip">目的点支撑分 {wd.get("目的点支撑分", 0):.1f}({purpose_count} 个)</span>\n'
+        else:
+            html += f'<span class="weight-chip">目的点支撑分 {wd.get("目的点支撑分", 0):.1f} / 15({purpose_count} 个)</span>\n'
+
+        keypoint_count = ss.get('关键点数量', 0)
+        if is_form:
+            html += f'<span class="weight-chip">关键点支撑分 {wd.get("关键点支撑分", 0):.1f}({keypoint_count} 个)</span>\n'
+        else:
+            html += f'<span class="weight-chip">关键点支撑分 {wd.get("关键点支撑分", 0):.1f} / 10({keypoint_count} 个)</span>\n'
+
+        html += '</div>\n'
+        html += '</div>\n'
+
+    html += '</div>\n'  # modal-body
+    html += '</div>\n'  # element-modal-content
+    return html
+
+
 def generate_tab3_content(data: Dict[str, Any]) -> str:
     """生成Tab3内容:按层次展示(实质/形式 → 具体元素/具体概念/抽象概念 → 树形展示)"""
     html = '<div class="tab-content" id="tab3" style="display:none;">\n'
 
-    # 添加全局控制按钮
+    # 添加全局控制按钮(移除展开/收起按钮)
     html += '<div class="global-controls">\n'
     html += '    <div class="color-legend">\n'
     html += '        <span class="legend-title">颜色图例(主导因素):</span>\n'
@@ -900,10 +1274,11 @@ def generate_tab3_content(data: Dict[str, Any]) -> str:
     html += '        <span class="legend-item legend-frequency">频次</span>\n'
     html += '        <span class="legend-item legend-intent">意图支撑</span>\n'
     html += '    </div>\n'
-    html += '    <button class="control-btn" onclick="toggleAllLevels(true)">全部展开</button>\n'
-    html += '    <button class="control-btn" onclick="toggleAllLevels(false)">全部收起</button>\n'
     html += '</div>\n'
 
+    # 收集所有元素用于弹窗
+    all_elements_dict = {}
+
     if '脚本理解' in data:
         script = data['脚本理解']
         # 尝试获取元素列表,如果不存在则合并实质列表和形式列表
@@ -913,6 +1288,12 @@ def generate_tab3_content(data: Dict[str, Any]) -> str:
             form_list = script.get('形式列表', [])
             elements = substance_list + form_list
 
+        # 收集所有元素到字典中(用于弹窗)
+        for elem in elements:
+            elem_id = elem.get('id', '')
+            if elem_id:
+                all_elements_dict[elem_id] = elem
+
         # 第一层:按维度.一级分组(实质 vs 形式)
         level1_groups = {}
         for elem in elements:
@@ -998,7 +1379,7 @@ def generate_tab3_content(data: Dict[str, Any]) -> str:
                         continue
 
                     # 计算一级分类的统计信息
-                    avg_coverage = sum((e.get('共性分析') or {}).get('段落覆盖率', 0.0) for e in all_cat_elements) / len(all_cat_elements)
+                    avg_coverage = sum(get_element_coverage(e) for e in all_cat_elements) / len(all_cat_elements)
                     avg_intent_count = sum(calculate_intent_support_count(e) for e in all_cat_elements) / len(all_cat_elements)
 
                     html += '<div class="category-group collapsible">\n'
@@ -1026,7 +1407,7 @@ def generate_tab3_content(data: Dict[str, Any]) -> str:
                             continue
 
                         # 计算二级分类的统计信息
-                        avg_coverage_l2 = sum((e.get('共性分析') or {}).get('段落覆盖率', 0.0) for e in cat_level2_elements) / len(cat_level2_elements)
+                        avg_coverage_l2 = sum(get_element_coverage(e) for e in cat_level2_elements) / len(cat_level2_elements)
                         avg_intent_count_l2 = sum(calculate_intent_support_count(e) for e in cat_level2_elements) / len(cat_level2_elements)
 
                         html += '<div class="subcategory-group collapsible">\n'
@@ -1055,5 +1436,491 @@ def generate_tab3_content(data: Dict[str, Any]) -> str:
             html += '</div>\n'  # level1-content
             html += '</div>\n'  # level1-section
 
+    # 预生成所有元素的弹窗内容(隐藏)
+    html += '<div id="elementModalTemplates" style="display:none;">\n'
+    for elem_id, elem in all_elements_dict.items():
+        html += render_element_modal(elem)
+    html += '</div>\n'
+
+    # 添加弹窗结构
+    html += '<div id="elementModal" class="element-modal" style="display:none;">\n'
+    html += '<div class="element-modal-overlay" onclick="closeElementModal()"></div>\n'
+    html += '<div class="element-modal-dialog">\n'
+    html += '<div id="elementModalContent" class="element-modal-content-wrapper">\n'
+    html += '<!-- 弹窗内容将通过JavaScript动态填充 -->\n'
+    html += '</div>\n'
+    html += '</div>\n'
+    html += '</div>\n'
+
+    # 添加CSS样式
+    html += '<style>\n'
+    html += '''
+    /* 元素卡片样式 */
+    .element-list {
+        display: grid;
+        grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+        gap: 20px;
+        list-style: none;
+        padding: 0;
+        margin: 20px 0;
+    }
+    
+    .element-card {
+        background: white;
+        border-radius: 8px;
+        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+        transition: all 0.3s ease;
+        overflow: hidden;
+    }
+    
+    .element-card:hover {
+        box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
+        transform: translateY(-2px);
+    }
+    
+    .element-card.dominant-coverage {
+        border-left: 4px solid #667eea;
+    }
+    
+    .element-card.dominant-frequency {
+        border-left: 4px solid #f093fb;
+    }
+    
+    .element-card.dominant-intent_support {
+        border-left: 4px solid #4facfe;
+    }
+    
+    .element-card-body {
+        padding: 16px;
+    }
+    
+    .element-card-header {
+        display: flex;
+        align-items: center;
+        gap: 8px;
+        margin-bottom: 12px;
+    }
+    
+    .element-card-id {
+        color: #667eea;
+        font-weight: 600;
+        font-size: 14px;
+    }
+    
+    .element-card-name {
+        margin: 0;
+        font-size: 18px;
+        font-weight: 600;
+        color: #333;
+        flex: 1;
+    }
+    
+    .element-card-stats {
+        display: flex;
+        flex-wrap: wrap;
+        gap: 8px;
+        margin-bottom: 12px;
+    }
+    
+    .stat-badge {
+        display: inline-block;
+        padding: 4px 10px;
+        border-radius: 12px;
+        font-size: 12px;
+        font-weight: 500;
+        background: #f5f5f5;
+        color: #666;
+    }
+    
+    .stat-badge.stat-weight {
+        background: #667eea;
+        color: white;
+    }
+    
+    .stat-badge.stat-coverage {
+        background: #e8f0ff;
+        color: #667eea;
+    }
+    
+    .stat-badge.stat-frequency {
+        background: #fce4ec;
+        color: #f093fb;
+    }
+    
+    .stat-badge.stat-intent {
+        background: #e0f7fa;
+        color: #4facfe;
+    }
+    
+    .stat-badge.stat-highlight {
+        font-weight: 700;
+        transform: scale(1.05);
+    }
+    
+    .element-card-description {
+        color: #666;
+        font-size: 14px;
+        line-height: 1.6;
+        margin-bottom: 12px;
+    }
+    
+    .element-detail-btn {
+        width: 100%;
+        padding: 10px;
+        background: #667eea;
+        color: white;
+        border: none;
+        border-radius: 6px;
+        font-size: 14px;
+        font-weight: 500;
+        cursor: pointer;
+        transition: all 0.2s;
+    }
+    
+    .element-detail-btn:hover {
+        background: #5568d3;
+        transform: translateY(-1px);
+        box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
+    }
+    
+    /* 弹窗样式 */
+    .element-modal {
+        display: none;
+        position: fixed;
+        top: 0;
+        left: 0;
+        width: 100%;
+        height: 100%;
+        z-index: 1000;
+        align-items: center;
+        justify-content: center;
+    }
+    
+    .element-modal-overlay {
+        position: absolute;
+        top: 0;
+        left: 0;
+        width: 100%;
+        height: 100%;
+        background: rgba(0, 0, 0, 0.5);
+        backdrop-filter: blur(4px);
+    }
+    
+    .element-modal-dialog {
+        position: relative;
+        width: 800px;
+        height: 90vh;
+        background: white;
+        border-radius: 12px;
+        box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
+        z-index: 1001;
+        display: flex;
+        flex-direction: column;
+        overflow: hidden;
+    }
+    
+    .element-modal-content-wrapper {
+        width: 100%;
+        height: 100%;
+        display: flex;
+        flex-direction: column;
+        overflow: hidden;
+    }
+    
+    .element-modal-content {
+        width: 100%;
+        height: 100%;
+        display: flex;
+        flex-direction: column;
+        padding: 0;
+        overflow: hidden;
+    }
+    
+    .modal-header {
+        width: 100%;
+        display: flex;
+        align-items: center;
+        gap: 12px;
+        padding: 20px 24px;
+        border-bottom: 1px solid rgba(255, 255, 255, 0.2);
+        background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+        color: white;
+        border-radius: 12px 12px 0 0;
+        position: sticky;
+        top: 0;
+        z-index: 10;
+        flex-shrink: 0;
+        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+    }
+    
+    .modal-id {
+        font-weight: 600;
+        font-size: 14px;
+        opacity: 0.9;
+    }
+    
+    .modal-title {
+        margin: 0;
+        font-size: 20px;
+        font-weight: 600;
+        flex: 1;
+    }
+    
+    .modal-close {
+        background: rgba(255, 255, 255, 0.2);
+        border: none;
+        color: white;
+        font-size: 24px;
+        width: 32px;
+        height: 32px;
+        border-radius: 50%;
+        cursor: pointer;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        transition: all 0.2s;
+    }
+    
+    .modal-close:hover {
+        background: rgba(255, 255, 255, 0.3);
+        transform: rotate(90deg);
+    }
+    
+    .modal-body {
+        width: 100%;
+        flex: 1;
+        padding: 24px;
+        overflow-y: auto;
+        overflow-x: hidden;
+        min-height: 0;
+    }
+    
+    .detail-section {
+        margin-bottom: 20px;
+    }
+    
+    .detail-section strong {
+        display: block;
+        color: #667eea;
+        font-size: 14px;
+        font-weight: 600;
+        margin-bottom: 8px;
+    }
+    
+    .detail-text {
+        color: #555;
+        line-height: 1.6;
+        padding: 12px;
+        background: #f8f9fa;
+        border-radius: 6px;
+        border-left: 3px solid #667eea;
+    }
+    
+    .detail-content {
+        display: flex;
+        flex-wrap: wrap;
+        gap: 8px;
+        margin-top: 8px;
+    }
+    
+    .detail-tag {
+        display: inline-block;
+        padding: 6px 12px;
+        background: #e8f0ff;
+        color: #667eea;
+        border-radius: 16px;
+        font-size: 13px;
+        font-weight: 500;
+    }
+    
+    .detail-tag.category-level1 {
+        width: 100%;
+        background: #667eea;
+        color: white;
+        font-weight: 600;
+        margin-top: 8px;
+    }
+    
+    .detail-tag.source-tag {
+        background: #fce4ec;
+        color: #f093fb;
+    }
+    
+    .detail-tag.para-id-tag {
+        background: #e0f7fa;
+        color: #4facfe;
+    }
+    
+    .weight-chip {
+        display: inline-block;
+        padding: 6px 12px;
+        background: #f5f5f5;
+        color: #333;
+        border-radius: 16px;
+        font-size: 12px;
+        margin: 4px;
+    }
+    
+    .weight-summary-grid {
+        display: flex;
+        flex-wrap: wrap;
+        gap: 8px;
+        margin-top: 12px;
+    }
+    
+    .weight-summary-header {
+        font-size: 13px;
+        color: #666;
+        margin-top: 8px;
+    }
+    
+    .weight-detail-section {
+        margin-top: 20px;
+        padding-top: 20px;
+        border-top: 1px dashed #e0e0e0;
+    }
+    
+    .score-type {
+        font-weight: 600;
+        color: #667eea;
+        margin-top: 16px;
+        margin-bottom: 8px;
+        font-size: 15px;
+    }
+    
+    .score-list {
+        margin-left: 16px;
+    }
+    
+    .score-item {
+        margin-bottom: 12px;
+        padding: 12px;
+        background: #f8f9fa;
+        border-radius: 6px;
+        border-left: 3px solid #667eea;
+    }
+    
+    .score-point {
+        font-weight: 600;
+        color: #333;
+        margin-bottom: 8px;
+    }
+    
+    .point-intention, .score-reasons {
+        margin-top: 8px;
+        font-size: 13px;
+        color: #666;
+    }
+    
+    .score-reason {
+        margin-top: 4px;
+        padding: 8px;
+        background: white;
+        border-radius: 4px;
+    }
+    
+    .context-verification {
+        margin-top: 8px;
+        padding: 12px;
+        background: #f8f9fa;
+        border-radius: 4px;
+        border-left: 3px solid #6c757d;
+    }
+    
+    .context-item {
+        margin-bottom: 8px;
+    }
+    
+    .context-text {
+        margin-top: 4px;
+        padding: 6px 10px;
+        background: white;
+        border-radius: 3px;
+        color: #212529;
+        font-style: italic;
+    }
+    
+    .grammar-tag {
+        margin-left: 8px;
+        padding: 3px 10px;
+        background: #e7f3ff;
+        color: #0066cc;
+        border-radius: 3px;
+        font-size: 12px;
+    }
+    
+    .paragraphs-detail-list {
+        display: flex;
+        flex-direction: column;
+        gap: 8px;
+        margin-top: 8px;
+    }
+    
+    .paragraph-detail-item {
+        display: flex;
+        align-items: flex-start;
+        gap: 8px;
+    }
+    
+    .para-how {
+        flex: 1;
+        padding: 8px;
+        background: white;
+        border-radius: 4px;
+        color: #555;
+        font-size: 13px;
+    }
+    
+    @media (max-width: 768px) {
+        .element-list {
+            grid-template-columns: 1fr;
+        }
+        
+        .element-modal-dialog {
+            width: 95%;
+            height: 95vh;
+        }
+        
+        .modal-body {
+            padding: 16px;
+        }
+    }
+    '''
+    html += '</style>\n'
+
+    # 添加JavaScript代码
+    html += '<script>\n'
+    html += '''
+    function openElementModal(elemId) {
+        const modal = document.getElementById('elementModal');
+        const contentWrapper = document.getElementById('elementModalContent');
+        const templates = document.getElementById('elementModalTemplates');
+        
+        // 查找预生成的弹窗内容
+        const modalContent = templates.querySelector(`.element-modal-content[data-elem-id="${elemId}"]`);
+        if (modalContent) {
+            contentWrapper.innerHTML = modalContent.outerHTML;
+            modal.style.display = 'flex';
+            document.body.style.overflow = 'hidden';
+        } else {
+            console.error('Modal content not found for element:', elemId);
+        }
+    }
+    
+    function closeElementModal() {
+        const modal = document.getElementById('elementModal');
+        modal.style.display = 'none';
+        document.body.style.overflow = '';
+    }
+    
+    // ESC键关闭弹窗
+    document.addEventListener('keydown', function(e) {
+        if (e.key === 'Escape') {
+            closeElementModal();
+        }
+    });
+    '''
+    html += '</script>\n'
+
     html += '</div>\n'
     return html