learn_detail.html 14.6 KB
{% extends "base.html" %}
{% block title %}学习详情 - WeChat Viewer{% endblock %}
{% block body %}
<style>
body{overflow-y:auto!important;height:auto!important;}
.detail-container{max-width:800px;margin:0 auto;padding:20px;}
.back-link{display:inline-block;margin-bottom:16px;font-size:13px;color:#3498db;text-decoration:none;}
.back-link:hover{text-decoration:underline;}
.card{background:white;border-radius:10px;padding:20px;margin-bottom:14px;box-shadow:0 1px 3px rgba(0,0,0,0.06);}
.card h2{font-size:16px;color:#2c3e50;margin-bottom:12px;}
.header{display:flex;align-items:center;gap:10px;margin-bottom:6px;flex-wrap:wrap;}
.badge{font-size:10px;padding:3px 8px;border-radius:8px;font-weight:600;white-space:nowrap;}
.badge.skill{background:#d5f5e3;color:#1e8449}.badge.memory{background:#d6eaf8;color:#1a5276}
.badge.reference{background:#e8daef;color:#6c3483}.badge.todo{background:#fdebd0;color:#b9770e}
.badge.score{font-weight:bold;font-size:12px;}
.badge.outcome{font-size:11px;padding:3px 10px;}
.badge.outcome.skill{background:#d5f5e3;color:#1e8449}
.badge.outcome.memory{background:#d6eaf8;color:#1a5276}
.badge.outcome.code{background:#fdebd0;color:#b9770e}
.badge.outcome.install{background:#d6eaf8;color:#1a5276}
.timeline{margin-top:14px;border-left:3px solid #e0e0e0;padding-left:16px;}
.timeline-item{margin-bottom:14px;position:relative;}
.timeline-item:before{content:'';position:absolute;left:-23px;top:4px;width:10px;height:10px;border-radius:50%;background:#3498db;}
.timeline-item .time{font-size:11px;color:#999;margin-bottom:4px;}
.timeline-item .text{font-size:13px;color:#555;line-height:1.6;}
.content-box{font-size:13px;color:#333;line-height:1.7;white-space:pre-wrap;background:#f8f9fa;padding:14px;border-radius:6px;margin-top:8px;max-height:600px;overflow-y:auto;}
.meta-row{display:flex;gap:20px;flex-wrap:wrap;font-size:12px;color:#888;margin-top:8px;}
.meta-row span b{color:#555;}
.empty{text-align:center;padding:30px;color:#999;font-size:13px;}
/* outcome cards */
.outcome-grid{display:flex;flex-direction:column;gap:10px;margin-top:10px;}
.outcome-card{border-radius:8px;padding:12px 14px;border-left:4px solid;font-size:13px;line-height:1.5;}
.outcome-card.skill{border-color:#27ae60;background:#f0faf0;}
.outcome-card.memory{border-color:#3498db;background:#f0f7fb;}
.outcome-card.code{border-color:#f39c12;background:#fef9e7;}
.outcome-card.install{border-color:#3498db;background:#f0f7fb;}
.outcome-card .title{font-weight:600;margin-bottom:2px;}
.outcome-card .detail{color:#666;font-size:12px;}
/* verify & behavior sections */
.section-list{font-size:13px;color:#555;line-height:1.6;}
.section-list .item{margin-bottom:6px;padding-left:8px;border-left:3px solid #e0e0e0;}
.section-list .item.pass{border-color:#27ae60;}
.section-list .item.fail{border-color:#e74c3c;}
</style>

<div class="detail-container">
<a href="/learn" class="back-link">← 返回学习管线</a>

{% set title = item.title or '未命名' %}
{% set ltype = item.learn_type or '?' %}
{% set score = item.score or 0 %}
{% set status = item.status or '?' %}
{% set target = item.learn_target or '' %}
{% set content = item.learn_content or '' %}
{% set confidence = item.confidence or 'medium' %}
{% set created = (item.created_at or '')[:16] %}
{% set reviewed = (item.reviewed_at or '')[:16] %}
{% set error = item.error_msg or '' %}

{% set st_labels = {'pending_review':'待审核','pending_learn':'待学习','applied':'已吸收','approved':'已通过','failed':'失败','rejected':'已拒绝'} %}
{% set cn_labels = {'high':'高','medium':'中','low':'低'} %}
{% set sc_color = '#27ae60' if score >= 7 else '#f39c12' if score >= 4 else '#95a5a6' %}

<!-- Header -->
<div class="card">
  <div class="header">
    <span class="badge {{ ltype }}">{{ {'skill':'🛠 Skill','memory':'🧠 记忆','reference':'📖 参考','todo':'📋 待办'}.get(ltype, ltype) }}</span>
    {% if score %}<span class="badge score" style="color:{{ sc_color }};">⭐{{ score }}</span>{% endif %}
    <span class="badge" style="background:#f0f0f0;color:#666;">{{ cn_labels.get(confidence, confidence) }}置信度</span>
    <span style="font-weight:bold;color:{{ '#27ae60' if status == 'applied' else '#e74c3c' if status == 'failed' else '#666' }};">{{ st_labels.get(status, status) }}</span>
  </div>
  <h2 style="margin-top:8px;">{{ title }}</h2>
  {% if target %}<div class="meta-row"><span>目标: <b>{{ target }}</b></span></div>{% endif %}
  <div class="meta-row">
    <span>创建: {{ created }}</span>
    {% if reviewed %}<span>审核: {{ reviewed }}</span>{% endif %}
  </div>
  <div class="action-bar" style="margin-top:12px;display:flex;gap:8px;flex-wrap:wrap;"></div>
</div>

<!-- Content -->
<div class="card">
  <h2>📄 学习内容</h2>
  {% if content %}
  <div class="content-box">{{ content }}</div>
  {% else %}
  <div class="empty">无内容</div>
  {% endif %}
</div>

<!-- Error -->
{% if error and status != 'applied' %}
<div class="card" style="border-left:3px solid #e74c3c;">
  <h2 style="color:#e74c3c;">❌ 错误信息</h2>
  <div class="content-box">{{ error }}</div>
</div>
{% endif %}

<!-- Structured Report -->
{% if result and result.result_summary %}
<div id="structured-report" data-result="{{ result.result_summary|e }}"></div>
<script>
(function(){
  var el = document.getElementById('structured-report');
  var raw = el.getAttribute('data-result') || '';
  // unescape HTML entities back
  var txt = document.createElement('textarea');
  txt.innerHTML = raw;
  raw = txt.value;
  if(!raw){ el.innerHTML='<div class="card"><h2>📊 学习报告</h2><div class="empty">无报告内容</div></div>'; return; }

  function extractSection(text, heading){
    var re = new RegExp('##\\s+' + heading + '\\s*\\n([\\s\\S]*?)(?=\\n##\\s|\\n###\\s|\\n```|\\Z)', 'i');
    var m = text.match(re);
    return m ? m[1].trim() : '';
  }
  function extractSubSection(text, heading){
    var re = new RegExp('###\\s+' + heading + '\\s*\\n([\\s\\S]*?)(?=\\n###\\s|\\n##\\s|\\n```|\\Z)', 'i');
    var m = text.match(re);
    return m ? m[1].trim() : '';
  }
  function parseItems(section){
    if(!section || section==='无') return [];
    var items = [];
    var lines = section.split('\n');
    for(var i=0;i<lines.length;i++){
      var line = lines[i].trim();
      if(line.startsWith('- **') || line.startsWith('- ')){
        items.push(line.replace(/^-\\s*\\*?\\*?/,'').replace(/\\*\\*$/,'').trim());
      }
    }
    return items;
  }
  function parseVerify(section){
    if(!section) return [];
    var items = [];
    var lines = section.split('\n');
    for(var i=0;i<lines.length;i++){
      var line = lines[i].trim();
      if(line.startsWith('- TC')){
        var pass = line.indexOf('PASS') !== -1;
        items.push({text: line.replace(/^-\\s*/,''), pass: pass});
      }
    }
    return items;
  }

  var contentSec = extractSection(raw, '学习内容');
  var outcomeSec = extractSection(raw, '吸收产出');
  var skillsSec = extractSubSection(outcomeSec, '创建/更新 Skill|创建.*Skill');
  if(!skillsSec) skillsSec = extractSubSection(outcomeSec, '创建/更新 Skill');
  var memSec = extractSubSection(outcomeSec, '写入 Memory');
  var codeSec = extractSubSection(outcomeSec, '代码变更');
  var installSec = extractSubSection(outcomeSec, '安装/集成');
  var verifySec = extractSection(raw, '验证结果');
  var behaviorSec = extractSection(raw, '行为变化');
  var undoneSec = extractSection(raw, '未完成/待跟进');

  var skills = parseItems(skillsSec);
  var mems = parseItems(memSec);
  var codes = parseItems(codeSec);
  var installs = parseItems(installSec);
  var verifies = parseVerify(verifySec);
  var behaviors = parseItems(behaviorSec);

  if(skills.length===0 && mems.length===0 && codes.length===0 && installs.length===0){
    // No structured data, show raw
    el.innerHTML = '<div class="card"><h2>📊 学习报告</h2><div class="content-box" style="background:#f0faf0;border-left:3px solid #27ae60;">'+raw.replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\n/g,'<br>')+'</div></div>';
    return;
  }

  var html = '';

  // Outcome cards
  html += '<div class="card"><h2>📊 吸收产出</h2><div class="outcome-grid">';

  skills.forEach(function(s){
    html += '<div class="outcome-card skill"><div class="title">🛠 Skill</div><div class="detail">'+s.replace(/</g,'&lt;')+'</div></div>';
  });
  mems.forEach(function(m){
    html += '<div class="outcome-card memory"><div class="title">🧠 Memory</div><div class="detail">'+m.replace(/</g,'&lt;')+'</div></div>';
  });
  codes.forEach(function(c){
    html += '<div class="outcome-card code"><div class="title">💻 代码变更</div><div class="detail">'+c.replace(/</g,'&lt;')+'</div></div>';
  });
  installs.forEach(function(i){
    html += '<div class="outcome-card install"><div class="title">📦 安装/集成</div><div class="detail">'+i.replace(/</g,'&lt;')+'</div></div>';
  });

  html += '</div></div>';

  // Verify
  if(verifies.length > 0){
    html += '<div class="card"><h2>✅ 验证结果</h2><div class="section-list">';
    verifies.forEach(function(v){
      html += '<div class="item '+(v.pass?'pass':'fail')+'">'+(v.pass?'✅':'❌')+' '+v.text.replace(/</g,'&lt;')+'</div>';
    });
    html += '</div></div>';
  }

  // Behavior
  if(behaviors.length > 0){
    html += '<div class="card"><h2>🔄 行为变化</h2><div class="section-list">';
    behaviors.forEach(function(b){
      html += '<div class="item" style="border-color:#27ae60;">'+b.replace(/</g,'&lt;')+'</div>';
    });
    html += '</div></div>';
  }

  // Undone
  if(undoneSec && undoneSec!=='无'){
    html += '<div class="card"><h2>📋 未完成/待跟进</h2><div class="content-box">'+undoneSec.replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\n/g,'<br>')+'</div></div>';
  }

  el.innerHTML = html;
})();
</script>
{% elif result %}
<div class="card"><h2>📊 学习报告</h2><div class="empty">无报告内容</div></div>
{% endif %}

<!-- Step 6: 知识应用审计 -->
{% if apply_result and apply_result.result_summary %}
<div class="card" style="border-left:3px solid #8e44ad;">
  <h2 style="color:#8e44ad;">🔍 Step 6 — 知识应用审计</h2>
  <div class="content-box" style="max-height:500px;">{{ apply_result.result_summary }}</div>
</div>
{% endif %}

<!-- Timeline -->
<div class="card">
  <h2>⏱ 时间线</h2>
  <div class="timeline">
    <div class="timeline-item">
      <div class="time">{{ created }}</div>
      <div class="text">分类器标记为 <b>{{ st_labels.get(status, status) }}</b>,评分 ⭐{{ score }}</div>
    </div>
    {% if reviewed %}
    <div class="timeline-item">
      <div class="time">{{ reviewed }}</div>
      <div class="text">吸收完成</div>
    </div>
    {% endif %}
    {% if result and result.created_at %}
    <div class="timeline-item">
      <div class="time">{{ (result.created_at or '')[:16] }}</div>
      <div class="text">学习报告生成</div>
    </div>
    {% endif %}
  </div>
</div>

<script>
(function(){
  var itemId = {{ item.id }};
  var currentStatus = '{{ status }}';
  var actionBar = document.querySelector('.action-bar');
  if (!actionBar) return;

  // Status dropdown
  var statusMap = {
    'pending_review': '待审核',
    'pending_learn': '待学习',
    'applied': '已吸收',
    'approved': '已通过',
    'failed': '失败',
    'rejected': '已拒绝'
  };
  var statusOptions = ['pending_review', 'pending_learn', 'applied', 'approved', 'failed', 'rejected'];

  var selWrapper = document.createElement('span');
  selWrapper.style.cssText = 'display:inline-flex;align-items:center;gap:4px;';

  var selLabel = document.createElement('span');
  selLabel.textContent = '状态:';
  selLabel.style.cssText = 'font-size:13px;color:#666;';
  selWrapper.appendChild(selLabel);

  var sel = document.createElement('select');
  sel.id = 'status-select';
  sel.style.cssText = 'padding:5px 10px;border:1px solid #ccc;border-radius:6px;background:white;font-size:13px;cursor:pointer;min-width:100px;';
  for (var i = 0; i < statusOptions.length; i++) {
    var opt = document.createElement('option');
    opt.value = statusOptions[i];
    opt.textContent = statusMap[statusOptions[i]];
    if (statusOptions[i] === currentStatus) {
      opt.selected = true;
    }
    sel.appendChild(opt);
  }
  sel.addEventListener('change', function handleStatusChange(){
    var newStatus = sel.value;
    sel.disabled = true;
    sel.style.opacity = '0.5';
    fetch('/api/learn/' + itemId + '/status', {
      method: 'PUT',
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify({status: newStatus})
    })
      .then(function(r){ return r.json(); })
      .then(function(data){
        if (data.ok) {
          sel.style.borderColor = '#27ae60';
          sel.style.background = '#f0faf0';
          setTimeout(function(){ location.reload(); }, 1500);
        } else {
          sel.disabled = false;
          sel.style.opacity = '1';
          sel.style.borderColor = '#e74c3c';
          sel.style.background = '#fdf0f0';
          setTimeout(function(){
            sel.style.borderColor = '#ccc';
            sel.style.background = 'white';
          }, 2000);
        }
      })
      .catch(function(){
        sel.disabled = false;
        sel.style.opacity = '1';
        sel.style.borderColor = '#e74c3c';
      });
  });
  selWrapper.appendChild(sel);
  actionBar.appendChild(selWrapper);

  var btn = document.createElement('button');
  btn.textContent = '\ud83d\udd04 \u91cd\u65b0\u5b66\u4e60';
  btn.style.cssText = 'padding:6px 16px;border:1px solid #3498db;border-radius:6px;background:white;color:#3498db;font-size:13px;cursor:pointer;';
  btn.onmouseenter = function(){ this.style.background='#3498db'; this.style.color='white'; };
  btn.onmouseleave = function(){ this.style.background='white'; this.style.color='#3498db'; };
  btn.onclick = function handleRelearn(){
    if (btn.disabled) return;
    btn.disabled = true;
    btn.textContent = '\u23f3 \u91cd\u65b0\u5b66\u4e60\u4e2d...';
    fetch('/api/learn/' + itemId + '/relearn', {method: 'POST'})
      .then(function(r){ return r.json(); })
      .then(function(data){
        if (data.ok) {
          btn.textContent = '\u2705 \u5df2\u89e6\u53d1\uff0c\u5237\u65b0\u4e2d...';
          setTimeout(function(){ location.reload(); }, 2000);
        } else {
          btn.textContent = '\u274c \u5931\u8d25';
          btn.disabled = false;
          setTimeout(function(){
            btn.textContent = '\ud83d\udd04 \u91cd\u65b0\u5b66\u4e60';
          }, 2000);
        }
      })
      .catch(function(){
        btn.textContent = '\u274c \u5931\u8d25';
        btn.disabled = false;
        setTimeout(function(){
          btn.textContent = '\ud83d\udd04 \u91cd\u65b0\u5b66\u4e60';
        }, 2000);
      });
  };
  actionBar.appendChild(btn);
})();
</script>

</div>
{% endblock %}