dashboard.html 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <title>Switchboard Dashboard</title>
  5. <meta charset="UTF-8">
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7. <style>
  8. * { box-sizing: border-box; margin: 0; padding: 0; }
  9. body {
  10. font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  11. background: #f5f5f5;
  12. padding: 20px;
  13. }
  14. .container { max-width: 1200px; margin: 0 auto; }
  15. h1 { color: #2563eb; margin-bottom: 20px; }
  16. h2 { color: #1e40af; margin: 30px 0 15px; font-size: 1.5em; }
  17. /* API Key Section */
  18. #api-key-section {
  19. background: white;
  20. padding: 20px;
  21. border-radius: 8px;
  22. margin-bottom: 20px;
  23. box-shadow: 0 1px 3px rgba(0,0,0,0.1);
  24. }
  25. #api-key-section input {
  26. width: 100%;
  27. padding: 10px;
  28. border: 1px solid #ddd;
  29. border-radius: 4px;
  30. font-size: 14px;
  31. }
  32. /* Buttons */
  33. button {
  34. background: #2563eb;
  35. color: white;
  36. border: none;
  37. padding: 10px 20px;
  38. border-radius: 4px;
  39. cursor: pointer;
  40. font-size: 14px;
  41. margin-top: 10px;
  42. }
  43. button:hover { background: #1d4ed8; }
  44. button.danger { background: #dc2626; }
  45. button.danger:hover { background: #b91c1c; }
  46. /* Tabs */
  47. .tabs {
  48. display: flex;
  49. gap: 10px;
  50. margin-bottom: 20px;
  51. }
  52. .tab {
  53. padding: 10px 20px;
  54. background: white;
  55. border: none;
  56. border-radius: 8px 8px 0 0;
  57. cursor: pointer;
  58. }
  59. .tab.active { background: #2563eb; color: white; }
  60. /* Content */
  61. .content {
  62. background: white;
  63. padding: 20px;
  64. border-radius: 8px;
  65. box-shadow: 0 1px 3px rgba(0,0,0,0.1);
  66. display: none;
  67. }
  68. .content.active { display: block; }
  69. /* Tables */
  70. table {
  71. width: 100%;
  72. border-collapse: collapse;
  73. margin-top: 15px;
  74. }
  75. th, td {
  76. padding: 12px;
  77. text-align: left;
  78. border-bottom: 1px solid #e5e7eb;
  79. }
  80. th { background: #f9fafb; font-weight: 600; }
  81. tr:hover { background: #f9fafb; }
  82. /* Forms */
  83. .form-group {
  84. margin-bottom: 15px;
  85. }
  86. label {
  87. display: block;
  88. margin-bottom: 5px;
  89. font-weight: 500;
  90. }
  91. input[type="text"], input[type="datetime-local"], textarea {
  92. width: 100%;
  93. padding: 8px;
  94. border: 1px solid #ddd;
  95. border-radius: 4px;
  96. }
  97. textarea { min-height: 80px; }
  98. /* Status badges */
  99. .badge {
  100. padding: 4px 8px;
  101. border-radius: 4px;
  102. font-size: 12px;
  103. font-weight: 500;
  104. }
  105. .badge.success { background: #d1fae5; color: #065f46; }
  106. .badge.failed { background: #fee2e2; color: #991b1b; }
  107. .hidden { display: none; }
  108. .error { color: #dc2626; margin-top: 10px; }
  109. .empty { text-align: center; padding: 40px; color: #6b7280; }
  110. </style>
  111. </head>
  112. <body>
  113. <div class="container">
  114. <h1>🔀 Switchboard Dashboard</h1>
  115. <!-- API Key Input -->
  116. <div id="api-key-section">
  117. <label for="api-key-input">Letta API Key:</label>
  118. <input type="password" id="api-key-input" placeholder="sk-let-...">
  119. <button onclick="saveApiKey()">Save Key</button>
  120. <button onclick="clearApiKey()" class="danger">Clear Key</button>
  121. <div id="api-key-status"></div>
  122. </div>
  123. <div id="main-content" class="hidden">
  124. <!-- Tabs -->
  125. <div class="tabs">
  126. <button class="tab active" onclick="showTab('schedules')">Schedules</button>
  127. <button class="tab" onclick="showTab('create')">Create New</button>
  128. <button class="tab" onclick="showTab('results')">Results</button>
  129. </div>
  130. <!-- Schedules Tab -->
  131. <div id="schedules-tab" class="content active">
  132. <h2>One-Time Schedules</h2>
  133. <table id="onetime-table">
  134. <thead>
  135. <tr>
  136. <th>ID</th>
  137. <th>Agent ID</th>
  138. <th>Execute At</th>
  139. <th>Message</th>
  140. <th>Actions</th>
  141. </tr>
  142. </thead>
  143. <tbody></tbody>
  144. </table>
  145. <h2>Recurring Schedules</h2>
  146. <table id="recurring-table">
  147. <thead>
  148. <tr>
  149. <th>ID</th>
  150. <th>Agent ID</th>
  151. <th>Cron</th>
  152. <th>Message</th>
  153. <th>Last Run</th>
  154. <th>Actions</th>
  155. </tr>
  156. </thead>
  157. <tbody></tbody>
  158. </table>
  159. <button onclick="loadSchedules()" style="margin-top: 20px;">Refresh</button>
  160. </div>
  161. <!-- Create Tab -->
  162. <div id="create-tab" class="content">
  163. <h2>Create Schedule</h2>
  164. <div class="form-group">
  165. <label>Schedule Type:</label>
  166. <select id="schedule-type" onchange="toggleScheduleType()">
  167. <option value="onetime">One-Time</option>
  168. <option value="recurring">Recurring</option>
  169. </select>
  170. </div>
  171. <div class="form-group">
  172. <label for="agent-id">Agent ID:</label>
  173. <input type="text" id="agent-id" placeholder="agent-xxx">
  174. </div>
  175. <div class="form-group">
  176. <label for="message">Message:</label>
  177. <textarea id="message" placeholder="Your message to the agent"></textarea>
  178. </div>
  179. <div id="onetime-fields">
  180. <div class="form-group">
  181. <label for="execute-at">Execute At (ISO 8601):</label>
  182. <input type="datetime-local" id="execute-at">
  183. <small style="display:block;margin-top:5px;color:#6b7280;">Or use: 2025-11-18T10:00:00Z</small>
  184. </div>
  185. </div>
  186. <div id="recurring-fields" class="hidden">
  187. <div class="form-group">
  188. <label for="cron">Cron Expression:</label>
  189. <input type="text" id="cron" placeholder="0 9 * * 1-5">
  190. <small style="display:block;margin-top:5px;color:#6b7280;">Examples: "0 9 * * *" (daily at 9am), "*/5 * * * *" (every 5 min)</small>
  191. </div>
  192. </div>
  193. <button onclick="createSchedule()">Create Schedule</button>
  194. <div id="create-error" class="error"></div>
  195. </div>
  196. <!-- Results Tab -->
  197. <div id="results-tab" class="content">
  198. <h2>Execution Results</h2>
  199. <table id="results-table">
  200. <thead>
  201. <tr>
  202. <th>Schedule ID</th>
  203. <th>Type</th>
  204. <th>Status</th>
  205. <th>Agent ID</th>
  206. <th>Message</th>
  207. <th>Run ID / Error</th>
  208. <th>Executed At</th>
  209. </tr>
  210. </thead>
  211. <tbody></tbody>
  212. </table>
  213. <button onclick="loadResults()" style="margin-top: 20px;">Refresh</button>
  214. </div>
  215. </div>
  216. </div>
  217. <script>
  218. const API_BASE = window.location.origin;
  219. let API_KEY = sessionStorage.getItem('letta_api_key') || '';
  220. // Initialize
  221. if (API_KEY) {
  222. document.getElementById('api-key-input').value = API_KEY;
  223. document.getElementById('main-content').classList.remove('hidden');
  224. loadSchedules();
  225. loadResults();
  226. }
  227. function saveApiKey() {
  228. API_KEY = document.getElementById('api-key-input').value.trim();
  229. if (!API_KEY) {
  230. alert('Please enter an API key');
  231. return;
  232. }
  233. sessionStorage.setItem('letta_api_key', API_KEY);
  234. document.getElementById('main-content').classList.remove('hidden');
  235. document.getElementById('api-key-status').innerHTML = '<p style="color:green;margin-top:10px;">✓ API key saved</p>';
  236. loadSchedules();
  237. loadResults();
  238. }
  239. function clearApiKey() {
  240. sessionStorage.removeItem('letta_api_key');
  241. API_KEY = '';
  242. document.getElementById('api-key-input').value = '';
  243. document.getElementById('main-content').classList.add('hidden');
  244. document.getElementById('api-key-status').innerHTML = '<p style="color:red;margin-top:10px;">API key cleared</p>';
  245. }
  246. function showTab(tab) {
  247. document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
  248. document.querySelectorAll('.content').forEach(c => c.classList.remove('active'));
  249. event.target.classList.add('active');
  250. document.getElementById(`${tab}-tab`).classList.add('active');
  251. }
  252. function toggleScheduleType() {
  253. const type = document.getElementById('schedule-type').value;
  254. document.getElementById('onetime-fields').classList.toggle('hidden', type !== 'onetime');
  255. document.getElementById('recurring-fields').classList.toggle('hidden', type !== 'recurring');
  256. }
  257. async function loadSchedules() {
  258. try {
  259. const [onetime, recurring] = await Promise.all([
  260. fetch(`${API_BASE}/schedules/one-time`, {
  261. headers: { 'Authorization': `Bearer ${API_KEY}` }
  262. }).then(r => r.json()),
  263. fetch(`${API_BASE}/schedules/recurring`, {
  264. headers: { 'Authorization': `Bearer ${API_KEY}` }
  265. }).then(r => r.json())
  266. ]);
  267. // One-time schedules
  268. const onetimeBody = document.querySelector('#onetime-table tbody');
  269. onetimeBody.innerHTML = onetime.length ? onetime.map(s => `
  270. <tr>
  271. <td>${s.id.substring(0, 8)}...</td>
  272. <td>${s.agent_id}</td>
  273. <td>${new Date(s.execute_at).toLocaleString()}</td>
  274. <td>${truncate(s.message, 50)}</td>
  275. <td><button class="danger" onclick="deleteSchedule('one-time', '${s.id}')">Delete</button></td>
  276. </tr>
  277. `).join('') : '<tr><td colspan="5" class="empty">No one-time schedules</td></tr>';
  278. // Recurring schedules
  279. const recurringBody = document.querySelector('#recurring-table tbody');
  280. recurringBody.innerHTML = recurring.length ? recurring.map(s => `
  281. <tr>
  282. <td>${s.id.substring(0, 8)}...</td>
  283. <td>${s.agent_id}</td>
  284. <td>${s.cron}</td>
  285. <td>${truncate(s.message, 50)}</td>
  286. <td>${s.last_run ? new Date(s.last_run).toLocaleString() : 'Never'}</td>
  287. <td><button class="danger" onclick="deleteSchedule('recurring', '${s.id}')">Delete</button></td>
  288. </tr>
  289. `).join('') : '<tr><td colspan="6" class="empty">No recurring schedules</td></tr>';
  290. } catch (error) {
  291. alert('Failed to load schedules: ' + error.message);
  292. }
  293. }
  294. async function loadResults() {
  295. try {
  296. const results = await fetch(`${API_BASE}/results`, {
  297. headers: { 'Authorization': `Bearer ${API_KEY}` }
  298. }).then(r => r.json());
  299. const resultsBody = document.querySelector('#results-table tbody');
  300. resultsBody.innerHTML = results.length ? results.map(r => `
  301. <tr>
  302. <td>${r.schedule_id.substring(0, 8)}...</td>
  303. <td>${r.schedule_type}</td>
  304. <td><span class="badge ${r.status}">${r.status}</span></td>
  305. <td>${r.agent_id}</td>
  306. <td>${truncate(r.message, 30)}</td>
  307. <td>${r.run_id || r.error || 'N/A'}</td>
  308. <td>${new Date(r.executed_at).toLocaleString()}</td>
  309. </tr>
  310. `).join('') : '<tr><td colspan="7" class="empty">No execution results</td></tr>';
  311. } catch (error) {
  312. alert('Failed to load results: ' + error.message);
  313. }
  314. }
  315. async function createSchedule() {
  316. const type = document.getElementById('schedule-type').value;
  317. const agentId = document.getElementById('agent-id').value.trim();
  318. const message = document.getElementById('message').value.trim();
  319. const errorDiv = document.getElementById('create-error');
  320. errorDiv.textContent = '';
  321. if (!agentId || !message) {
  322. errorDiv.textContent = 'Please fill in all fields';
  323. return;
  324. }
  325. try {
  326. const payload = {
  327. agent_id: agentId,
  328. message: message,
  329. role: 'user'
  330. };
  331. if (type === 'onetime') {
  332. const executeAt = document.getElementById('execute-at').value;
  333. if (!executeAt) {
  334. errorDiv.textContent = 'Please specify execution time';
  335. return;
  336. }
  337. // Convert datetime-local to ISO 8601
  338. payload.execute_at = new Date(executeAt).toISOString();
  339. } else {
  340. const cron = document.getElementById('cron').value.trim();
  341. if (!cron) {
  342. errorDiv.textContent = 'Please specify cron expression';
  343. return;
  344. }
  345. payload.cron = cron;
  346. }
  347. const endpoint = type === 'onetime' ? '/schedules/one-time' : '/schedules/recurring';
  348. const response = await fetch(`${API_BASE}${endpoint}`, {
  349. method: 'POST',
  350. headers: {
  351. 'Authorization': `Bearer ${API_KEY}`,
  352. 'Content-Type': 'application/json'
  353. },
  354. body: JSON.stringify(payload)
  355. });
  356. if (!response.ok) {
  357. const error = await response.json();
  358. throw new Error(error.detail || 'Failed to create schedule');
  359. }
  360. alert('Schedule created successfully!');
  361. document.getElementById('agent-id').value = '';
  362. document.getElementById('message').value = '';
  363. document.getElementById('execute-at').value = '';
  364. document.getElementById('cron').value = '';
  365. showTab('schedules');
  366. loadSchedules();
  367. } catch (error) {
  368. errorDiv.textContent = 'Error: ' + error.message;
  369. }
  370. }
  371. async function deleteSchedule(type, id) {
  372. if (!confirm('Are you sure you want to delete this schedule?')) return;
  373. try {
  374. const response = await fetch(`${API_BASE}/schedules/${type}/${id}`, {
  375. method: 'DELETE',
  376. headers: { 'Authorization': `Bearer ${API_KEY}` }
  377. });
  378. if (!response.ok) throw new Error('Failed to delete');
  379. alert('Schedule deleted successfully!');
  380. loadSchedules();
  381. } catch (error) {
  382. alert('Error deleting schedule: ' + error.message);
  383. }
  384. }
  385. function truncate(str, len) {
  386. return str.length > len ? str.substring(0, len) + '...' : str;
  387. }
  388. </script>
  389. </body>
  390. </html>