chat.html 15.4 KB
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title id="pageTitle">WeChat Chat</title>
<style>
* { margin:0;padding:0;box-sizing:border-box; }
body { font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif; background:#f0f2f5; }

/* Header */
.chat-header { position:sticky;top:0;z-index:100;background:#2c3e50;color:white;padding:10px 16px;display:flex;align-items:center;gap:12px; }
.chat-header a { color:white;text-decoration:none;font-size:20px; }
.chat-header h2 { font-size:16px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis; }
.chat-header .sub { font-size:11px;color:#aaa; }

/* Messages */
.messages { max-width:700px;margin:0 auto;padding:16px 12px;display:flex;flex-direction:column; }
.msg-row { display:flex;margin-bottom:16px;align-items:flex-start;gap:8px; }
.msg-row.sent { justify-content:flex-end; }
.msg-row.received { justify-content:flex-start; }

/* Avatar */
.msg-avatar { width:36px;height:36px;border-radius:4px;flex-shrink:0;
  display:flex;align-items:center;justify-content:center;
  font-size:15px;font-weight:bold;color:white;user-select:none; }

/* Content */
.msg-content { max-width:65%;display:flex;flex-direction:column; }
.msg-row.sent .msg-content { align-items:flex-end; }
.msg-row.received .msg-content { align-items:flex-start; }

/* Nickname */
.msg-nickname { font-size:11px;color:#999;margin-bottom:2px;padding-left:2px; }

/* Bubble */
.msg-bubble { padding:10px 14px;border-radius:8px;font-size:14px;line-height:1.5;word-break:break-word; }
.msg-row.received .msg-bubble { background:white;color:#333; }
.msg-row.sent .msg-bubble { background:#95EC69;color:#333; }

/* Time */
.msg-time { font-size:11px;color:#999;margin-top:3px;padding:0 2px; }

/* Image */
.msg-image-wrap { position:relative;display:inline-block;max-width:280px; }
.msg-image-wrap img { width:100%;border-radius:6px;cursor:pointer; }

/* Article card */
.article-card { background:white;border-radius:8px;padding:12px;margin:4px 0;border:1px solid #e8e8e8;text-decoration:none;color:inherit;display:block; }
.article-card:hover { background:#f8f9fa; }
.article-card .card-thumb { width:60px;height:60px;border-radius:4px;object-fit:cover;flex-shrink:0;background:#f0f0f0; }
.article-card .card-title { font-size:13px;font-weight:600;line-height:1.4;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden; }
.article-card .card-desc { font-size:11px;color:#999;margin-top:4px;display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical;overflow:hidden; }

/* Finder card */
.finder-card { background:white;border-radius:8px;overflow:hidden;margin:4px 0;border:1px solid #e8e8e8;cursor:pointer; }
.finder-card .finder-cover { position:relative;width:100%;aspect-ratio:9/16;max-height:260px;background:#000;overflow:hidden; }
.finder-card .finder-cover img { width:100%;height:100%;object-fit:cover; }
.finder-card .finder-play { position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:44px;height:44px;background:rgba(0,0,0,0.6);border-radius:50%;display:flex;align-items:center;justify-content:center; }
.finder-card .finder-play::after { content:'';border-left:12px solid white;border-top:7px solid transparent;border-bottom:7px solid transparent;margin-left:3px; }
.finder-card .finder-dur { position:absolute;bottom:6px;right:6px;background:rgba(0,0,0,0.7);color:white;font-size:10px;padding:2px 6px;border-radius:3px; }
.finder-card .finder-info { padding:8px 10px;display:flex;gap:8px;align-items:flex-start; }
.finder-card .finder-avatar { width:28px;height:28px;border-radius:50%;flex-shrink:0; }
.finder-card .finder-nick { font-size:12px;font-weight:600;color:#576b95; }
.finder-card .finder-desc { font-size:11px;color:#333;margin-top:3px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden; }

/* Load more */
.load-more { text-align:center;padding:16px;color:#999;font-size:13px;cursor:pointer; }
.load-more:hover { color:#3498db; }
.spinner { display:inline-block;width:20px;height:20px;border:3px solid #ddd;border-top-color:#3498db;border-radius:50%;animation:spin .6s linear infinite; }
@keyframes spin { to{transform:rotate(360deg); } }
.empty { text-align:center;padding:60px 20px;color:#999; }

/* Modal */
.img-modal { display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.9);z-index:999;align-items:center;justify-content:center; }
.img-modal img { max-width:95%;max-height:85%;border-radius:4px; }

audio { max-width:200px; }
</style>
</head>
<body>

<div class="chat-header">
  <a href="/">←</a>
  <div style="flex:1;min-width:0;">
    <h2 id="chatName">loading...</h2>
    <div class="sub" id="chatSub"></div>
  </div>
  <button onclick="showDatePicker()" style="background:#3498db;color:white;border:none;padding:6px 12px;border-radius:6px;font-size:13px;cursor:pointer;white-space:nowrap;">📋 generate report</button>
</div>

<div class="messages" id="messageList">
  <div class="empty"><div class="spinner"></div></div>
</div>

<div id="imgModal" class="img-modal" onclick="this.style.display='none'">
  <img id="imgModalSrc">
</div>

<script>
const talker = '{{ talker }}';
let offset = 0;
let loading = false;
let hasMore = true;

// Load messages
async function loadMessages() {
  if (loading || !hasMore) return;
  loading = true;

  if (offset === 0) {
    document.getElementById('messageList').innerHTML = '';
  }

  try {
    const r = await fetch('/api/messages?talker=' + encodeURIComponent(talker) + '&limit=40&offset=' + offset);
    const j = await r.json();
    if (!j.ok) return;

    const msgs = j.data;
    if (msgs.length < 40) hasMore = false;

    for (var i = 0; i < msgs.length; i++) {
      document.getElementById('messageList').insertAdjacentHTML('beforeend', renderMsg(msgs[i]));
    }
    offset += msgs.length;

    if (!hasMore && msgs.length === 0 && offset === 0) {
      document.getElementById('messageList').innerHTML = '<div class="empty">no messages</div>';
    }
  } catch(e) {}
  loading = false;
}

// Render single message
function renderMsg(m) {
  var cls = m.isSend ? 'sent' : 'received';
  var time = new Date((m.createTime||0)*1000);
  var now = new Date();
  var isToday = time.getDate() === now.getDate() &&
                time.getMonth() === now.getMonth() &&
                time.getFullYear() === now.getFullYear();
  var timeStr = isToday
    ? time.toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'})
    : pad(time.getMonth()+1) + '-' + pad(time.getDate()) + ' ' + time.toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'});
  var p = m;
  var inner = '';

  // Images
  if (p.images && p.images.length) {
    for (var im = 0; im < p.images.length; im++) {
      var img = p.images[im];
      var imgUrl = '/media/image?path=' + encodeURIComponent(img.src||'');
      var fullUrl = '/media/image/full?path=' + encodeURIComponent(img.src||'');
      inner += '<div class="msg-image-wrap"><img src="' + imgUrl + '" onclick="showImg(\'' + fullUrl + '\')" loading="lazy"></div>';
    }
  }

  // Voice
  if (p.voice) {
    inner += '<span>🎤</span> <audio controls src="/media/voice?path=' + encodeURIComponent(p.voice.path||'') + '"></audio>';
  }

  // Article cards
  if (p.articles && p.articles.length) {
    for (var ai = 0; ai < p.articles.length; ai++) {
      var a = p.articles[ai];
      if (a.title || a.description) {
        inner += '<div style="margin-bottom:4px;">';
        inner += '<a href="' + esc(a.url||'#') + '" target="_blank" class="article-card" style="display:block;">';
        inner += '<div style="display:flex;gap:10px;">';
        if (a.thumbnail) inner += '<img src="' + esc(a.thumbnail) + '" class="card-thumb" onerror="this.style.display=\'none\'">';
        else inner += '<div class="card-thumb" style="display:flex;align-items:center;justify-content:center;font-size:18px;">📄</div>';
        inner += '<div style="flex:1;min-width:0;">';
        inner += '<div class="card-title">' + esc(a.title||a.url||'') + '</div>';
        if (a.description) inner += '<div class="card-desc">' + esc(a.description) + '</div>';
        inner += '</div></div></a>';
        inner += '<div style="display:flex;gap:6px;margin-top:4px;">';
        inner += '<button class="summarize-btn" data-title="' + esc(a.title||'') + '" data-url="' + esc(a.url||'') + '" style="font-size:10px;padding:3px 8px;border:1px solid #3498db;border-radius:4px;background:white;color:#3498db;cursor:pointer;">📝 Summary</button>';
        inner += '<button class="learn-btn" data-title="' + esc(a.title||'') + '" data-url="' + esc(a.url||'') + '" style="font-size:10px;padding:3px 8px;border:1px solid #9b59b6;border-radius:4px;background:white;color:#9b59b6;cursor:pointer;">🔧 Learn</button>';
        inner += '</div></div>';
      } else {
        inner += '<div>📄 <a href="' + esc(a.url||'#') + '" target="_blank">' + esc(a.title||a.url) + '</a></div>';
      }
    }
  }

  // Finder
  if (p.finder && p.finder.nickname) {
    var f = p.finder;
    var dm = Math.floor(f.duration/60), ds = f.duration%60;
    inner += '<div class="finder-card" onclick="window.open(\'' + esc(f.video_url||'#') + '\',\'_blank\')">';
    inner += '<div class="finder-cover">';
    if (f.cover) inner += '<img src="' + esc(f.cover) + '" loading="lazy">';
    inner += '<div class="finder-play"></div>';
    if (f.duration) inner += '<div class="finder-dur">' + dm + ':' + pad(ds) + '</div>';
    inner += '</div>';
    inner += '<div class="finder-info">';
    if (f.avatar) inner += '<img src="' + esc(f.avatar) + '" class="finder-avatar">';
    inner += '<div style="flex:1;">';
    inner += '<div class="finder-nick">@' + esc(f.nickname) + '</div>';
    if (f.description) inner += '<div class="finder-desc">' + esc(f.description) + '</div>';
    inner += '</div></div></div>';
  }

  // Text
  if (p.text && p.text.trim()) {
    inner += '<div>' + linkify(esc(p.text)) + '</div>';
  }

  // Empty fallback
  if (!inner.trim()) {
    inner = '<span style="font-size:11px;color:#999;">[msg]</span>';
  }

  // Assemble layout
  var nicknameHtml = '';
  if (cls === 'received' && p.sender_nickname) {
    nicknameHtml = '<div class="msg-nickname">' + esc(p.sender_nickname) + '</div>';
  }

  var avatarHtml = '';
  if (cls === 'received') {
    var nameForInit = p.sender_nickname || p.sender_username || '?';
    var initial = nameForInit.charAt(0).toUpperCase();
    var colors = ['#3498db','#e74c3c','#2ecc71','#f39c12','#9b59b6','#1abc9c','#e67e22','#34495e'];
    var seed = (p.sender_username || '').length || 1;
    var color = colors[seed % colors.length];
    var username = p.sender_username || '';
    if (username) {
      avatarHtml = '<img class="msg-avatar" src="/media/avatar/' + esc(username) + '" ' +
        'onerror="this.onerror=null;var d=document.createElement(\'div\');d.className=\'msg-avatar\';' +
        'd.style.cssText=\'background:' + color + ';display:flex;align-items:center;justify-content:center;font-size:15px;font-weight:bold;color:white\';' +
        'd.textContent=\'' + esc(initial) + '\';this.parentNode.insertBefore(d,this);this.remove()" ' +
        'style="object-fit:cover;">';
    } else {
      avatarHtml = '<div class="msg-avatar" style="background:' + color + '">' + esc(initial) + '</div>';
    }
  }

  if (cls === 'sent') {
    return '<div class="msg-row sent"><div class="msg-content"><div class="msg-bubble">' + inner + '</div><div class="msg-time">' + timeStr + '</div></div></div>';
  } else {
    return '<div class="msg-row received">' + avatarHtml + '<div class="msg-content">' + nicknameHtml + '<div class="msg-bubble">' + inner + '</div><div class="msg-time">' + timeStr + '</div></div></div>';
  }
}

// Utils
function pad(n) { return String(n).padStart(2,'0'); }
function esc(s) { return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
function linkify(t) { return t.replace(/(https?:\/\/[^\s<]+)/g,'<a href="$1" target="_blank">$1</a>'); }
function showImg(url) { document.getElementById('imgModalSrc').src=url;document.getElementById('imgModal').style.display='flex'; }

// Event delegation for article action buttons
document.getElementById('messageList').addEventListener('click', function(e) {
  var btn = e.target;
  if (btn.classList.contains('summarize-btn')) {
    e.preventDefault();
    var t = btn.getAttribute('data-title');
    var u = btn.getAttribute('data-url');
    doSummarize(t, u);
  } else if (btn.classList.contains('learn-btn')) {
    e.preventDefault();
    var t = btn.getAttribute('data-title');
    var u = btn.getAttribute('data-url');
    doLearn(t, u);
  }
});

// Infinite scroll
window.addEventListener('scroll', function() {
  if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 300) {
    loadMessages();
  }
});

// Load chat info
async function loadInfo() {
  try {
    var r = await fetch('/api/talker/' + encodeURIComponent(talker));
    var j = await r.json();
    if (j.ok && j.data) {
      document.getElementById('chatName').textContent = j.data.displayname || j.data.nickname || talker;
      var sub = j.data.memberCount ? j.data.memberCount + ' people' : '';
      document.getElementById('chatSub').textContent = sub;
      document.title = j.data.displayname || j.data.nickname || 'Chat';
    }
  } catch(e) {}
}

// Report generation
function showDatePicker() {
  var yesterday = new Date(Date.now()-86400000).toISOString().slice(0,10);
  var date = prompt('Date (YYYY-MM-DD):', yesterday);
  if (!date) return;
  generateReport(date);
}

async function generateReport(date) {
  var btn = document.querySelector('.chat-header button');
  btn.textContent = 'Submitting...';
  btn.disabled = true;
  try {
    var r = await fetch('/api/reports/generate', {
      method: 'POST',
      headers: {'Content-Type':'application/json'},
      body: JSON.stringify({chatroom_id: talker, date: date})
    });
    var j = await r.json();
    if (!j.ok) { alert('Failed: ' + (j.error || '')); btn.textContent = '📋 Report'; btn.disabled = false; return; }

    btn.textContent = 'Generating...';
    for (var i = 0; i < 60; i++) {
      await new Promise(function(rr) { setTimeout(rr, 3000); });
      try {
        var sr = await fetch('/api/reports/generate/status');
        var sj = await sr.json();
        if (sj.ok && sj.data && sj.data.content) {
          if (confirm('Done! View reports?')) {
            window.location.href = '/reports';
          }
          btn.textContent = '📋 Report';
          btn.disabled = false;
          return;
        }
      } catch(e) {}
    }
    alert('Timeout. Check reports list.');
  } catch(e) { alert('Error: ' + e.message); }
  btn.textContent = '📋 Report';
  btn.disabled = false;
}

async function doSummarize(title, url) {
  if (!confirm('Generate summary for this article?')) return;
  try {
    var r = await fetch('/api/articles/summarize', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({title:title,url:url})});
    var j = await r.json();
    if (j.ok) alert('Task submitted!');
    else alert('Failed: '+(j.error||''));
  } catch(e) { alert('Error: '+e.message); }
}

async function doLearn(title, url) {
  if (!confirm('Learn this article?')) return;
  try {
    var r = await fetch('/api/articles/learn', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({title:title,url:url})});
    var j = await r.json();
    if (j.ok) alert('Task submitted!');
    else alert('Failed: '+(j.error||''));
  } catch(e) { alert('Error: '+e.message); }
}

loadInfo();
loadMessages();
</script>
</body>
</html>