agents.html 12 KB
{% extends "base.html" %}
{% block title %}Agents - WeChat Viewer{% endblock %}
{% block page_title %}Agents{% endblock %}
{% block body %}
<style>
body{overflow-y:auto!important;height:auto!important;}
.agents-container{max-width:960px;margin:0 auto;padding:20px;}
.agents-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;flex-wrap:wrap;gap:8px;}
.agents-header h1{font-size:20px;color:#2c3e50;margin:0;}
.agents-header button{padding:6px 14px;background:#3498db;color:white;border:none;border-radius:6px;cursor:pointer;font-size:13px;}
.agents-controls{display:flex;gap:10px;margin-bottom:16px;flex-wrap:wrap;align-items:center;}
.agents-controls input{padding:7px 12px;border:1px solid #ddd;border-radius:6px;font-size:13px;flex:1;min-width:180px;max-width:280px;}
.agents-table{width:100%;border-collapse:collapse;background:white;border-radius:8px;overflow:hidden;box-shadow:0 1px 2px rgba(0,0,0,0.05);}
.agents-table th,.agents-table td{text-align:left;padding:10px 14px;border-bottom:1px solid #f0f0f0;font-size:13px;}
.agents-table th{background:#f8f9fa;color:#666;font-weight:600;font-size:12px;}
.agents-table tr:hover{background:#f8f9fa;}
.agents-table tr{cursor:pointer;}
.agent-name{font-weight:600;color:#2c3e50;}
.agent-default{font-size:10px;background:#27ae60;color:white;padding:1px 6px;border-radius:8px;margin-left:6px;}
.agent-meta{font-size:11px;color:#999;}
.empty-state{text-align:center;padding:60px 20px;color:#999;}
.loading{text-align:center;padding:40px;color:#999;}
.spinner{display:inline-block;width:24px;height:24px;border:3px solid #ddd;border-top-color:#3498db;border-radius:50%;animation:spin .6s linear infinite;}
@keyframes spin{to{transform:rotate(360deg)}}
.modal-overlay{display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);z-index:9999;align-items:center;justify-content:center;}
.modal-overlay.show{display:flex;}
.modal-box{background:white;border-radius:10px;padding:24px;max-width:500px;width:90%;box-shadow:0 4px 20px rgba(0,0,0,0.15);}
.modal-box h3{margin:0 0 16px;color:#2c3e50;}
.modal-box input,.modal-box textarea{width:100%;padding:8px 12px;border:1px solid #ddd;border-radius:6px;font-size:14px;margin-bottom:12px;box-sizing:border-box;}
.modal-box textarea{min-height:60px;resize:vertical;}
.modal-box button{padding:8px 20px;border:none;border-radius:6px;cursor:pointer;font-size:14px;margin-right:8px;}
.modal-box .btn-primary{background:#3498db;color:white;}
.modal-box .btn-danger{background:#e74c3c;color:white;}
.modal-box .btn-cancel{background:#ddd;color:#666;}
.chat-response{background:#f8f9fa;border-radius:8px;padding:12px;margin-top:12px;font-size:13px;max-height:300px;overflow-y:auto;white-space:pre-wrap;}
.agents-summary{font-size:11px;color:#999;margin-top:12px;text-align:right;}
@media(max-width:768px){
  .agents-controls{flex-direction:column;}
  .agents-controls input{max-width:100%;}
  .agents-table th,.agents-table td{padding:8px 10px;font-size:12px;}
}
</style>
<div class="agents-container">
<div class="agents-header">
<h1>🤖 Agents</h1>
<button onclick="showCreateModal()">➕ 新建</button>
</div>
<div class="agents-controls">
<input type="text" id="searchInput" placeholder="🔍 搜索 profile..." oninput="renderTable()">
<select id="agentFilter" onchange="renderTable()" style="padding:7px 12px;border:1px solid #ddd;border-radius:6px;font-size:13px;background:white;cursor:pointer;min-width:120px;">
  <option value="">全部类型</option>
  <option value="default">默认</option>
  <option value="custom">自定义</option>
</select>
<select id="modelFilter" onchange="renderTable()" style="padding:7px 12px;border:1px solid #ddd;border-radius:6px;font-size:13px;background:white;cursor:pointer;min-width:120px;">
  <option value="">全部模型</option>
</select>
<select id="sortBy" onchange="renderTable()" style="padding:7px 12px;border:1px solid #ddd;border-radius:6px;font-size:13px;background:white;cursor:pointer;min-width:120px;">
  <option value="name">按名称</option>
  <option value="model">按模型</option>
</select>
</div>
<div id="agents-list"></div>
<div class="agents-summary" id="agents-count"></div>
</div>

<div class="modal-overlay" id="create-modal">
<div class="modal-box">
<h3>创建新 Profile</h3>
<input type="text" id="create-name" placeholder="profile 名称(英文+数字+横线)">
<textarea id="create-desc" placeholder="描述(可选)"></textarea>
<div>
<button class="btn-primary" onclick="createProfile()">创建</button>
<button class="btn-cancel" onclick="hideCreateModal()">取消</button>
</div>
</div>
</div>

<div class="modal-overlay" id="chat-modal">
<div class="modal-box">
<h3 id="chat-title">发消息</h3>
<textarea id="chat-message" placeholder="输入消息..."></textarea>
<div id="chat-response-area"></div>
<div>
<button class="btn-primary" onclick="sendMessage()">发送</button>
<button class="btn-cancel" onclick="hideChatModal()">关闭</button>
</div>
</div>
</div>

<div class="modal-overlay" id="delete-modal">
<div class="modal-box">
<h3>确认删除</h3>
<p id="delete-confirm-text">确定要删除这个 profile 吗?</p>
<div>
<button class="btn-danger" id="delete-confirm-btn">删除</button>
<button class="btn-cancel" onclick="hideDeleteModal()">取消</button>
</div>
</div>
</div>

<script>
var allProfiles = [];
var pendingDeleteName = null;
var chatTargetName = null;

function escHtml(s){return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');}

function renderTable() {
  var el = document.getElementById('agents-list');
  var query = (document.getElementById('searchInput').value || '').toLowerCase();
  var af = document.getElementById('agentFilter').value;
  var mf = document.getElementById('modelFilter').value;
  var sb = document.getElementById('sortBy').value;
  var filtered = allProfiles.filter(function(p) {
    if (query && p.name.toLowerCase().indexOf(query) === -1) return false;
    if (af === 'default' && !p.is_default) return false;
    if (af === 'custom' && p.is_default) return false;
    if (mf && p.model !== mf) return false;
    return true;
  });
  if (sb === 'model') filtered.sort(function(a,b){return (a.model||'').localeCompare(b.model||'')});
  else filtered.sort(function(a,b){return (a.name||'').localeCompare(b.name||'')});

  document.getElementById('agents-count').textContent = '共 ' + filtered.length + ' 个 Profile';

  if (!filtered.length) {
    el.innerHTML = '<div class="empty-state"><p>没有 Profile</p></div>';
    return;
  }

  var html = '<table class="agents-table"><thead><tr>';
  html += '<th>Profile</th>';
  html += '<th>Model</th>';
  html += '<th>Provider</th>';
  html += '<th>Skills</th>';
  html += '<th>Memory</th>';
  html += '<th>操作</th>';
  html += '</tr></thead><tbody>';

  for (var i = 0; i < filtered.length; i++) {
    var p = filtered[i];
    html += '<tr onclick="location.href=\'/agent/' + escHtml(p.name) + '\'">';
    html += '<td><span class="agent-name">' + escHtml(p.name) + '</span>';
    if (p.is_default) html += '<span class="agent-default">◆</span>';
    html += '</td>';
    html += '<td>' + escHtml(p.model) + '</td>';
    html += '<td>' + escHtml(p.provider) + '</td>';
    html += '<td>' + (p.skills_count || 0) + '</td>';
    html += '<td>' + (p.memory_exists ? '✅' : '—') + '</td>';
    html += '<td>';
    html += '<button class="btn-primary" style="padding:4px 10px;font-size:12px;margin-right:4px;" onclick="event.stopPropagation();openChatModal(\'' + escHtml(p.name) + '\')">💬</button>';
    if (!p.is_default) {
      html += '<button class="btn-danger" style="padding:4px 10px;font-size:12px;" onclick="event.stopPropagation();openDeleteModal(\'' + escHtml(p.name) + '\')">🗑</button>';
    }
    html += '</td>';
    html += '</tr>';
  }
  html += '</tbody></table>';
  el.innerHTML = html;
}

function refreshAgents() {
  var el = document.getElementById('agents-list');
  el.innerHTML = '<div class="loading"><div class="spinner"></div><p style="margin-top:12px;">加载中...</p></div>';
  fetch('/api/profiles').then(function(r){return r.json()}).then(function(j){
    if (!j.ok) {
      el.innerHTML = '<div class="empty-state"><p>加载失败</p></div>';
      return;
    }
    allProfiles = j.data || [];
    // populate model filter
    var models = {};
    for (var i = 0; i < allProfiles.length; i++) { var m = allProfiles[i].model; if (m) models[m] = 1; }
    var sel = document.getElementById('modelFilter');
    var cur = sel.value;
    var opts = '<option value="">全部模型</option>';
    var mk = Object.keys(models).sort();
    for (var i = 0; i < mk.length; i++) opts += '<option value="' + escHtml(mk[i]) + '">' + escHtml(mk[i]) + '</option>';
    sel.innerHTML = opts;
    sel.value = cur;
    renderTable();
  }).catch(function(e){
    el.innerHTML = '<div class="empty-state"><p>加载失败: ' + e.message + '</p></div>';
  });
}

function showCreateModal() {
  document.getElementById('create-name').value = '';
  document.getElementById('create-desc').value = '';
  document.getElementById('create-modal').classList.add('show');
  document.getElementById('create-name').focus();
}

function hideCreateModal() {
  document.getElementById('create-modal').classList.remove('show');
}

function createProfile() {
  var name = document.getElementById('create-name').value.trim();
  var desc = document.getElementById('create-desc').value.trim();
  if (!name) { alert('请输入名称'); return; }
  fetch('/api/profile', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify({name: name, description: desc})
  }).then(function(r){return r.json()}).then(function(j){
    if (j.ok) {
      hideCreateModal();
      refreshAgents();
    } else {
      alert('创建失败: ' + (j.error || '未知错误'));
    }
  }).catch(function(e){
    alert('创建失败: ' + e.message);
  });
}

function openDeleteModal(name) {
  pendingDeleteName = name;
  document.getElementById('delete-confirm-text').textContent = '确定要删除 profile "' + name + '" 吗?此操作不可恢复。';
  document.getElementById('delete-modal').classList.add('show');
}

function hideDeleteModal() {
  document.getElementById('delete-modal').classList.remove('show');
  pendingDeleteName = null;
}

document.getElementById('delete-confirm-btn').onclick = function() {
  if (!pendingDeleteName) return;
  fetch('/api/profile/' + pendingDeleteName, {method: 'DELETE'})
    .then(function(r){return r.json()})
    .then(function(j){
      if (j.ok) {
        hideDeleteModal();
        refreshAgents();
      } else {
        alert('删除失败: ' + (j.error || '未知错误'));
      }
    }).catch(function(e){
      alert('删除失败: ' + e.message);
    });
};

function openChatModal(name) {
  chatTargetName = name;
  document.getElementById('chat-title').textContent = '发消息给 ' + name;
  document.getElementById('chat-message').value = '';
  document.getElementById('chat-response-area').innerHTML = '';
  document.getElementById('chat-modal').classList.add('show');
  document.getElementById('chat-message').focus();
}

function hideChatModal() {
  document.getElementById('chat-modal').classList.remove('show');
  chatTargetName = null;
}

function sendMessage() {
  var msg = document.getElementById('chat-message').value.trim();
  if (!msg || !chatTargetName) return;
  var area = document.getElementById('chat-response-area');
  area.innerHTML = '<div class="loading"><div class="spinner"></div><p>发送中...</p></div>';
  fetch('/api/profile/' + chatTargetName + '/chat', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify({message: msg})
  }).then(function(r){return r.json()}).then(function(j){
    if (j.ok) {
      var resp = j.data.response || '';
      area.innerHTML = '<div class="chat-response">' + escHtml(resp) + '</div>';
    } else {
      area.innerHTML = '<div class="chat-response" style="color:#e74c3c;">' + escHtml(j.error || '发送失败') + '</div>';
    }
  }).catch(function(e){
    area.innerHTML = '<div class="chat-response" style="color:#e74c3c;">' + escHtml(e.message) + '</div>';
  });
}

refreshAgents();
</script>
{% endblock %}