conversations.html

  1<!DOCTYPE html>
  2<html lang="en">
  3<head>
  4  <meta charset="UTF-8">
  5  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6  <title>Debug: Conversations</title>
  7  <style>
  8    * { box-sizing: border-box; }
  9    body {
 10      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
 11      margin: 0;
 12      padding: 20px;
 13      background: #fff;
 14      color: #1a1a1a;
 15    }
 16    h1 { margin: 0 0 20px 0; font-size: 24px; }
 17    .search-box {
 18      margin-bottom: 16px;
 19    }
 20    .search-box input {
 21      padding: 8px 12px;
 22      font-size: 14px;
 23      border: 1px solid #ccc;
 24      border-radius: 4px;
 25      width: 300px;
 26    }
 27    .search-box input:focus {
 28      outline: none;
 29      border-color: #1976d2;
 30    }
 31    table {
 32      width: 100%;
 33      border-collapse: collapse;
 34      font-size: 14px;
 35    }
 36    th, td {
 37      padding: 10px 12px;
 38      text-align: left;
 39      border-bottom: 1px solid #e0e0e0;
 40    }
 41    th {
 42      background: #f5f5f5;
 43      font-weight: 600;
 44      position: sticky;
 45      top: 0;
 46    }
 47    tr:hover { background: #f8f8f8; }
 48    .slug { font-family: 'SF Mono', Monaco, monospace; font-size: 13px; }
 49    .date { color: #666; font-size: 13px; }
 50    .btn {
 51      background: #f5f5f5;
 52      border: 1px solid #ccc;
 53      color: #333;
 54      padding: 6px 12px;
 55      border-radius: 4px;
 56      cursor: pointer;
 57      font-size: 13px;
 58      text-decoration: none;
 59      display: inline-block;
 60    }
 61    .btn:hover { background: #e8e8e8; }
 62    .btn:disabled { opacity: 0.5; cursor: not-allowed; }
 63    .loading { color: #666; font-style: italic; }
 64    .info {
 65      margin-top: 20px;
 66      padding: 12px;
 67      background: #e3f2fd;
 68      border-radius: 4px;
 69      font-size: 13px;
 70    }
 71    .info code {
 72      background: #fff;
 73      padding: 2px 6px;
 74      border-radius: 3px;
 75      font-family: 'SF Mono', Monaco, monospace;
 76    }
 77  </style>
 78</head>
 79<body>
 80  <h1>Conversations</h1>
 81  
 82  <div class="search-box">
 83    <input type="text" id="search" placeholder="Filter by slug..." oninput="filterTable()">
 84  </div>
 85  
 86  <table id="conversations-table">
 87    <thead>
 88      <tr>
 89        <th>Slug</th>
 90        <th>Created</th>
 91        <th>Actions</th>
 92      </tr>
 93    </thead>
 94    <tbody id="conversations-body">
 95      <tr><td colspan="3" class="loading">Loading...</td></tr>
 96    </tbody>
 97  </table>
 98  
 99  <div class="info">
100    <strong>CSV Format:</strong> <code>input_tokens,cache_write_tokens,cache_read_tokens,output_tokens</code><br>
101    One row per agent message. Use with the standalone <a href="https://phil-dev.exe.xyz:8000/">Token Spend Visualizer</a>.
102  </div>
103  
104  <script>
105    let allConversations = [];
106    
107    async function loadConversations() {
108      try {
109        const response = await fetch('/api/conversations?limit=500');
110        allConversations = await response.json();
111        renderTable(allConversations);
112      } catch (e) {
113        document.getElementById('conversations-body').innerHTML =
114          '<tr><td colspan="3" class="loading">Error loading conversations</td></tr>';
115      }
116    }
117    
118    function renderTable(conversations) {
119      const tbody = document.getElementById('conversations-body');
120      if (!conversations || conversations.length === 0) {
121        tbody.innerHTML = '<tr><td colspan="3">No conversations found</td></tr>';
122        return;
123      }
124      
125      tbody.innerHTML = conversations.map(conv => {
126        const slug = conv.slug || conv.conversation_id.slice(0, 8);
127        const date = new Date(conv.created_at).toLocaleString();
128        return `
129          <tr data-slug="${slug.toLowerCase()}">
130            <td class="slug">${escapeHtml(slug)}</td>
131            <td class="date">${date}</td>
132            <td>
133              <button class="btn" onclick="downloadCSV('${conv.conversation_id}', '${escapeHtml(slug)}')">Download CSV</button>
134            </td>
135          </tr>
136        `;
137      }).join('');
138    }
139    
140    function filterTable() {
141      const query = document.getElementById('search').value.toLowerCase();
142      const rows = document.querySelectorAll('#conversations-body tr');
143      rows.forEach(row => {
144        const slug = row.dataset.slug || '';
145        row.style.display = slug.includes(query) ? '' : 'none';
146      });
147    }
148    
149    function escapeHtml(str) {
150      return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
151    }
152    
153    async function downloadCSV(conversationId, slug) {
154      try {
155        const response = await fetch(`/api/conversation/${conversationId}`);
156        const data = await response.json();
157        
158        const rows = ['input_tokens,cache_write_tokens,cache_read_tokens,output_tokens'];
159        
160        for (const msg of data.messages) {
161          if (msg.type !== 'agent' || !msg.usage_data) continue;
162          
163          let usage;
164          try {
165            usage = JSON.parse(msg.usage_data);
166          } catch (e) {
167            continue;
168          }
169          
170          rows.push([
171            usage.input_tokens || 0,
172            usage.cache_creation_input_tokens || 0,
173            usage.cache_read_input_tokens || 0,
174            usage.output_tokens || 0
175          ].join(','));
176        }
177        
178        const csv = rows.join('\n');
179        const blob = new Blob([csv], { type: 'text/csv' });
180        const url = URL.createObjectURL(blob);
181        const a = document.createElement('a');
182        a.href = url;
183        a.download = `${slug}-tokens.csv`;
184        a.click();
185        URL.revokeObjectURL(url);
186      } catch (e) {
187        console.error('Failed to download CSV:', e);
188      }
189    }
190    
191    loadConversations();
192  </script>
193</body>
194</html>