dashboard.html 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657
  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: #f8f9fa;
  12. color: #1a1a1a;
  13. padding: 24px;
  14. line-height: 1.6;
  15. }
  16. .container { max-width: 1100px; margin: 0 auto; }
  17. /* Header */
  18. .header {
  19. display: flex;
  20. justify-content: space-between;
  21. align-items: center;
  22. margin-bottom: 24px;
  23. flex-wrap: wrap;
  24. gap: 16px;
  25. }
  26. .header h1 {
  27. font-size: 24px;
  28. font-weight: 600;
  29. }
  30. .header-links {
  31. display: flex;
  32. gap: 16px;
  33. }
  34. .header-links a {
  35. color: #666;
  36. text-decoration: none;
  37. font-size: 14px;
  38. }
  39. .header-links a:hover {
  40. color: #1a1a1a;
  41. }
  42. h2 {
  43. font-size: 16px;
  44. font-weight: 600;
  45. margin: 24px 0 12px 0;
  46. color: #1a1a1a;
  47. }
  48. /* API Key Section */
  49. .card {
  50. background: white;
  51. border: 1px solid #e0e0e0;
  52. border-radius: 8px;
  53. padding: 20px;
  54. margin-bottom: 20px;
  55. }
  56. .card-title {
  57. font-size: 14px;
  58. font-weight: 600;
  59. margin-bottom: 12px;
  60. color: #666;
  61. }
  62. #api-key-section input {
  63. width: 100%;
  64. padding: 10px 12px;
  65. border: 1px solid #e0e0e0;
  66. border-radius: 6px;
  67. font-size: 14px;
  68. margin-bottom: 12px;
  69. }
  70. #api-key-section input:focus {
  71. outline: none;
  72. border-color: #1a1a1a;
  73. }
  74. .btn-group {
  75. display: flex;
  76. gap: 8px;
  77. }
  78. /* Buttons */
  79. button, .btn {
  80. background: #1a1a1a;
  81. color: white;
  82. border: none;
  83. padding: 8px 16px;
  84. border-radius: 6px;
  85. cursor: pointer;
  86. font-size: 14px;
  87. font-weight: 500;
  88. }
  89. button:hover { background: #333; }
  90. button.secondary {
  91. background: white;
  92. color: #1a1a1a;
  93. border: 1px solid #e0e0e0;
  94. }
  95. button.secondary:hover {
  96. background: #f5f5f5;
  97. }
  98. button.danger {
  99. background: white;
  100. color: #dc2626;
  101. border: 1px solid #fecaca;
  102. }
  103. button.danger:hover {
  104. background: #fef2f2;
  105. }
  106. button.sm {
  107. padding: 6px 12px;
  108. font-size: 13px;
  109. }
  110. /* Tabs */
  111. .tabs {
  112. display: flex;
  113. gap: 4px;
  114. border-bottom: 1px solid #e0e0e0;
  115. margin-bottom: 20px;
  116. }
  117. .tab {
  118. padding: 10px 20px;
  119. background: transparent;
  120. color: #666;
  121. border: none;
  122. border-bottom: 2px solid transparent;
  123. cursor: pointer;
  124. font-weight: 500;
  125. font-size: 14px;
  126. border-radius: 0;
  127. margin-bottom: -1px;
  128. }
  129. .tab:hover {
  130. color: #1a1a1a;
  131. background: transparent;
  132. }
  133. .tab.active {
  134. color: #1a1a1a;
  135. border-bottom-color: #1a1a1a;
  136. background: transparent;
  137. }
  138. /* Content */
  139. .content {
  140. display: none;
  141. }
  142. .content.active { display: block; }
  143. /* Tables */
  144. .table-container {
  145. background: white;
  146. border: 1px solid #e0e0e0;
  147. border-radius: 8px;
  148. overflow: hidden;
  149. margin-bottom: 16px;
  150. }
  151. table {
  152. width: 100%;
  153. border-collapse: collapse;
  154. }
  155. th, td {
  156. padding: 12px 16px;
  157. text-align: left;
  158. border-bottom: 1px solid #e0e0e0;
  159. }
  160. th {
  161. background: #f8f9fa;
  162. font-weight: 600;
  163. font-size: 13px;
  164. color: #666;
  165. }
  166. tr:last-child td { border-bottom: none; }
  167. tr:hover td { background: #fafafa; }
  168. td { font-size: 14px; }
  169. /* Forms */
  170. .form-group {
  171. margin-bottom: 16px;
  172. }
  173. label {
  174. display: block;
  175. margin-bottom: 6px;
  176. font-weight: 500;
  177. font-size: 14px;
  178. }
  179. input[type="text"], input[type="datetime-local"], textarea, select {
  180. width: 100%;
  181. padding: 10px 12px;
  182. border: 1px solid #e0e0e0;
  183. border-radius: 6px;
  184. font-size: 14px;
  185. font-family: inherit;
  186. }
  187. input:focus, textarea:focus, select:focus {
  188. outline: none;
  189. border-color: #1a1a1a;
  190. }
  191. textarea { min-height: 100px; resize: vertical; }
  192. .help-text { color: #666; font-size: 13px; margin-top: 4px; }
  193. /* Status badges */
  194. .badge {
  195. display: inline-flex;
  196. align-items: center;
  197. padding: 4px 10px;
  198. border-radius: 4px;
  199. font-size: 12px;
  200. font-weight: 500;
  201. }
  202. .badge.success {
  203. background: #dcfce7;
  204. color: #166534;
  205. }
  206. .badge.failed {
  207. background: #fef2f2;
  208. color: #dc2626;
  209. }
  210. .hidden { display: none; }
  211. .error { color: #dc2626; margin-top: 8px; font-size: 14px; }
  212. .empty {
  213. text-align: center;
  214. padding: 40px;
  215. color: #666;
  216. }
  217. /* Status indicator */
  218. .status-msg {
  219. display: flex;
  220. align-items: center;
  221. gap: 8px;
  222. padding: 8px 0;
  223. font-size: 14px;
  224. }
  225. .status-msg.success { color: #166534; }
  226. .status-msg.error { color: #dc2626; }
  227. /* Loading spinner */
  228. .loading {
  229. text-align: center;
  230. padding: 40px;
  231. color: #666;
  232. }
  233. .spinner {
  234. display: inline-block;
  235. width: 32px;
  236. height: 32px;
  237. border: 3px solid #e0e0e0;
  238. border-top-color: #1a1a1a;
  239. border-radius: 50%;
  240. animation: spin 0.8s linear infinite;
  241. margin-bottom: 8px;
  242. }
  243. @keyframes spin {
  244. to { transform: rotate(360deg); }
  245. }
  246. .mono {
  247. font-family: 'SF Mono', Consolas, monospace;
  248. font-size: 13px;
  249. }
  250. @media (max-width: 768px) {
  251. body { padding: 16px; }
  252. th, td { padding: 10px 12px; }
  253. .header { flex-direction: column; align-items: flex-start; }
  254. }
  255. </style>
  256. </head>
  257. <body>
  258. <div class="container">
  259. <div class="header">
  260. <h1>Switchboard Dashboard</h1>
  261. <div class="header-links">
  262. <a href="/">Home</a>
  263. <a href="https://github.com/cpfiffer/letta-switchboard">Docs</a>
  264. </div>
  265. </div>
  266. <!-- API Key Input -->
  267. <div id="api-key-section" class="card">
  268. <div class="card-title">Authentication</div>
  269. <input type="password" id="api-key-input" placeholder="Enter your Letta API key (sk-let-...)">
  270. <div class="btn-group">
  271. <button onclick="saveApiKey()">Connect</button>
  272. <button onclick="clearApiKey()" class="secondary">Disconnect</button>
  273. </div>
  274. <div id="api-key-status"></div>
  275. </div>
  276. <div id="main-content" class="hidden">
  277. <!-- Tabs -->
  278. <div class="tabs">
  279. <button class="tab active" onclick="showTab('schedules')">Schedules</button>
  280. <button class="tab" onclick="showTab('create')">Create New</button>
  281. <button class="tab" onclick="showTab('results')">Results</button>
  282. </div>
  283. <!-- Schedules Tab -->
  284. <div id="schedules-tab" class="content active">
  285. <h2>One-Time Schedules</h2>
  286. <div class="table-container">
  287. <table id="onetime-table">
  288. <thead>
  289. <tr>
  290. <th>ID</th>
  291. <th>Agent ID</th>
  292. <th>Execute At</th>
  293. <th>Message</th>
  294. <th></th>
  295. </tr>
  296. </thead>
  297. <tbody></tbody>
  298. </table>
  299. </div>
  300. <h2>Recurring Schedules</h2>
  301. <div class="table-container">
  302. <table id="recurring-table">
  303. <thead>
  304. <tr>
  305. <th>ID</th>
  306. <th>Agent ID</th>
  307. <th>Cron</th>
  308. <th>Timezone</th>
  309. <th>Message</th>
  310. <th>Last Run</th>
  311. <th></th>
  312. </tr>
  313. </thead>
  314. <tbody></tbody>
  315. </table>
  316. </div>
  317. <button onclick="loadSchedules()" class="secondary">Refresh</button>
  318. </div>
  319. <!-- Create Tab -->
  320. <div id="create-tab" class="content">
  321. <div class="card">
  322. <h2 style="margin-top: 0;">Create Schedule</h2>
  323. <div class="form-group">
  324. <label>Schedule Type</label>
  325. <select id="schedule-type" onchange="toggleScheduleType()">
  326. <option value="onetime">One-Time</option>
  327. <option value="recurring">Recurring</option>
  328. </select>
  329. </div>
  330. <div class="form-group">
  331. <label for="agent-id">Agent ID</label>
  332. <input type="text" id="agent-id" placeholder="agent-xxx">
  333. </div>
  334. <div class="form-group">
  335. <label for="message">Message</label>
  336. <textarea id="message" placeholder="Your message to the agent"></textarea>
  337. </div>
  338. <div id="onetime-fields">
  339. <div class="form-group">
  340. <label for="execute-at">Execute At</label>
  341. <input type="datetime-local" id="execute-at">
  342. </div>
  343. <div class="form-group">
  344. <label for="timezone">Timezone</label>
  345. <select id="timezone">
  346. <option value="local" selected>Local Browser Time</option>
  347. <option value="UTC">UTC</option>
  348. <option value="America/New_York">Eastern (US)</option>
  349. <option value="America/Chicago">Central (US)</option>
  350. <option value="America/Denver">Mountain (US)</option>
  351. <option value="America/Los_Angeles">Pacific (US)</option>
  352. <option value="Europe/London">London</option>
  353. <option value="Europe/Paris">Paris</option>
  354. <option value="Asia/Tokyo">Tokyo</option>
  355. </select>
  356. <div class="help-text">Time will be converted to UTC for storage</div>
  357. </div>
  358. </div>
  359. <div id="recurring-fields" class="hidden">
  360. <div class="form-group">
  361. <label for="cron">Cron Expression</label>
  362. <input type="text" id="cron" placeholder="0 9 * * 1-5">
  363. <div class="help-text">Examples: "0 9 * * *" (daily at 9am), "*/5 * * * *" (every 5 min)</div>
  364. </div>
  365. <div class="form-group">
  366. <label for="recurring-timezone">Timezone</label>
  367. <select id="recurring-timezone">
  368. <option value="UTC" selected>UTC</option>
  369. <option value="America/New_York">Eastern (US)</option>
  370. <option value="America/Chicago">Central (US)</option>
  371. <option value="America/Denver">Mountain (US)</option>
  372. <option value="America/Los_Angeles">Pacific (US)</option>
  373. <option value="Europe/London">London</option>
  374. <option value="Europe/Paris">Paris</option>
  375. <option value="Asia/Tokyo">Tokyo</option>
  376. </select>
  377. <div class="help-text">Cron expression will be evaluated in this timezone</div>
  378. </div>
  379. </div>
  380. <button onclick="createSchedule()">Create Schedule</button>
  381. <div id="create-error" class="error"></div>
  382. </div>
  383. </div>
  384. <!-- Results Tab -->
  385. <div id="results-tab" class="content">
  386. <h2>Execution Results</h2>
  387. <div class="table-container">
  388. <table id="results-table">
  389. <thead>
  390. <tr>
  391. <th>Schedule ID</th>
  392. <th>Type</th>
  393. <th>Status</th>
  394. <th>Agent ID</th>
  395. <th>Message</th>
  396. <th>Run ID / Error</th>
  397. <th>Executed At</th>
  398. </tr>
  399. </thead>
  400. <tbody></tbody>
  401. </table>
  402. </div>
  403. <button onclick="loadResults()" class="secondary">Refresh</button>
  404. </div>
  405. </div>
  406. </div>
  407. <script>
  408. const API_BASE = window.location.origin;
  409. let API_KEY = sessionStorage.getItem('letta_api_key') || '';
  410. // Initialize
  411. if (API_KEY) {
  412. document.getElementById('api-key-input').value = API_KEY;
  413. document.getElementById('main-content').classList.remove('hidden');
  414. loadSchedules();
  415. loadResults();
  416. }
  417. function saveApiKey() {
  418. API_KEY = document.getElementById('api-key-input').value.trim();
  419. if (!API_KEY) {
  420. showStatus('api-key-status', 'API key is required', 'error');
  421. return;
  422. }
  423. sessionStorage.setItem('letta_api_key', API_KEY);
  424. document.getElementById('main-content').classList.remove('hidden');
  425. showStatus('api-key-status', 'Connected successfully', 'success');
  426. loadSchedules();
  427. loadResults();
  428. }
  429. function clearApiKey() {
  430. sessionStorage.removeItem('letta_api_key');
  431. API_KEY = '';
  432. document.getElementById('api-key-input').value = '';
  433. document.getElementById('main-content').classList.add('hidden');
  434. showStatus('api-key-status', 'Disconnected', 'error');
  435. }
  436. function showStatus(elementId, message, type) {
  437. const el = document.getElementById(elementId);
  438. el.innerHTML = `<div class="status-msg ${type}">${message}</div>`;
  439. }
  440. function showTab(tab) {
  441. document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
  442. document.querySelectorAll('.content').forEach(c => c.classList.remove('active'));
  443. event.target.classList.add('active');
  444. document.getElementById(`${tab}-tab`).classList.add('active');
  445. }
  446. function toggleScheduleType() {
  447. const type = document.getElementById('schedule-type').value;
  448. document.getElementById('onetime-fields').classList.toggle('hidden', type !== 'onetime');
  449. document.getElementById('recurring-fields').classList.toggle('hidden', type !== 'recurring');
  450. }
  451. async function loadSchedules() {
  452. const onetimeBody = document.querySelector('#onetime-table tbody');
  453. const recurringBody = document.querySelector('#recurring-table tbody');
  454. onetimeBody.innerHTML = '<tr><td colspan="5" class="loading"><div class="spinner"></div><p>Loading...</p></td></tr>';
  455. recurringBody.innerHTML = '<tr><td colspan="7" class="loading"><div class="spinner"></div><p>Loading...</p></td></tr>';
  456. try {
  457. const [onetime, recurring] = await Promise.all([
  458. fetch(`${API_BASE}/schedules/one-time`, {
  459. headers: { 'Authorization': `Bearer ${API_KEY}` }
  460. }).then(r => r.json()),
  461. fetch(`${API_BASE}/schedules/recurring`, {
  462. headers: { 'Authorization': `Bearer ${API_KEY}` }
  463. }).then(r => r.json())
  464. ]);
  465. onetimeBody.innerHTML = onetime.length ? onetime.map(s => `
  466. <tr>
  467. <td class="mono">${s.id.substring(0, 8)}...</td>
  468. <td class="mono">${s.agent_id}</td>
  469. <td>${new Date(s.execute_at).toLocaleString()}</td>
  470. <td>${truncate(s.message, 50)}</td>
  471. <td><button class="danger sm" onclick="deleteSchedule('one-time', '${s.id}')">Delete</button></td>
  472. </tr>
  473. `).join('') : '<tr><td colspan="5" class="empty">No one-time schedules</td></tr>';
  474. recurringBody.innerHTML = recurring.length ? recurring.map(s => `
  475. <tr>
  476. <td class="mono">${s.id.substring(0, 8)}...</td>
  477. <td class="mono">${s.agent_id}</td>
  478. <td class="mono">${s.cron}</td>
  479. <td>${s.timezone || 'UTC'}</td>
  480. <td>${truncate(s.message, 50)}</td>
  481. <td>${s.last_run ? new Date(s.last_run).toLocaleString() : 'Never'}</td>
  482. <td><button class="danger sm" onclick="deleteSchedule('recurring', '${s.id}')">Delete</button></td>
  483. </tr>
  484. `).join('') : '<tr><td colspan="7" class="empty">No recurring schedules</td></tr>';
  485. } catch (error) {
  486. onetimeBody.innerHTML = `<tr><td colspan="5" class="empty">Error loading schedules</td></tr>`;
  487. recurringBody.innerHTML = `<tr><td colspan="7" class="empty">Error loading schedules</td></tr>`;
  488. }
  489. }
  490. async function loadResults() {
  491. const resultsBody = document.querySelector('#results-table tbody');
  492. resultsBody.innerHTML = '<tr><td colspan="7" class="loading"><div class="spinner"></div><p>Loading...</p></td></tr>';
  493. try {
  494. const results = await fetch(`${API_BASE}/results`, {
  495. headers: { 'Authorization': `Bearer ${API_KEY}` }
  496. }).then(r => r.json());
  497. resultsBody.innerHTML = results.length ? results.map(r => `
  498. <tr>
  499. <td class="mono">${r.schedule_id.substring(0, 8)}...</td>
  500. <td>${r.schedule_type}</td>
  501. <td><span class="badge ${r.status}">${r.status}</span></td>
  502. <td class="mono">${r.agent_id}</td>
  503. <td>${truncate(r.message, 30)}</td>
  504. <td class="mono">${r.run_id || r.error || '-'}</td>
  505. <td>${new Date(r.executed_at).toLocaleString()}</td>
  506. </tr>
  507. `).join('') : '<tr><td colspan="7" class="empty">No execution results</td></tr>';
  508. } catch (error) {
  509. resultsBody.innerHTML = `<tr><td colspan="7" class="empty">Error loading results</td></tr>`;
  510. }
  511. }
  512. async function createSchedule() {
  513. const type = document.getElementById('schedule-type').value;
  514. const agentId = document.getElementById('agent-id').value.trim();
  515. const message = document.getElementById('message').value.trim();
  516. const errorDiv = document.getElementById('create-error');
  517. errorDiv.textContent = '';
  518. if (!agentId || !message) {
  519. errorDiv.textContent = 'Please fill in all required fields';
  520. return;
  521. }
  522. try {
  523. const payload = {
  524. agent_id: agentId,
  525. message: message,
  526. role: 'user'
  527. };
  528. if (type === 'onetime') {
  529. const executeAt = document.getElementById('execute-at').value;
  530. const timezone = document.getElementById('timezone').value;
  531. if (!executeAt) {
  532. errorDiv.textContent = 'Please specify execution time';
  533. return;
  534. }
  535. let executeDate;
  536. if (timezone === 'local') {
  537. executeDate = new Date(executeAt);
  538. } else if (timezone === 'UTC') {
  539. executeDate = new Date(executeAt + 'Z');
  540. } else {
  541. executeDate = new Date(executeAt);
  542. }
  543. payload.execute_at = executeDate.toISOString();
  544. } else {
  545. const cron = document.getElementById('cron').value.trim();
  546. if (!cron) {
  547. errorDiv.textContent = 'Please specify cron expression';
  548. return;
  549. }
  550. payload.cron = cron;
  551. payload.timezone = document.getElementById('recurring-timezone').value;
  552. }
  553. const endpoint = type === 'onetime' ? '/schedules/one-time' : '/schedules/recurring';
  554. const response = await fetch(`${API_BASE}${endpoint}`, {
  555. method: 'POST',
  556. headers: {
  557. 'Authorization': `Bearer ${API_KEY}`,
  558. 'Content-Type': 'application/json'
  559. },
  560. body: JSON.stringify(payload)
  561. });
  562. if (!response.ok) {
  563. const error = await response.json();
  564. throw new Error(error.detail || 'Failed to create schedule');
  565. }
  566. document.getElementById('agent-id').value = '';
  567. document.getElementById('message').value = '';
  568. document.getElementById('execute-at').value = '';
  569. document.getElementById('cron').value = '';
  570. document.getElementById('recurring-timezone').value = 'UTC';
  571. showTab('schedules');
  572. document.querySelector('.tab').click();
  573. loadSchedules();
  574. } catch (error) {
  575. errorDiv.textContent = error.message;
  576. }
  577. }
  578. async function deleteSchedule(type, id) {
  579. if (!confirm('Delete this schedule?')) return;
  580. try {
  581. const response = await fetch(`${API_BASE}/schedules/${type}/${id}`, {
  582. method: 'DELETE',
  583. headers: { 'Authorization': `Bearer ${API_KEY}` }
  584. });
  585. if (!response.ok) throw new Error('Failed to delete');
  586. loadSchedules();
  587. } catch (error) {
  588. alert('Error: ' + error.message);
  589. }
  590. }
  591. function truncate(str, len) {
  592. return str.length > len ? str.substring(0, len) + '...' : str;
  593. }
  594. </script>
  595. </body>
  596. </html>