dashboard.html 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975
  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <title>Letta Scheduling 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. /* Modal */
  43. .modal {
  44. display: none;
  45. position: fixed;
  46. z-index: 1000;
  47. left: 0;
  48. top: 0;
  49. width: 100%;
  50. height: 100%;
  51. background: rgba(0, 0, 0, 0.5);
  52. }
  53. .modal.active {
  54. display: flex;
  55. align-items: center;
  56. justify-content: center;
  57. }
  58. .modal-content {
  59. background: white;
  60. border-radius: 8px;
  61. padding: 24px;
  62. max-width: 600px;
  63. width: 90%;
  64. max-height: 80vh;
  65. overflow-y: auto;
  66. }
  67. .modal-header {
  68. display: flex;
  69. justify-content: space-between;
  70. align-items: center;
  71. margin-bottom: 16px;
  72. }
  73. .modal-header h3 {
  74. font-size: 18px;
  75. font-weight: 600;
  76. }
  77. .modal-close {
  78. font-size: 24px;
  79. cursor: pointer;
  80. color: #666;
  81. }
  82. .modal-close:hover {
  83. color: #1a1a1a;
  84. }
  85. .modal-body {
  86. font-size: 14px;
  87. line-height: 1.6;
  88. white-space: pre-wrap;
  89. word-wrap: break-word;
  90. }
  91. .modal-label {
  92. font-weight: 600;
  93. margin-top: 12px;
  94. margin-bottom: 4px;
  95. color: #666;
  96. }
  97. /* Migration Banner */
  98. .migration-banner {
  99. background: #fff3cd;
  100. border: 2px solid #ffc107;
  101. border-radius: 8px;
  102. padding: 16px;
  103. margin-bottom: 24px;
  104. }
  105. .migration-banner h3 {
  106. font-size: 16px;
  107. font-weight: 600;
  108. margin-bottom: 8px;
  109. color: #856404;
  110. }
  111. .migration-banner p {
  112. font-size: 14px;
  113. color: #856404;
  114. margin-bottom: 8px;
  115. }
  116. .migration-banner a {
  117. color: #856404;
  118. font-weight: 600;
  119. }
  120. h2 {
  121. font-size: 16px;
  122. font-weight: 600;
  123. margin: 24px 0 12px 0;
  124. color: #1a1a1a;
  125. }
  126. /* API Key Section */
  127. .card {
  128. background: white;
  129. border: 1px solid #e0e0e0;
  130. border-radius: 8px;
  131. padding: 20px;
  132. margin-bottom: 20px;
  133. }
  134. .card-title {
  135. font-size: 14px;
  136. font-weight: 600;
  137. margin-bottom: 12px;
  138. color: #666;
  139. }
  140. #api-key-section input {
  141. width: 100%;
  142. padding: 10px 12px;
  143. border: 1px solid #e0e0e0;
  144. border-radius: 6px;
  145. font-size: 14px;
  146. margin-bottom: 12px;
  147. }
  148. #api-key-section input:focus {
  149. outline: none;
  150. border-color: #1a1a1a;
  151. }
  152. .btn-group {
  153. display: flex;
  154. gap: 8px;
  155. }
  156. /* Buttons */
  157. button, .btn {
  158. background: #1a1a1a;
  159. color: white;
  160. border: none;
  161. padding: 8px 16px;
  162. border-radius: 6px;
  163. cursor: pointer;
  164. font-size: 14px;
  165. font-weight: 500;
  166. }
  167. button:hover { background: #333; }
  168. button.sm {
  169. padding: 4px 12px;
  170. font-size: 13px;
  171. margin-left: 4px;
  172. }
  173. button.secondary {
  174. background: white;
  175. color: #1a1a1a;
  176. border: 1px solid #e0e0e0;
  177. }
  178. button.secondary:hover {
  179. background: #f5f5f5;
  180. }
  181. button.danger {
  182. background: white;
  183. color: #dc2626;
  184. border: 1px solid #fecaca;
  185. }
  186. button.danger:hover {
  187. background: #fef2f2;
  188. }
  189. button.sm {
  190. padding: 6px 12px;
  191. font-size: 13px;
  192. }
  193. /* Tabs */
  194. .tabs {
  195. display: flex;
  196. gap: 4px;
  197. border-bottom: 1px solid #e0e0e0;
  198. margin-bottom: 20px;
  199. }
  200. .tab {
  201. padding: 10px 20px;
  202. background: transparent;
  203. color: #666;
  204. border: none;
  205. border-bottom: 2px solid transparent;
  206. cursor: pointer;
  207. font-weight: 500;
  208. font-size: 14px;
  209. border-radius: 0;
  210. margin-bottom: -1px;
  211. }
  212. .tab:hover {
  213. color: #1a1a1a;
  214. background: transparent;
  215. }
  216. .tab.active {
  217. color: #1a1a1a;
  218. border-bottom-color: #1a1a1a;
  219. background: transparent;
  220. }
  221. /* Content */
  222. .content {
  223. display: none;
  224. }
  225. .content.active { display: block; }
  226. /* Tables */
  227. .table-container {
  228. background: white;
  229. border: 1px solid #e0e0e0;
  230. border-radius: 8px;
  231. overflow: hidden;
  232. margin-bottom: 16px;
  233. }
  234. table {
  235. width: 100%;
  236. border-collapse: collapse;
  237. }
  238. th, td {
  239. padding: 12px 16px;
  240. text-align: left;
  241. border-bottom: 1px solid #e0e0e0;
  242. }
  243. th {
  244. background: #f8f9fa;
  245. font-weight: 600;
  246. font-size: 13px;
  247. color: #666;
  248. }
  249. tr:last-child td { border-bottom: none; }
  250. tr:hover td { background: #fafafa; }
  251. td { font-size: 14px; }
  252. /* Forms */
  253. .form-group {
  254. margin-bottom: 16px;
  255. }
  256. label {
  257. display: block;
  258. margin-bottom: 6px;
  259. font-weight: 500;
  260. font-size: 14px;
  261. }
  262. input[type="text"], input[type="datetime-local"], textarea, select {
  263. width: 100%;
  264. padding: 10px 12px;
  265. border: 1px solid #e0e0e0;
  266. border-radius: 6px;
  267. font-size: 14px;
  268. font-family: inherit;
  269. }
  270. input:focus, textarea:focus, select:focus {
  271. outline: none;
  272. border-color: #1a1a1a;
  273. }
  274. textarea { min-height: 100px; resize: vertical; }
  275. .help-text { color: #666; font-size: 13px; margin-top: 4px; }
  276. /* Status badges */
  277. .badge {
  278. display: inline-flex;
  279. align-items: center;
  280. padding: 4px 10px;
  281. border-radius: 4px;
  282. font-size: 12px;
  283. font-weight: 500;
  284. }
  285. .badge.success {
  286. background: #dcfce7;
  287. color: #166534;
  288. }
  289. .badge.failed {
  290. background: #fef2f2;
  291. color: #dc2626;
  292. }
  293. .hidden { display: none; }
  294. .error { color: #dc2626; margin-top: 8px; font-size: 14px; }
  295. .empty {
  296. text-align: center;
  297. padding: 40px;
  298. color: #666;
  299. }
  300. /* Status indicator */
  301. .status-msg {
  302. display: flex;
  303. align-items: center;
  304. gap: 8px;
  305. padding: 8px 0;
  306. font-size: 14px;
  307. }
  308. .status-msg.success { color: #166534; }
  309. .status-msg.error { color: #dc2626; }
  310. /* Loading spinner */
  311. .loading {
  312. text-align: center;
  313. padding: 40px;
  314. color: #666;
  315. }
  316. .spinner {
  317. display: inline-block;
  318. width: 32px;
  319. height: 32px;
  320. border: 3px solid #e0e0e0;
  321. border-top-color: #1a1a1a;
  322. border-radius: 50%;
  323. animation: spin 0.8s linear infinite;
  324. margin-bottom: 8px;
  325. }
  326. @keyframes spin {
  327. to { transform: rotate(360deg); }
  328. }
  329. .mono {
  330. font-family: 'SF Mono', Consolas, monospace;
  331. font-size: 13px;
  332. }
  333. @media (max-width: 768px) {
  334. body { padding: 16px; }
  335. th, td { padding: 10px 12px; }
  336. .header { flex-direction: column; align-items: flex-start; }
  337. }
  338. </style>
  339. </head>
  340. <body>
  341. <div class="container">
  342. <div class="header">
  343. <h1>Letta Scheduling Dashboard</h1>
  344. <div class="header-links">
  345. <a href="/">Home</a>
  346. <a href="https://docs.letta.com/guides/agents/scheduling/" target="_blank">Letta Docs</a>
  347. <a href="https://github.com/cpfiffer/letta-switchboard">GitHub</a>
  348. <button id="logout-btn" onclick="clearApiKey()" class="secondary" style="display: none;">Logout</button>
  349. </div>
  350. </div>
  351. <!-- Migration Notice -->
  352. <div id="migration-banner" class="migration-banner">
  353. <h3>✨ Now powered by Letta's native scheduling!</h3>
  354. <p>
  355. This dashboard now calls Letta's API directly. The old Switchboard backend has been deprecated.
  356. Read the <a href="https://docs.letta.com/guides/agents/scheduling/" target="_blank">full scheduling docs →</a>
  357. </p>
  358. </div>
  359. <!-- API Key Input -->
  360. <div id="api-key-section" class="card">
  361. <div class="card-title">Authentication</div>
  362. <input type="password" id="api-key-input" placeholder="Enter your Letta API key (sk-let-...)">
  363. <div class="btn-group">
  364. <button onclick="saveApiKey()">Connect</button>
  365. <button onclick="clearApiKey()" class="secondary">Disconnect</button>
  366. </div>
  367. <div id="api-key-status"></div>
  368. </div>
  369. <div id="main-content" class="hidden">
  370. <!-- Tabs -->
  371. <div class="tabs">
  372. <button class="tab active" onclick="showTab('schedules')">Schedules</button>
  373. <button class="tab" onclick="showTab('create')">Create New</button>
  374. <button class="tab" onclick="showTab('results')">Results</button>
  375. </div>
  376. <!-- Schedules Tab -->
  377. <div id="schedules-tab" class="content active">
  378. <div class="card" style="margin-bottom: 20px;">
  379. <div class="form-group" style="margin-bottom: 0;">
  380. <label for="schedules-agent-id">Agent ID</label>
  381. <div style="display: flex; gap: 8px;">
  382. <input type="text" id="schedules-agent-id" placeholder="agent-xxx" style="flex: 1;">
  383. <button onclick="loadSchedulesFromInput()" class="secondary">Load Schedules</button>
  384. </div>
  385. </div>
  386. </div>
  387. <h2>One-Time Schedules</h2>
  388. <div class="table-container">
  389. <table id="onetime-table">
  390. <thead>
  391. <tr>
  392. <th>ID</th>
  393. <th>Agent ID</th>
  394. <th>Execute At</th>
  395. <th>Message</th>
  396. <th></th>
  397. </tr>
  398. </thead>
  399. <tbody></tbody>
  400. </table>
  401. </div>
  402. <h2>Recurring Schedules</h2>
  403. <div class="table-container">
  404. <table id="recurring-table">
  405. <thead>
  406. <tr>
  407. <th>ID</th>
  408. <th>Agent ID</th>
  409. <th>Cron</th>
  410. <th>Timezone</th>
  411. <th>Message</th>
  412. <th>Last Run</th>
  413. <th></th>
  414. </tr>
  415. </thead>
  416. <tbody></tbody>
  417. </table>
  418. </div>
  419. </div>
  420. <!-- Create Tab -->
  421. <div id="create-tab" class="content">
  422. <div class="card">
  423. <h2 style="margin-top: 0;">Create Schedule</h2>
  424. <div class="form-group">
  425. <label>Schedule Type</label>
  426. <select id="schedule-type" onchange="toggleScheduleType()">
  427. <option value="onetime">One-Time</option>
  428. <option value="recurring">Recurring</option>
  429. </select>
  430. </div>
  431. <div class="form-group">
  432. <label for="agent-id">Agent ID</label>
  433. <input type="text" id="agent-id" placeholder="agent-xxx">
  434. </div>
  435. <div class="form-group">
  436. <label for="message">Message</label>
  437. <textarea id="message" placeholder="Your message to the agent"></textarea>
  438. </div>
  439. <div id="onetime-fields">
  440. <div class="form-group">
  441. <label for="execute-at">Execute At</label>
  442. <input type="datetime-local" id="execute-at">
  443. </div>
  444. <div class="form-group">
  445. <label for="timezone">Timezone</label>
  446. <select id="timezone">
  447. <option value="local" selected>Local Browser Time</option>
  448. <option value="UTC">UTC</option>
  449. <option value="America/New_York">Eastern (US)</option>
  450. <option value="America/Chicago">Central (US)</option>
  451. <option value="America/Denver">Mountain (US)</option>
  452. <option value="America/Los_Angeles">Pacific (US)</option>
  453. <option value="Europe/London">London</option>
  454. <option value="Europe/Paris">Paris</option>
  455. <option value="Asia/Tokyo">Tokyo</option>
  456. </select>
  457. <div class="help-text">Time will be converted to UTC for storage</div>
  458. </div>
  459. </div>
  460. <div id="recurring-fields" class="hidden">
  461. <div class="form-group">
  462. <label for="cron">Cron Expression</label>
  463. <input type="text" id="cron" placeholder="0 9 * * 1-5">
  464. <div class="help-text">Examples: "0 9 * * *" (daily at 9am), "*/5 * * * *" (every 5 min)</div>
  465. </div>
  466. <div class="form-group">
  467. <label for="recurring-timezone">Timezone</label>
  468. <select id="recurring-timezone">
  469. <option value="UTC" selected>UTC</option>
  470. <option value="America/New_York">Eastern (US)</option>
  471. <option value="America/Chicago">Central (US)</option>
  472. <option value="America/Denver">Mountain (US)</option>
  473. <option value="America/Los_Angeles">Pacific (US)</option>
  474. <option value="Europe/London">London</option>
  475. <option value="Europe/Paris">Paris</option>
  476. <option value="Asia/Tokyo">Tokyo</option>
  477. </select>
  478. <div class="help-text">Cron expression will be evaluated in this timezone</div>
  479. </div>
  480. </div>
  481. <button onclick="createSchedule()">Create Schedule</button>
  482. <div id="create-error" class="error"></div>
  483. </div>
  484. </div>
  485. <!-- Results Tab -->
  486. <div id="results-tab" class="content">
  487. <h2>Execution Results</h2>
  488. <div class="table-container">
  489. <table id="results-table">
  490. <thead>
  491. <tr>
  492. <th>Schedule ID</th>
  493. <th>Type</th>
  494. <th>Status</th>
  495. <th>Agent ID</th>
  496. <th>Message</th>
  497. <th>Run ID / Error</th>
  498. <th>Executed At</th>
  499. </tr>
  500. </thead>
  501. <tbody></tbody>
  502. </table>
  503. </div>
  504. </div>
  505. </div>
  506. </div>
  507. <!-- Modal for viewing schedule details -->
  508. <div id="schedule-modal" class="modal" onclick="closeModal(event)">
  509. <div class="modal-content" onclick="event.stopPropagation()">
  510. <div class="modal-header">
  511. <h3>Schedule Details</h3>
  512. <span class="modal-close" onclick="closeModal()">&times;</span>
  513. </div>
  514. <div class="modal-body">
  515. <div class="modal-label">Schedule ID</div>
  516. <div id="modal-id" class="mono"></div>
  517. <div class="modal-label">Agent ID</div>
  518. <div id="modal-agent-id" class="mono"></div>
  519. <div class="modal-label">Type</div>
  520. <div id="modal-type"></div>
  521. <div class="modal-label">Schedule</div>
  522. <div id="modal-schedule"></div>
  523. <div class="modal-label">Messages</div>
  524. <div id="modal-messages"></div>
  525. </div>
  526. </div>
  527. </div>
  528. <script>
  529. const LETTA_API = 'https://api.letta.com/v1';
  530. let API_KEY = sessionStorage.getItem('letta_api_key') || '';
  531. let DEFAULT_AGENT_ID = sessionStorage.getItem('default_agent_id') || '';
  532. let CURRENT_SCHEDULES = []; // Store schedules for modal viewing
  533. // Initialize
  534. if (API_KEY) {
  535. document.getElementById('api-key-input').value = API_KEY;
  536. showAuthenticatedState();
  537. if (DEFAULT_AGENT_ID) {
  538. document.getElementById('agent-id').value = DEFAULT_AGENT_ID;
  539. document.getElementById('schedules-agent-id').value = DEFAULT_AGENT_ID;
  540. loadSchedules();
  541. }
  542. }
  543. function showAuthenticatedState() {
  544. document.getElementById('main-content').classList.remove('hidden');
  545. document.getElementById('api-key-section').style.display = 'none';
  546. document.getElementById('migration-banner').style.display = 'none';
  547. document.getElementById('logout-btn').style.display = 'inline-block';
  548. }
  549. function showUnauthenticatedState() {
  550. document.getElementById('main-content').classList.add('hidden');
  551. document.getElementById('api-key-section').style.display = 'block';
  552. document.getElementById('migration-banner').style.display = 'block';
  553. document.getElementById('logout-btn').style.display = 'none';
  554. }
  555. function loadSchedulesFromInput() {
  556. const agentId = document.getElementById('schedules-agent-id').value.trim();
  557. if (agentId) {
  558. // Sync to create tab
  559. document.getElementById('agent-id').value = agentId;
  560. DEFAULT_AGENT_ID = agentId;
  561. sessionStorage.setItem('default_agent_id', agentId);
  562. loadSchedules();
  563. }
  564. }
  565. // Helper: Convert ISO timestamp to Unix milliseconds
  566. function toUnixMs(isoString) {
  567. return new Date(isoString).getTime();
  568. }
  569. // Helper: Convert Unix milliseconds to readable date
  570. function fromUnixMs(ms) {
  571. return new Date(ms).toLocaleString();
  572. }
  573. function saveApiKey() {
  574. API_KEY = document.getElementById('api-key-input').value.trim();
  575. if (!API_KEY) {
  576. showStatus('api-key-status', 'API key is required', 'error');
  577. return;
  578. }
  579. sessionStorage.setItem('letta_api_key', API_KEY);
  580. showStatus('api-key-status', 'Connected successfully', 'success');
  581. setTimeout(() => {
  582. showAuthenticatedState();
  583. // Pre-fill agent ID if saved
  584. if (DEFAULT_AGENT_ID) {
  585. document.getElementById('schedules-agent-id').value = DEFAULT_AGENT_ID;
  586. document.getElementById('agent-id').value = DEFAULT_AGENT_ID;
  587. loadSchedules();
  588. }
  589. }, 500);
  590. }
  591. function clearApiKey() {
  592. sessionStorage.removeItem('letta_api_key');
  593. sessionStorage.removeItem('default_agent_id');
  594. API_KEY = '';
  595. DEFAULT_AGENT_ID = '';
  596. document.getElementById('api-key-input').value = '';
  597. document.getElementById('schedules-agent-id').value = '';
  598. document.getElementById('agent-id').value = '';
  599. showUnauthenticatedState();
  600. }
  601. function showStatus(elementId, message, type) {
  602. const el = document.getElementById(elementId);
  603. el.innerHTML = `<div class="status-msg ${type}">${message}</div>`;
  604. }
  605. function showTab(tab) {
  606. document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
  607. document.querySelectorAll('.content').forEach(c => c.classList.remove('active'));
  608. event.target.classList.add('active');
  609. document.getElementById(`${tab}-tab`).classList.add('active');
  610. }
  611. function toggleScheduleType() {
  612. const type = document.getElementById('schedule-type').value;
  613. document.getElementById('onetime-fields').classList.toggle('hidden', type !== 'onetime');
  614. document.getElementById('recurring-fields').classList.toggle('hidden', type !== 'recurring');
  615. }
  616. async function loadSchedules() {
  617. // Try schedules tab input first, then create tab input
  618. let agentId = document.getElementById('schedules-agent-id').value.trim();
  619. if (!agentId) {
  620. agentId = document.getElementById('agent-id').value.trim();
  621. }
  622. if (!agentId) {
  623. const onetimeBody = document.querySelector('#onetime-table tbody');
  624. const recurringBody = document.querySelector('#recurring-table tbody');
  625. onetimeBody.innerHTML = '<tr><td colspan="5" class="empty">Enter an Agent ID above and click Load Schedules</td></tr>';
  626. recurringBody.innerHTML = '<tr><td colspan="7" class="empty">Enter an Agent ID above and click Load Schedules</td></tr>';
  627. return;
  628. }
  629. // Sync both inputs
  630. document.getElementById('schedules-agent-id').value = agentId;
  631. document.getElementById('agent-id').value = agentId;
  632. // Save for next time
  633. sessionStorage.setItem('default_agent_id', agentId);
  634. DEFAULT_AGENT_ID = agentId;
  635. const onetimeBody = document.querySelector('#onetime-table tbody');
  636. const recurringBody = document.querySelector('#recurring-table tbody');
  637. onetimeBody.innerHTML = '<tr><td colspan="5" class="loading"><div class="spinner"></div><p>Loading...</p></td></tr>';
  638. recurringBody.innerHTML = '<tr><td colspan="7" class="loading"><div class="spinner"></div><p>Loading...</p></td></tr>';
  639. try {
  640. // Letta API returns all schedules (one-time and recurring) in a single call
  641. const response = await fetch(`${LETTA_API}/agents/${agentId}/schedule`, {
  642. headers: { 'Authorization': `Bearer ${API_KEY}` }
  643. });
  644. if (!response.ok) {
  645. throw new Error(`Failed to load schedules: ${response.statusText}`);
  646. }
  647. const data = await response.json();
  648. console.log('Letta API response:', data); // Debug log
  649. const schedules = data.scheduled_messages || data.schedules || data || [];
  650. // Store for modal viewing
  651. CURRENT_SCHEDULES = schedules;
  652. // Debug: Log first schedule if available
  653. if (schedules.length > 0) {
  654. console.log('First schedule:', schedules[0]);
  655. console.log('First schedule messages:', schedules[0].messages);
  656. console.log('Full schedule object:', JSON.stringify(schedules[0], null, 2));
  657. }
  658. // Split into one-time and recurring
  659. const onetime = schedules.filter(s => s.schedule?.type === 'one-time');
  660. const recurring = schedules.filter(s => s.schedule?.type === 'recurring');
  661. onetimeBody.innerHTML = onetime.length ? onetime.map((s, idx) => {
  662. const globalIdx = CURRENT_SCHEDULES.indexOf(s);
  663. return `
  664. <tr>
  665. <td class="mono">${s.id?.substring(0, 8) || 'N/A'}...</td>
  666. <td class="mono">${s.agent_id || 'N/A'}</td>
  667. <td>${s.next_scheduled_at ? fromUnixMs(s.next_scheduled_at) : 'N/A'}</td>
  668. <td>${truncate(getMessageContent(s), 30)}</td>
  669. <td>
  670. <button class="secondary sm" onclick="viewSchedule(${globalIdx})">View</button>
  671. <button class="danger sm" onclick="deleteSchedule('${s.id}')">Delete</button>
  672. </td>
  673. </tr>
  674. `;
  675. }).join('') : '<tr><td colspan="5" class="empty">No one-time schedules</td></tr>';
  676. recurringBody.innerHTML = recurring.length ? recurring.map((s, idx) => {
  677. const globalIdx = CURRENT_SCHEDULES.indexOf(s);
  678. return `
  679. <tr>
  680. <td class="mono">${s.id?.substring(0, 8) || 'N/A'}...</td>
  681. <td class="mono">${s.agent_id || 'N/A'}</td>
  682. <td class="mono">${s.schedule?.cron_expression || 'N/A'}</td>
  683. <td>UTC</td>
  684. <td>${truncate(getMessageContent(s), 30)}</td>
  685. <td>${s.next_scheduled_at ? fromUnixMs(s.next_scheduled_at) : 'Never'}</td>
  686. <td>
  687. <button class="secondary sm" onclick="viewSchedule(${globalIdx})">View</button>
  688. <button class="danger sm" onclick="deleteSchedule('${s.id}')">Delete</button>
  689. </td>
  690. </tr>
  691. `;
  692. }).join('') : '<tr><td colspan="7" class="empty">No recurring schedules</td></tr>';
  693. } catch (error) {
  694. onetimeBody.innerHTML = `<tr><td colspan="5" class="empty">Error: ${error.message}</td></tr>`;
  695. recurringBody.innerHTML = `<tr><td colspan="7" class="empty">Error: ${error.message}</td></tr>`;
  696. }
  697. }
  698. async function loadResults() {
  699. const resultsBody = document.querySelector('#results-table tbody');
  700. resultsBody.innerHTML = '<tr><td colspan="7" class="empty">Results tracking not available yet. Check Letta Cloud dashboard for execution history.</td></tr>';
  701. // NOTE: Letta's scheduling API doesn't expose execution history yet
  702. // Users should check the Letta Cloud dashboard or use the runs API
  703. }
  704. async function createSchedule() {
  705. const type = document.getElementById('schedule-type').value;
  706. const agentId = document.getElementById('agent-id').value.trim();
  707. const message = document.getElementById('message').value.trim();
  708. const errorDiv = document.getElementById('create-error');
  709. errorDiv.textContent = '';
  710. if (!agentId || !message) {
  711. errorDiv.textContent = 'Please fill in all required fields';
  712. return;
  713. }
  714. // Save agent ID for convenience
  715. sessionStorage.setItem('default_agent_id', agentId);
  716. DEFAULT_AGENT_ID = agentId;
  717. try {
  718. // Build Letta API format
  719. const payload = {
  720. messages: [{
  721. role: 'user',
  722. content: message
  723. }]
  724. };
  725. if (type === 'onetime') {
  726. const executeAt = document.getElementById('execute-at').value;
  727. const timezone = document.getElementById('timezone').value;
  728. if (!executeAt) {
  729. errorDiv.textContent = 'Please specify execution time';
  730. return;
  731. }
  732. let executeDate;
  733. if (timezone === 'local') {
  734. executeDate = new Date(executeAt);
  735. } else if (timezone === 'UTC') {
  736. executeDate = new Date(executeAt + 'Z');
  737. } else {
  738. executeDate = new Date(executeAt);
  739. }
  740. // Convert to Unix milliseconds for Letta API
  741. payload.schedule = {
  742. type: 'one-time',
  743. scheduled_at: executeDate.getTime()
  744. };
  745. } else {
  746. const cron = document.getElementById('cron').value.trim();
  747. if (!cron) {
  748. errorDiv.textContent = 'Please specify cron expression';
  749. return;
  750. }
  751. // Letta uses 5-field cron (no seconds)
  752. payload.schedule = {
  753. type: 'recurring',
  754. cron_expression: cron
  755. };
  756. }
  757. const response = await fetch(`${LETTA_API}/agents/${agentId}/schedule`, {
  758. method: 'POST',
  759. headers: {
  760. 'Authorization': `Bearer ${API_KEY}`,
  761. 'Content-Type': 'application/json'
  762. },
  763. body: JSON.stringify(payload)
  764. });
  765. if (!response.ok) {
  766. const error = await response.json();
  767. throw new Error(error.detail || error.message || 'Failed to create schedule');
  768. }
  769. // Clear form
  770. document.getElementById('message').value = '';
  771. document.getElementById('execute-at').value = '';
  772. document.getElementById('cron').value = '';
  773. document.getElementById('recurring-timezone').value = 'UTC';
  774. // Switch to schedules tab
  775. showTab('schedules');
  776. document.querySelector('.tab').click();
  777. loadSchedules();
  778. } catch (error) {
  779. errorDiv.textContent = error.message;
  780. }
  781. }
  782. async function deleteSchedule(id) {
  783. if (!confirm('Delete this schedule?')) return;
  784. let agentId = document.getElementById('schedules-agent-id').value.trim();
  785. if (!agentId) {
  786. agentId = document.getElementById('agent-id').value.trim();
  787. }
  788. if (!agentId) {
  789. alert('Agent ID is required');
  790. return;
  791. }
  792. try {
  793. const response = await fetch(`${LETTA_API}/agents/${agentId}/schedule/${id}`, {
  794. method: 'DELETE',
  795. headers: { 'Authorization': `Bearer ${API_KEY}` }
  796. });
  797. if (!response.ok) {
  798. const error = await response.json();
  799. throw new Error(error.detail || error.message || 'Failed to delete');
  800. }
  801. loadSchedules();
  802. } catch (error) {
  803. alert('Error: ' + error.message);
  804. }
  805. }
  806. function truncate(str, len) {
  807. if (!str) return '';
  808. if (typeof str === 'object') str = JSON.stringify(str);
  809. return str.length > len ? str.substring(0, len) + '...' : str;
  810. }
  811. function viewSchedule(scheduleIndex) {
  812. const schedule = CURRENT_SCHEDULES[scheduleIndex];
  813. if (!schedule) {
  814. alert('Schedule not found');
  815. return;
  816. }
  817. console.log('Viewing schedule:', schedule); // Debug
  818. document.getElementById('modal-id').textContent = schedule.id || 'N/A';
  819. document.getElementById('modal-agent-id').textContent = schedule.agent_id || 'N/A';
  820. document.getElementById('modal-type').textContent = schedule.schedule?.type || 'N/A';
  821. // Format schedule details
  822. if (schedule.schedule?.type === 'one-time') {
  823. const time = schedule.next_scheduled_at ? fromUnixMs(schedule.next_scheduled_at) : 'N/A';
  824. document.getElementById('modal-schedule').textContent = time;
  825. } else if (schedule.schedule?.type === 'recurring') {
  826. document.getElementById('modal-schedule').textContent =
  827. `Cron: ${schedule.schedule.cron_expression}\nNext run: ${schedule.next_scheduled_at ? fromUnixMs(schedule.next_scheduled_at) : 'Never'}`;
  828. }
  829. // Format messages - handle different formats
  830. const messagesEl = document.getElementById('modal-messages');
  831. // Check all possible message fields
  832. console.log('Messages field:', schedule.messages);
  833. console.log('Message field:', schedule.message);
  834. console.log('Raw schedule:', JSON.stringify(schedule, null, 2));
  835. if (schedule.messages && Array.isArray(schedule.messages) && schedule.messages.length > 0) {
  836. messagesEl.innerHTML = schedule.messages.map((msg, idx) => {
  837. const content = typeof msg === 'string' ? msg :
  838. (msg.content || msg.text || JSON.stringify(msg));
  839. const role = typeof msg === 'object' ? (msg.role || 'user') : 'user';
  840. return `
  841. <div style="margin-bottom: 12px; padding: 12px; background: #f5f5f5; border-radius: 4px;">
  842. <div style="font-weight: 600; margin-bottom: 4px;">Message ${idx + 1}</div>
  843. <div><strong>Role:</strong> ${role}</div>
  844. <div style="margin-top: 4px;"><strong>Content:</strong></div>
  845. <div style="white-space: pre-wrap;">${typeof content === 'string' ? content : JSON.stringify(content, null, 2)}</div>
  846. </div>
  847. `;
  848. }).join('');
  849. } else if (schedule.message) {
  850. // Single message field
  851. const content = typeof schedule.message === 'string' ? schedule.message : JSON.stringify(schedule.message, null, 2);
  852. messagesEl.innerHTML = `<div style="white-space: pre-wrap;">${content}</div>`;
  853. } else {
  854. // Show raw data for debugging
  855. messagesEl.innerHTML = `<div style="white-space: pre-wrap; font-family: monospace; font-size: 12px;">${JSON.stringify(schedule, null, 2)}</div>`;
  856. }
  857. document.getElementById('schedule-modal').classList.add('active');
  858. }
  859. function closeModal(event) {
  860. if (!event || event.target.id === 'schedule-modal' || event.target.classList.contains('modal-close')) {
  861. document.getElementById('schedule-modal').classList.remove('active');
  862. }
  863. }
  864. function getMessageContent(schedule) {
  865. // Try different possible message formats
  866. if (schedule.messages && Array.isArray(schedule.messages) && schedule.messages.length > 0) {
  867. const msg = schedule.messages[0];
  868. if (typeof msg === 'string') return msg;
  869. if (msg.content) {
  870. if (typeof msg.content === 'string') return msg.content;
  871. if (typeof msg.content === 'object') return JSON.stringify(msg.content);
  872. }
  873. if (msg.text) return msg.text;
  874. return JSON.stringify(msg);
  875. }
  876. if (schedule.message) return schedule.message;
  877. return 'N/A';
  878. }
  879. </script>
  880. </body>
  881. </html>