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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
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>