khoj 1.17.1.dev229__py3-none-any.whl → 1.17.1.dev233__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. khoj/routers/web_client.py +29 -130
  2. {khoj-1.17.1.dev229.dist-info → khoj-1.17.1.dev233.dist-info}/METADATA +1 -1
  3. {khoj-1.17.1.dev229.dist-info → khoj-1.17.1.dev233.dist-info}/RECORD +6 -51
  4. khoj/interface/web/404.html +0 -56
  5. khoj/interface/web/agent.html +0 -312
  6. khoj/interface/web/agents.html +0 -276
  7. khoj/interface/web/assets/icons/cancel.svg +0 -3
  8. khoj/interface/web/assets/icons/collapse.svg +0 -17
  9. khoj/interface/web/assets/icons/computer.png +0 -0
  10. khoj/interface/web/assets/icons/confirm-icon.svg +0 -1
  11. khoj/interface/web/assets/icons/copy-button-success.svg +0 -6
  12. khoj/interface/web/assets/icons/copy-button.svg +0 -5
  13. khoj/interface/web/assets/icons/credit-card.png +0 -0
  14. khoj/interface/web/assets/icons/delete.svg +0 -26
  15. khoj/interface/web/assets/icons/docx.svg +0 -7
  16. khoj/interface/web/assets/icons/edit.svg +0 -4
  17. khoj/interface/web/assets/icons/key.svg +0 -4
  18. khoj/interface/web/assets/icons/markdown.svg +0 -1
  19. khoj/interface/web/assets/icons/new.svg +0 -23
  20. khoj/interface/web/assets/icons/notion.svg +0 -4
  21. khoj/interface/web/assets/icons/openai-logomark.svg +0 -1
  22. khoj/interface/web/assets/icons/org.svg +0 -1
  23. khoj/interface/web/assets/icons/pdf.svg +0 -23
  24. khoj/interface/web/assets/icons/pencil-edit.svg +0 -5
  25. khoj/interface/web/assets/icons/plaintext.svg +0 -1
  26. khoj/interface/web/assets/icons/question-mark-icon.svg +0 -1
  27. khoj/interface/web/assets/icons/send.svg +0 -1
  28. khoj/interface/web/assets/icons/share.svg +0 -8
  29. khoj/interface/web/assets/icons/speaker.svg +0 -4
  30. khoj/interface/web/assets/icons/stop-solid.svg +0 -37
  31. khoj/interface/web/assets/icons/thumbs-down-svgrepo-com.svg +0 -6
  32. khoj/interface/web/assets/icons/thumbs-up-svgrepo-com.svg +0 -6
  33. khoj/interface/web/assets/icons/user-silhouette.svg +0 -4
  34. khoj/interface/web/assets/icons/voice.svg +0 -8
  35. khoj/interface/web/assets/icons/web.svg +0 -2
  36. khoj/interface/web/assets/icons/whatsapp.svg +0 -17
  37. khoj/interface/web/assets/markdown-it.min.js +0 -8476
  38. khoj/interface/web/assets/natural-cron.min.js +0 -1
  39. khoj/interface/web/assets/org.min.js +0 -1823
  40. khoj/interface/web/assets/pico.min.css +0 -5
  41. khoj/interface/web/assets/purify.min.js +0 -3
  42. khoj/interface/web/chat.html +0 -3436
  43. khoj/interface/web/config_automation.html +0 -1103
  44. khoj/interface/web/content_source_computer_input.html +0 -139
  45. khoj/interface/web/content_source_notion_input.html +0 -94
  46. khoj/interface/web/public_conversation.html +0 -2006
  47. khoj/interface/web/search.html +0 -470
  48. khoj/interface/web/settings.html +0 -1011
  49. {khoj-1.17.1.dev229.dist-info → khoj-1.17.1.dev233.dist-info}/WHEEL +0 -0
  50. {khoj-1.17.1.dev229.dist-info → khoj-1.17.1.dev233.dist-info}/entry_points.txt +0 -0
  51. {khoj-1.17.1.dev229.dist-info → khoj-1.17.1.dev233.dist-info}/licenses/LICENSE +0 -0
@@ -1,1103 +0,0 @@
1
- {% extends "base_config.html" %}
2
- {% block content %}
3
- <div class="page">
4
- <div class="section">
5
- <div id="overlay" style="display: none;"></div>
6
- <h2 class="section-title">
7
- <img class="card-icon" src="/static/assets/icons/automation.svg?v={{ khoj_version }}" alt="Automate">
8
- <span class="card-title-text">Automate (Preview)</span>
9
- <div class="instructions">
10
- Automations allow you to schedule smart reminders using Khoj. This is an experimental feature, so your results may vary! Send any feedback to <a class="inline-link-light" href="mailto:team@khoj.dev">team@khoj.dev</a>.
11
- </div>
12
- <div class="instructions notice">
13
- Sending automation results to <a class="inline-link-light" href="mailto:{{ username}}">{{ username }}</a>.
14
- </div>
15
- </h2>
16
- <div class="section-body">
17
- <button id="create-automation-button" type="button" class="positive-button">
18
- <img class="automation-action-icon" src="/static/assets/icons/new.svg" alt="Automations">
19
- <span id="create-automation-button-text">Build Your Own</span>
20
- </button>
21
- <div id="automations" class="section-cards"></div>
22
- <div id="suggested-automations">
23
- <h2 class="section-title">
24
- <span class="card-title-text">Suggested Automations</span>
25
- </h2>
26
- <div id="suggested-automations-list" class="section-cards"></div>
27
- </div>
28
- </div>
29
- </div>
30
- <div id="footer">
31
- <a href="/">Back to Chat</a>
32
- </div>
33
- </div>
34
- <script>
35
- document.getElementById("automations-nav").classList.add("khoj-nav-selected");
36
- </script>
37
-
38
- <script src="/static/assets/natural-cron.min.js"></script>
39
- <style>
40
- td {
41
- padding: 10px 0;
42
- }
43
- #overlay {
44
- position: fixed;
45
- top: 0;
46
- left: 0;
47
- width: 100%;
48
- height: 100%;
49
- background-color: black;
50
- opacity: 0.5;
51
- z-index: 1;
52
- }
53
- div.automation {
54
- width: 100%;
55
- height: 100%;
56
- grid-template-rows: none;
57
- background-color: white;
58
- border-radius: 20px;
59
- padding: 20px;
60
- box-shadow: rgba(3, 3, 3, 0.08) 0px 1px 12px;
61
- transition: box-shadow 0.3s ease, transform 0.3s ease;
62
- }
63
-
64
- div#footer {
65
- width: auto;
66
- padding: 10px;
67
- background-color: var(--background-color);
68
- border-top: 1px solid var(--main-text-color);
69
- text-align: left;
70
- margin-top: 12px;
71
- margin-bottom: 12px;
72
- }
73
-
74
- div#footer a {
75
- font-size: 18px;
76
- font-weight: bold;
77
- color: var(--primary-color);
78
- }
79
-
80
- img.automation-share-icon,
81
- img.automation-edit-cancel-icon,
82
- img.automation-edit-icon {
83
- width: 24px;
84
- height: 24px;
85
- object-fit: cover;
86
- cursor: pointer;
87
- margin: auto;
88
- }
89
-
90
- textarea.fake-input,
91
- input.fake-input {
92
- height: auto;
93
- padding-top: 0px;
94
- padding-bottom: 0px;
95
- padding-left: 0px;
96
- padding-right: 0px;
97
- border: none;
98
- }
99
-
100
- #create-automation-button {
101
- width: auto;
102
- }
103
-
104
- div.notice {
105
- border-top: 1px solid black;
106
- padding-top: 8px;
107
- }
108
-
109
- div#suggested-automations-list,
110
- div#automations {
111
- margin-bottom: 12px;
112
- grid-template-columns: 1fr 1fr;
113
- }
114
-
115
- button.negative-button {
116
- background-color: gainsboro;
117
- }
118
- .positive-button {
119
- background-color: var(--primary-hover)
120
- }
121
- .positive-button:hover {
122
- background-color: var(--summer-sun);
123
- }
124
-
125
- div.automation-buttons {
126
- display: grid;
127
- grid-gap: 8px;
128
- grid-template-columns: 1fr 1fr 1fr;
129
- }
130
-
131
- button.save-automation-button {
132
- background-color: var(--summer-sun);
133
- }
134
-
135
- button.save-automation-button,
136
- button.cancel-edit-automation-button,
137
- button.send-preview-automation-button,
138
- button.delete-automation-button {
139
- padding: 8px;
140
- margin-bottom: 0;
141
- }
142
-
143
- button.send-preview-automation-button {
144
- border-color: var(--summer-sun);
145
- }
146
-
147
- button.save-automation-button:hover {
148
- background-color: var(--primary-hover);
149
- }
150
-
151
- div.new-automation {
152
- border-radius: 20px;
153
- box-shadow: 0 4px 6px 0 hsla(0, 0%, 0%, 0.2);
154
- margin-bottom: 20px;
155
- transition: box-shadow 0.3s ease, transform 0.3s ease;
156
- position: absolute;
157
- top: 50%;
158
- left: 50%;
159
- width: auto;
160
- transform: translate(-50%, -50%);
161
- z-index: 2;
162
- height: auto;
163
- }
164
-
165
- div.automation:not(.new-automation):hover {
166
- box-shadow: 0 10px 15px 0 hsla(0, 0%, 0%, 0.1);
167
- transform: translateY(-5px);
168
- }
169
-
170
- .hide-details {
171
- display: none !important;
172
- }
173
-
174
- div.card-header {
175
- display: grid;
176
- grid-template-rows: auto 1fr;
177
- grid-gap: 8px;
178
- align-items: baseline;
179
- }
180
- input.schedule {
181
- font-size: medium;
182
- height: auto;
183
- font-weight: lighter !important;
184
- }
185
-
186
- h2.section-title {
187
- font-size: larger;
188
- }
189
-
190
- div.card-header input {
191
- font-weight: bold;
192
- margin-bottom: 0 !important;
193
- }
194
-
195
- div.automation textarea {
196
- width: 100%;
197
- box-sizing: border-box;
198
- border-radius: 4px;
199
- resize: none;
200
- }
201
-
202
- img.promo-image {
203
- width: 100%;
204
- height: 100px;
205
- object-fit: cover;
206
- border-radius: 4px;
207
- }
208
-
209
-
210
- div.card-header textarea,
211
- div.card-header input,
212
- div.card-header:hover {
213
- cursor: pointer;
214
- }
215
-
216
- div.toggle-icon {
217
- width: 24px;
218
- height: 24px;
219
- }
220
-
221
- div.subject-wrapper {
222
- display: grid;
223
- grid-template-columns: 1fr auto auto;
224
- grid-gap: 8px;
225
- }
226
-
227
- div.subject-wrapper p {
228
- margin: 0;
229
- }
230
-
231
- @keyframes confirmation {
232
- 0% { background-color: normal; transform: scale(1); }
233
- 50% { background-color: var(--primary); transform: scale(1.1); }
234
- 100% { background-color: normal; transform: scale(1); }
235
- }
236
-
237
- .confirmation {
238
- animation: confirmation 1s;
239
- }
240
-
241
- @media screen and (max-width: 600px) {
242
- div#automations,
243
- div#suggested-automations-list {
244
- grid-template-columns: 1fr;
245
- }
246
- div.automation-buttons {
247
- grid-template-columns: 1fr;
248
- }
249
- div.new-automation {
250
- width: 100%;
251
- height: auto;
252
- }
253
- }
254
-
255
- </style>
256
- <script>
257
- function deleteAutomation(automationId) {
258
- const AutomationList = document.getElementById("automations");
259
- if ("{{ username }}" === "None") {
260
- const AutomationItem = document.getElementById(`automation-card-${automationId}`);
261
- AutomationList.removeChild(AutomationItem);
262
- // Remove the Automation from the DOM and return
263
- AutomationItem.remove();
264
- document.getElementById('overlay').style.display = 'none';
265
- return;
266
- }
267
- fetch(`/api/automation?automation_id=${automationId}`, {
268
- method: 'DELETE',
269
- })
270
- .then(response => {
271
- if (response.status == 200 || response.status == 204) {
272
- const AutomationItem = document.getElementById(`automation-card-${automationId}`);
273
- AutomationList.removeChild(AutomationItem);
274
- document.getElementById('overlay').style.display = 'none';
275
- }
276
- });
277
- }
278
-
279
- function updateAutomationRow(automation) {
280
- let automationId = automation.id;
281
- let automationSubject = DOMPurify.sanitize(automation.subject);
282
- let automationSchedule = DOMPurify.sanitize(automation.schedule);
283
- let automationQueryToRun = DOMPurify.sanitize(automation.query_to_run);
284
- let automationCrontime = DOMPurify.sanitize(automation.crontime);
285
- let automationNextRun = `Next run at ${automation.next}\nCron: ${automationCrontime}`;
286
-
287
- let scheduleEl = document.getElementById(`automation-schedule-${automationId}`);
288
- scheduleEl.setAttribute('data-original', automationSchedule);
289
- scheduleEl.setAttribute('data-cron', automationCrontime);
290
- scheduleEl.setAttribute('title', automationNextRun);
291
- scheduleEl.value = automationSchedule;
292
-
293
- let subjectEl = document.getElementById(`automation-subject-${automationId}`);
294
- subjectEl.setAttribute('data-original', automationSubject);
295
- subjectEl.value = automationSubject;
296
-
297
- let queryEl = document.getElementById(`automation-queryToRun-${automationId}`);
298
- queryEl.setAttribute('data-original', automationQueryToRun);
299
- queryEl.value = automationQueryToRun;
300
- }
301
-
302
- function onClickEditAutomationCard(automationId) {
303
- const automationIDElements = document.querySelectorAll(`.${automationId}`);
304
- automationIDElements.forEach(el => {
305
- if (el.classList.contains("automation-edit-icon")) {
306
- el.classList.remove("automation-edit-icon");
307
- el.classList.add("automation-edit-cancel-icon");
308
- el.src = "/static/assets/icons/cancel.svg";
309
- el.onclick = function(event) { clickCancelEdit(event, automationId); };
310
- }
311
-
312
- if (el.classList.contains("hide-details")) {
313
- el.classList.add("hide-details-placeholder");
314
- el.classList.remove("hide-details");
315
- }
316
- if (el.classList.contains("fake-input")) {
317
- el.classList.add("fake-input-placeholder");
318
- el.classList.remove("fake-input");
319
- }
320
- });
321
- }
322
-
323
- function sendAPreviewAutomation(automationId) {
324
- const notificationEl = document.getElementById(`automation-success-${automationId}`);
325
-
326
- fetch(`/api/trigger/automation?automation_id=${automationId}`, { method: 'POST' })
327
- .then(response =>
328
- {
329
- if (!response.ok) {
330
- throw new Error('Network response was not ok');
331
- }
332
- return response;
333
- })
334
- .then(automations => {
335
- notificationEl.style.display = 'block';
336
- notificationEl.textContent = "Automation triggered. Check your inbox in a few minutes!";
337
- })
338
- .catch(error => {
339
- notificationEl.style.display = 'block';
340
- notificationEl.textContent = "Sorry, something went wrong. Try again later."
341
- })
342
-
343
- }
344
-
345
- function clickCancelEdit(event, automationId) {
346
- event.preventDefault();
347
- event.stopPropagation();
348
- const automationIDElements = document.querySelectorAll(`.${automationId}`);
349
- automationIDElements.forEach(el => {
350
- if (el.classList.contains("automation-edit-cancel-icon")) {
351
- el.classList.remove("automation-edit-cancel-icon");
352
- el.classList.add("automation-edit-icon");
353
- el.src = "/static/assets/icons/pencil-edit.svg";
354
- el.onclick = function() { onClickEditAutomationCard(automationId); };
355
- }
356
-
357
- if (el.classList.contains("hide-details-placeholder")) {
358
- el.classList.remove("hide-details-placeholder");
359
- el.classList.add("hide-details");
360
- }
361
- if (el.classList.contains("fake-input-placeholder")) {
362
- el.classList.remove("fake-input-placeholder");
363
- el.classList.add("fake-input");
364
- }
365
- })
366
- }
367
-
368
- function copyShareLink(event, automationId, subject, crontime, queryToRun) {
369
- event.preventDefault();
370
- event.stopPropagation();
371
-
372
- const encodedSubject = encodeURIComponent(subject);
373
- const encodedCrontime = encodeURIComponent(crontime);
374
- const encodedQueryToRun = encodeURIComponent(queryToRun);
375
-
376
- const shareLink = `${window.location.origin}/automations?subject=${encodedSubject}&crontime=${encodedCrontime}&queryToRun=${encodedQueryToRun}`;
377
- const button = document.getElementById(`share-link-${automationId}`);
378
-
379
- navigator.clipboard.writeText(shareLink).then(() => {
380
- button.src = "/static/assets/icons/copy-button-success.svg";
381
- setTimeout(() => {
382
- button.src = "/static/assets/icons/share.svg";
383
- }, 1000);
384
- }).catch((error) => {
385
- console.error("Error copying share link output to clipboard:", error);
386
- setTimeout(() => {
387
- button.src = "/static/assets/icons/share.svg";
388
- }, 1000);
389
- });
390
- }
391
-
392
- function generateAutomationRow(automation, isSuggested=false) {
393
- let automationId = automation.id;
394
- let automationSubject = DOMPurify.sanitize(automation.subject);
395
- let automationSchedule = DOMPurify.sanitize(automation.schedule);
396
- let automationQueryToRun = DOMPurify.sanitize(automation.query_to_run);
397
- let automationCrontime = DOMPurify.sanitize(automation.crontime);
398
- let automationNextRun = `Next run at ${automation.next}\nCron: ${automationCrontime}`;
399
-
400
- // Create card top elements
401
- let automationEl = document.createElement("div");
402
- let automationCardEl = document.createElement("div");
403
- automationCardEl.id = `automation-card-${automationId}`;
404
- automationCardEl.classList.add("card", "automation");
405
-
406
- // Create card header elements
407
- let automationCardFormEl = document.createElement("div");
408
- automationCardFormEl.className = "card-header";
409
-
410
- let automationButtonsWrapperEl = document.createElement("div");
411
- automationButtonsWrapperEl.id = "automation-buttons-wrapper";
412
-
413
- let automationSuccessEl = document.createElement("div");
414
- automationSuccessEl.id = `automation-success-${automationId}`;
415
- automationSuccessEl.style.display = "none";
416
-
417
- // Create automation card form section
418
- automationCardFormEl.onclick = function() { onClickEditAutomationCard(automationId); };
419
-
420
- // automation subject input
421
- let subjectWrapperEl = document.createElement("div");
422
- subjectWrapperEl.className = "subject-wrapper";
423
- let subjectEl = document.createElement("input");
424
- subjectEl.type = "text";
425
- subjectEl.id = `automation-subject-${automationId}`;
426
- subjectEl.classList.add(automationId, "fake-input");
427
- subjectEl.name = "subject";
428
- subjectEl.setAttribute("data-original", automationSubject);
429
- subjectEl.value = automationSubject;
430
-
431
- // automation share link
432
- let shareLinkEl = document.createElement("img");
433
- shareLinkEl.id = `share-link-${automationId}`,
434
- shareLinkEl.className = "automation-share-icon";
435
- shareLinkEl.src = "/static/assets/icons/share.svg";
436
- shareLinkEl.alt = "Share";
437
- shareLinkEl.onclick = function(event) { copyShareLink(event, automationId, automationSubject, automationCrontime, automationQueryToRun); };
438
-
439
- // automation edit action
440
- let editIconEl = document.createElement("img");
441
- editIconEl.classList.add("automation-edit-icon", automationId);
442
- editIconEl.src = "/static/assets/icons/pencil-edit.svg";
443
- editIconEl.alt = "Automations";
444
- editIconEl.onclick = function() { onClickEditAutomationCard(automationId); };
445
-
446
- // automation schedule input
447
- let scheduleEl = document.createElement("input");
448
- scheduleEl.type = "text";
449
- scheduleEl.id = `automation-schedule-${automationId}`;
450
- scheduleEl.name = "schedule";
451
- scheduleEl.classList.add("schedule", automationId, "fake-input");
452
- scheduleEl.setAttribute("data-cron", automationCrontime);
453
- scheduleEl.setAttribute("data-original", automationSchedule);
454
- scheduleEl.title = automationNextRun;
455
- scheduleEl.value = automationSchedule;
456
-
457
- // automation query to run input
458
- let queryToRunEl = document.createElement("textarea");
459
- queryToRunEl.id = `automation-queryToRun-${automationId}`;
460
- queryToRunEl.classList.add("automation-instructions", automationId, "fake-input");
461
- queryToRunEl.setAttribute("data-original", automationQueryToRun);
462
- queryToRunEl.name = "query-to-run";
463
- queryToRunEl.textContent = automationQueryToRun;
464
-
465
- // Create automation actions section
466
- let automationButtonsEl = document.createElement("div");
467
- automationButtonsEl.className = "automation-buttons";
468
- if (!isSuggested) {
469
- automationButtonsEl.classList.add("hide-details", automationId);
470
- }
471
-
472
- // save automation button
473
- let saveAutomationButtonEl = document.createElement("button");
474
- saveAutomationButtonEl.type = "button";
475
- saveAutomationButtonEl.className = "save-automation-button positive-button";
476
- saveAutomationButtonEl.id = `save-automation-button-${automationId}`;
477
- saveAutomationButtonEl.textContent = isSuggested ? "Add" : "Save";
478
- saveAutomationButtonEl.onclick = async () => { await saveAutomation(automation.id, isSuggested); };
479
-
480
- // promo image for suggested automations
481
- let promoImageEl = isSuggested ? document.createElement("img") : null;
482
- if (isSuggested) {
483
- promoImageEl.className = "promo-image";
484
- promoImageEl.src = automation.promoImage;
485
- promoImageEl.alt = "Promo Image";
486
- }
487
-
488
- // delete automation button
489
- let emptyDivEl = document.createElement("div");
490
- emptyDivEl.className = "empty-div";
491
- let deleteAutomationButtonEl = !isSuggested ? document.createElement("button") : emptyDivEl;
492
- if (!isSuggested) {
493
- deleteAutomationButtonEl.type = "button";
494
- deleteAutomationButtonEl.className = "delete-automation-button negative-button";
495
- deleteAutomationButtonEl.id = `delete-automation-button-${automationId}`;
496
- deleteAutomationButtonEl.textContent = "Delete";
497
- deleteAutomationButtonEl.onclick = function() { deleteAutomation(automationId); document.getElementById('overlay').style.display = 'none'; };
498
- }
499
-
500
- // send preview automation button
501
- emptyDivEl = document.createElement("div");
502
- emptyDivEl.className = "empty-div";
503
- let sendPreviewAutomationButtonEl = !isSuggested ? document.createElement("button") : emptyDivEl;
504
- if (!isSuggested) {
505
- sendPreviewAutomationButtonEl.type = "button";
506
- sendPreviewAutomationButtonEl.className = "send-preview-automation-button positive-button";
507
- sendPreviewAutomationButtonEl.title = "Immediately get a preview of this automation";
508
- sendPreviewAutomationButtonEl.textContent = "Preview";
509
- sendPreviewAutomationButtonEl.onclick = function() { sendAPreviewAutomation(automationId); };
510
- }
511
-
512
- // Construct automation card from elements
513
- subjectWrapperEl.append(subjectEl, shareLinkEl, editIconEl);
514
- automationButtonsEl.append(deleteAutomationButtonEl, sendPreviewAutomationButtonEl, saveAutomationButtonEl);
515
-
516
- automationCardFormEl.append(subjectWrapperEl, scheduleEl, queryToRunEl);
517
- if (isSuggested) {
518
- automationCardFormEl.append(promoImageEl);
519
- }
520
- automationButtonsWrapperEl.append(automationButtonsEl);
521
-
522
- automationCardEl.append(automationCardFormEl, automationButtonsWrapperEl, automationSuccessEl);
523
- automationEl.append(automationCardEl);
524
-
525
- return automationEl.firstElementChild;
526
- }
527
-
528
- let timestamp = Math.floor(Date.now() / 1000);
529
-
530
- let suggestedAutomationsMetadata = [
531
- {
532
- "subject": "Weekly Newsletter",
533
- "query_to_run": "Compile a message including: 1. A recap of news from last week 2. A reminder to work out and stay hydrated 3. A quote to inspire me for the week ahead",
534
- "schedule": "9AM every Monday",
535
- "next": "Next run at 9AM on Monday",
536
- "crontime": "0 9 * * 1",
537
- "id": "suggested-automation" + timestamp,
538
- "promoImage": "https://assets.khoj.dev/abstract_rectangles.webp",
539
- },
540
- {
541
- "subject": "Daily Weather Update",
542
- "query_to_run": "Get the weather forecast for today",
543
- "schedule": "9AM every morning",
544
- "next": "Next run at 9AM today",
545
- "crontime": "0 9 * * *",
546
- "id": "suggested-automation" + (timestamp + 1),
547
- "promoImage": "https://assets.khoj.dev/blue_waves.webp",
548
- },
549
- {
550
- "subject": "Front Page of Hacker News",
551
- "query_to_run": "Summarize the top 5 posts from https://news.ycombinator.com/best and share them with me, including links",
552
- "schedule": "9PM on every Wednesday",
553
- "next": "Next run at 9PM on Wednesday",
554
- "crontime": "0 21 * * 3",
555
- "id": "suggested-automation" + (timestamp + 2),
556
- "promoImage": "https://assets.khoj.dev/purple_triangles.webp",
557
- },
558
- {
559
- "subject": "Market Summary",
560
- "query_to_run": "Get the market summary for today and share it with me. Focus on tech stocks and the S&P 500.",
561
- "schedule": "9AM on every weekday",
562
- "next": "Next run at 9AM on Monday",
563
- "crontime": "0 9 * * 1-5",
564
- "id": "suggested-automation" + (timestamp + 3),
565
- "promoImage": "https://assets.khoj.dev/blue_gears.webp",
566
- }
567
- ];
568
-
569
- function listAutomations() {
570
- const AutomationsList = document.getElementById("automations");
571
- fetch('/api/automations')
572
- .then(response => response.json())
573
- .then(automations => {
574
- if (!automations?.length > 0) return;
575
- AutomationsList.innerHTML = ''; // Clear existing content
576
- AutomationsList.append(...automations.map(automation => generateAutomationRow(automation)));
577
- // Check if any of the automations 'query-to-run' fields match the suggested automations
578
- automations.forEach(automation => {
579
- suggestedAutomationsMetadata.forEach(suggestedAutomation => {
580
- if (automation.query_to_run === suggestedAutomation.query_to_run) {
581
- let suggestedAutomationEl = document.getElementById(`automation-card-${suggestedAutomation.id}`);
582
- suggestedAutomationEl.remove();
583
- }
584
- });
585
- });
586
- // Check if subject, crontime, query_to_run are all filled out. If so, show it as a populated suggested automation.
587
- const subject = DOMPurify.sanitize("{{ subject }}");
588
- const crontime = DOMPurify.sanitize("{{ crontime }}");
589
- const query = DOMPurify.sanitize("{{ queryToRun }}");
590
-
591
- if (subject && crontime && query) {
592
- const preFilledAutomation = createPreFilledAutomation(subject, crontime, query);
593
- }
594
- });
595
- }
596
-
597
- if ("{{ username }}" !== "None") {
598
- listAutomations();
599
- } else {
600
- // Check if subject, crontime, query_to_run are all filled out. If so, show it as a populated suggested automation.
601
- const subject = DOMPurify.sanitize("{{ subject }}");
602
- const crontime = DOMPurify.sanitize("{{ crontime }}");
603
- const query = DOMPurify.sanitize("{{ queryToRun }}");
604
-
605
- if (subject && crontime && query) {
606
- const preFilledAutomation = createPreFilledAutomation(subject, crontime, query);
607
- }
608
- }
609
-
610
- if (suggestedAutomationsMetadata.length > 0) {
611
- suggestedAutomationsMetadata.forEach(automation => {
612
- automation.id = "suggested-automation" + timestamp;
613
- timestamp++;
614
- });
615
- }
616
-
617
- function listSuggestedAutomations() {
618
- const SuggestedAutomationsList = document.getElementById("suggested-automations-list");
619
- SuggestedAutomationsList.innerHTML = ''; // Clear existing content
620
- SuggestedAutomationsList.append(...suggestedAutomationsMetadata.map(automation => generateAutomationRow(automation, true)));
621
- }
622
- listSuggestedAutomations();
623
-
624
- function enableSaveOnlyWhenInputsChanged() {
625
- const inputs = document.querySelectorAll('input[name="schedule"], textarea[name="query-to-run"], input[name="subject"]');
626
- inputs.forEach(input => {
627
- input.addEventListener('change', function() {
628
- // Get automation id by splitting the id by "-" and taking all elements after the second one
629
- const automationId = this.id.split("-").slice(2).join("-");
630
- let anyChanged = false;
631
- let inputNameStubs = ["subject", "query-to-run", "schedule"]
632
- for (let stub of inputNameStubs) {
633
- let el = document.getElementById(`automation-${stub}-${automationId}`);
634
- let originalValue = el.getAttribute('data-original');
635
- let currentValue = el.value;
636
- if (originalValue !== currentValue) {
637
- anyChanged = true;
638
- break;
639
- }
640
- }
641
- document.getElementById(`save-automation-button-${automationId}`).disabled = !anyChanged;
642
- });
643
- });
644
- }
645
-
646
- function createScheduleSelector(automationId) {
647
- var scheduleContainer = document.createElement('div');
648
- scheduleContainer.id = `schedule-container-${automationId}`;
649
-
650
- var frequencyLabel = document.createElement('label');
651
- frequencyLabel.for = `frequency-selector-${automationId}`;
652
- frequencyLabel.textContent = 'Every';
653
- var frequencySelector = document.createElement('select')
654
- frequencySelector.id = `frequency-selector-${automationId}`;
655
- var dayLabel = document.createElement('label');
656
- dayLabel.id = `day-selector-label-${automationId}`;
657
- dayLabel.for = `day-selector-${automationId}`;
658
- dayLabel.textContent = 'on';
659
- var daySelector = document.createElement('select');
660
- daySelector.id = `day-selector-${automationId}`;
661
- var dateLabel = document.createElement('label');
662
- dateLabel.id = `date-label-${automationId}`;
663
- dateLabel.for = `date-selector-${automationId}`;
664
- dateLabel.textContent = 'on the';
665
- var dateSelector = document.createElement('select');
666
- dateSelector.id = `date-selector-${automationId}`;
667
- var timeLabel = document.createElement('label');
668
- timeLabel.for = `time-selector-${automationId}`;
669
- timeLabel.textContent = 'at';
670
- var timeSelector = document.createElement('select');
671
- timeSelector.id = `time-selector-${automationId}`;
672
-
673
-
674
- // Populate frequency selector with options for day, week, and month
675
- var frequencies = ['day', 'week', 'month'];
676
- for (var i = 0; i < frequencies.length; i++) {
677
- var option = document.createElement('option');
678
- option.value = frequencies[i];
679
- option.text = frequencies[i];
680
- frequencySelector.appendChild(option);
681
- }
682
-
683
- // Event listener for frequency selector change
684
- frequencySelector.addEventListener('change', function() {
685
- switch (this.value) {
686
- case 'day':
687
- daySelector.style.display = 'none';
688
- dateSelector.style.display = 'none';
689
- break;
690
- case 'week':
691
- daySelector.style.display = 'block';
692
- dateSelector.style.display = 'none';
693
- break;
694
- case 'month':
695
- daySelector.style.display = 'none';
696
- dateSelector.style.display = 'block';
697
- break;
698
- }
699
- });
700
-
701
- // Populate the date selector with options for each day of the month
702
- for (var i = 1; i <= 31; i++) {
703
- var option = document.createElement('option');
704
- option.value = i;
705
- option.text = i;
706
- dateSelector.appendChild(option);
707
- }
708
-
709
- // Populate the day selector with options for each day of the week
710
- var days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
711
- for (var i = 0; i < days.length; i++) {
712
- var option = document.createElement('option');
713
- option.value = i;
714
- option.text = days[i];
715
- daySelector.appendChild(option);
716
- }
717
-
718
- var timePeriods = ['AM', 'PM'];
719
- // Populate the time selector with options for each hour of the day
720
- for (var i = 0; i < timePeriods.length; i++) {
721
- for (var hour = 0; hour < 12; hour++) {
722
- for (var minute = 0; minute < 60; minute+=15) {
723
- // Ensure all minutes are two digits
724
- var paddedMinute = String(minute).padStart(2, '0');
725
- var option = document.createElement('option');
726
- var friendlyHour = hour === 0 ? 12 : hour;
727
- option.value = `${friendlyHour}:${paddedMinute} ${timePeriods[i]}`;
728
- option.text = `${friendlyHour}:${paddedMinute} ${timePeriods[i]}`;
729
- timeSelector.appendChild(option);
730
- }
731
- }
732
- }
733
-
734
- // Populate date selector with options 1 through 31
735
- for (var i = 1; i <= 31; i++) {
736
- var option = document.createElement('option');
737
- option.value = i;
738
- option.text = i;
739
- dateSelector.appendChild(option);
740
- }
741
-
742
- var hoursMinutesSelectorContainer = document.createElement('div');
743
- hoursMinutesSelectorContainer.classList.add('hours-minutes-selector-container');
744
- hoursMinutesSelectorContainer.appendChild(timeLabel);
745
- hoursMinutesSelectorContainer.appendChild(timeSelector);
746
-
747
- scheduleContainer.appendChild(frequencyLabel);
748
- scheduleContainer.appendChild(frequencySelector);
749
- scheduleContainer.appendChild(dayLabel);
750
- scheduleContainer.appendChild(daySelector);
751
- scheduleContainer.appendChild(dateLabel);
752
- scheduleContainer.appendChild(dateSelector);
753
- scheduleContainer.appendChild(hoursMinutesSelectorContainer);
754
-
755
- return scheduleContainer;
756
- }
757
-
758
- function setupScheduleViewListener(cronString, automationId) {
759
- // Parse the cron string
760
- var cronParts = cronString.split(' ');
761
- var minutes = cronParts[0];
762
- var hours = cronParts[1];
763
- var dayOfMonth = cronParts[2];
764
- var month = cronParts[3];
765
- var dayOfWeek = cronParts[4];
766
-
767
- var timeSelector = document.getElementById(`time-selector-${automationId}`);
768
-
769
- // Set the initial value of the time selector based on the cron string. Convert 24-hour time to 12-hour time
770
- if (hours === '*' && minutes === '*') {
771
- var currentTime = new Date();
772
- hours = currentTime.getHours();
773
- minutes = currentTime.getMinutes();
774
- }
775
- var hours = parseInt(hours);
776
- var minutes = parseInt(minutes);
777
- minutes = Math.round(minutes / 15) * 15;
778
- if (minutes === 60) {
779
- hours = (hours % 12) + 1;
780
- minutes = 0;
781
- }
782
- var timePeriod = hours >= 12 ? 'PM' : 'AM';
783
- hours = hours % 12;
784
- hours = hours ? hours : 12; // 0 should be 12
785
- minutes = Math.round(minutes / 15) * 15;
786
- minutes = String(minutes).padStart(2, '0');
787
- // Resolve minutes to the nearest 15 minute interval
788
-
789
- timeSelector.value = `${hours}:${minutes} ${timePeriod}`;
790
-
791
- const frequencySelector = document.getElementById(`frequency-selector-${automationId}`);
792
- const daySelector = document.getElementById(`day-selector-${automationId}`);
793
- const daySelectorLabel = document.getElementById(`day-selector-label-${automationId}`);
794
- const dateSelector = document.getElementById(`date-selector-${automationId}`);
795
- const dateLabel = document.getElementById(`date-label-${automationId}`);
796
-
797
- // Event listener for frequency selector change
798
- frequencySelector.addEventListener('change', function() {
799
- processFrequencySelector(frequencySelector, daySelector, daySelectorLabel, dateSelector, dateLabel);
800
- });
801
-
802
- // Set the initial value based on the frequency selector value
803
- processFrequencySelector(frequencySelector, daySelector, daySelectorLabel, dateSelector, dateLabel);
804
- }
805
-
806
- function processFrequencySelector(frequencySelector, daySelector, daySelectorLabel, dateSelector, dateLabel) {
807
- switch (frequencySelector.value) {
808
- case 'day':
809
- daySelector.style.display = 'none';
810
- dateSelector.style.display = 'none';
811
- daySelectorLabel.style.display = 'none';
812
- dateLabel.style.display = 'none';
813
- break;
814
- case 'week':
815
- daySelector.style.display = 'block';
816
- dateSelector.style.display = 'none';
817
- daySelectorLabel.style.display = 'block';
818
- dateLabel.style.display = 'none';
819
- break;
820
- case 'month':
821
- daySelector.style.display = 'none';
822
- dateSelector.style.display = 'block';
823
- daySelectorLabel.style.display = 'none';
824
- dateLabel.style.display = 'block';
825
- break;
826
- }
827
- }
828
-
829
- function convertFrequencyToCron(automationId) {
830
- var frequencySelector = document.getElementById(`frequency-selector-${automationId}`);
831
- var daySelector = document.getElementById(`day-selector-${automationId}`);
832
- var dateSelector = document.getElementById(`date-selector-${automationId}`);
833
- var timeSelector = document.getElementById(`time-selector-${automationId}`);
834
-
835
- var hours = timeSelector.value.split(':')[0];
836
- var minutes = timeSelector.value.split(':')[1].split(' ')[0];
837
-
838
- var cronString = '';
839
- switch (frequencySelector.value) {
840
- case 'day':
841
- cronString = `${minutes} ${hours} * * *`;
842
- break;
843
- case 'week':
844
- cronString = `${minutes} ${hours} * * ${daySelector.value}`;
845
- break;
846
- case 'month':
847
- cronString = `${minutes} ${hours} ${dateSelector.value} * *`;
848
- break;
849
- }
850
-
851
- return cronString;
852
- }
853
-
854
- async function saveAutomation(automationId, create=false) {
855
- if ("{{ username }}" == "None") {
856
- url_encoded_href = encodeURIComponent(window.location.href);
857
- window.location.href = `/login?next=${url_encoded_href}`;
858
- return;
859
- }
860
-
861
- const scheduleEl = document.getElementById(`automation-schedule-${automationId}`);
862
- const notificationEl = document.getElementById(`automation-success-${automationId}`);
863
- const saveButtonEl = document.getElementById(`save-automation-button-${automationId}`);
864
- const queryToRunEl = document.getElementById(`automation-queryToRun-${automationId}`);
865
- const queryToRun = encodeURIComponent(queryToRunEl.value);
866
- const actOn = create ? "Creat" : "Sav";
867
- var cronTime = null;
868
-
869
- if (queryToRun == "") {
870
- if (!create && scheduleEl.value == "") {
871
- notificationEl.textContent = `⚠️ Failed to automate. All input fields need to be filled.`;
872
- notificationEl.style.display = "block";
873
- let originalQueryToRunElBorder = queryToRunEl.style.border;
874
- if (queryToRun === "") queryToRunEl.style.border = "2px solid red";
875
- let originalScheduleElBorder = scheduleEl.style.border;
876
- if (scheduleEl.value === "") scheduleEl.style.border = "2px solid red";
877
- setTimeout(function() {
878
- if (queryToRun == "") queryToRunEl.style.border = originalQueryToRunElBorder;
879
- if (scheduleEl.value == "") scheduleEl.style.border = originalScheduleElBorder;
880
- }, 2000);
881
-
882
- return;
883
- }
884
- }
885
-
886
- // Get client location information from IP
887
- const ip_response = await fetch("https://ipapi.co/json");
888
- let ip_data = null;
889
- if (ip_response.ok) {
890
- ip_data = await ip_response.json();
891
- }
892
-
893
- // Get cron string from natural language user schedule, if changed
894
- if (create && !scheduleEl) {
895
- crontime = convertFrequencyToCron(automationId);
896
- } else {
897
- crontime = scheduleEl.getAttribute('data-original') !== scheduleEl.value ? getCronString(scheduleEl.value) : scheduleEl.getAttribute('data-cron');
898
- }
899
-
900
- if (crontime.startsWith("ERROR:")) {
901
- notificationEl.textContent = `⚠️ Failed to automate. Fix or simplify Schedule input field.`;
902
- notificationEl.style.display = "block";
903
- let originalScheduleElBorder = scheduleEl.style.border;
904
- scheduleEl.style.border = "2px solid red";
905
- setTimeout(function() {
906
- scheduleEl.style.border = originalScheduleElBorder;
907
- }, 2000);
908
-
909
- return;
910
- }
911
- const encodedCrontime = encodeURIComponent(crontime);
912
-
913
- // Construct query string and select method for API call
914
- let query_string = `q=${queryToRun}&crontime=${encodedCrontime}`;
915
- if (ip_data) {
916
- query_string += `&city=${ip_data.city}&region=${ip_data.region}&country=${ip_data.country_name}&timezone=${ip_data.timezone}`;
917
- }
918
-
919
- let method = "POST";
920
- if (!create) {
921
- const subjectEl = document.getElementById(`automation-subject-${automationId}`);
922
- const subject = encodeURIComponent(subjectEl.value);
923
- query_string += `&automation_id=${automationId}`;
924
- query_string += `&subject=${subject}`;
925
- method = "PUT"
926
- }
927
-
928
- // Create a loading animation while waiting for the API response
929
- // TODO add a more pleasant loading symbol here.
930
- notificationEl.textContent = `⏳ ${actOn}ing automation...`;
931
- notificationEl.style.display = "block";
932
-
933
- fetch(`/api/automation?${query_string}`, {
934
- method: method,
935
- headers: {
936
- 'Content-Type': 'application/json',
937
- },
938
- })
939
- .then(response => response.ok ? response.json() : Promise.reject(data))
940
- .then(automation => {
941
- // Remove modal overlay
942
- document.getElementById('overlay').style.display = 'none';
943
- if (create) {
944
- const automationEl = document.getElementById(`automation-card-${automationId}`);
945
- // Create a more interesting confirmation animation.
946
- automationEl.classList.remove("new-automation");
947
- setTimeout(function() {
948
- // Check if automationEl is a child of #automations or #suggested-automations-list
949
- // If #suggested-automations-list, remove the element from the list and add it to #automations
950
- let parentEl = automationEl.parentElement;
951
- let isSuggested = parentEl.id === "suggested-automations-list";
952
- if (isSuggested) {
953
- parentEl.removeChild(automationEl);
954
- document.getElementById("automations").prepend(automationEl);
955
- }
956
- automationEl.replaceWith(generateAutomationRow(automation));
957
- }, 1000);
958
- } else {
959
- updateAutomationRow(automation);
960
- }
961
-
962
- notificationEl.style.display = "none";
963
- saveButtonEl.textContent = `✅ Automation ${actOn}ed`;
964
- setTimeout(function() {
965
- const automationIDElements = document.querySelectorAll(`.${automationId}`);
966
- automationIDElements.forEach(el => {
967
- // If it has the class automation-buttons, turn on the hide-details class
968
- if (el.classList.contains("automation-buttons"))
969
- {
970
- el.classList.add("hide-details");
971
- }
972
- // If it has the class automationId, turn on the fake-input class
973
- else if (el.classList.contains(automationId))
974
- {
975
- el.classList.add("fake-input");
976
- }
977
- });
978
- saveButtonEl.textContent = "Save";
979
- }, 2000);
980
- })
981
- .catch(error => {
982
- notificationEl.textContent = `⚠️ Failed to ${actOn.toLowerCase()}e automation.`;
983
- notificationEl.style.display = "block";
984
- saveButtonEl.textContent = `⚠️ Failed to ${actOn.toLowerCase()}e automation`;
985
- setTimeout(function() {
986
- saveButtonEl.textContent = `${actOn}e`;
987
- }, 2000);
988
- return;
989
- });
990
- }
991
-
992
- function createAutomationEl(placeholderId = null) {
993
- let automationEl = document.createElement("div");
994
- automationEl.classList.add("card", "automation", "new-automation");
995
- placeholderId = placeholderId ?? `automation_${Date.now()}`;
996
- automationEl.id = "automation-card-" + placeholderId;
997
-
998
- // Create label for schedule
999
- let scheduleLabel = document.createElement("label");
1000
- scheduleLabel.setAttribute("for", "schedule");
1001
- scheduleLabel.textContent = "New Automation";
1002
-
1003
- // Create schedule selector
1004
- let scheduleSelector = createScheduleSelector(placeholderId);
1005
-
1006
- // Create label for query-to-run
1007
- let queryLabel = document.createElement("label");
1008
- queryLabel.setAttribute("for", "query-to-run");
1009
- queryLabel.textContent = "What would you like to receive in your automation?";
1010
-
1011
- // Create textarea for query-to-run
1012
- let queryTextarea = document.createElement("textarea");
1013
- queryTextarea.id = `automation-queryToRun-${placeholderId}`;
1014
- queryTextarea.classList.add(`automation-queryToRun-${placeholderId}`);
1015
- queryTextarea.placeholder = "Provide me with a mindful moment, reminding me to be centered.";
1016
-
1017
- // Create buttons container
1018
- let buttonsContainer = document.createElement("div");
1019
- buttonsContainer.classList.add("automation-buttons");
1020
-
1021
- // Create cancel button
1022
- let deleteButton = document.createElement("button");
1023
- deleteButton.type = "button";
1024
- deleteButton.classList.add("delete-automation-button", "negative-button");
1025
- deleteButton.textContent = "Cancel";
1026
- deleteButton.id = `delete-automation-button-${placeholderId}`;
1027
- deleteButton.onclick = () => deleteAutomation(placeholderId, true);
1028
-
1029
- // Create save button
1030
- let saveButton = document.createElement("button");
1031
- saveButton.type = "button";
1032
- saveButton.classList.add("save-automation-button");
1033
- saveButton.textContent = "Create";
1034
- saveButton.id = `save-automation-button-${placeholderId}`;
1035
- saveButton.onclick = () => saveAutomation(placeholderId, true);
1036
-
1037
- // Create success message container
1038
- let successMessage = document.createElement("div");
1039
- successMessage.id = `automation-success-${placeholderId}`;
1040
- successMessage.style.display = "none";
1041
-
1042
- // Append schedule label and selector
1043
- automationEl.appendChild(scheduleLabel);
1044
- automationEl.appendChild(scheduleSelector);
1045
-
1046
- // Append query label and textarea
1047
- automationEl.appendChild(queryLabel);
1048
- automationEl.appendChild(queryTextarea);
1049
-
1050
- // Append buttons to their container
1051
- buttonsContainer.appendChild(deleteButton);
1052
- buttonsContainer.appendChild(saveButton);
1053
-
1054
- // Append buttons container to automationEl
1055
- automationEl.appendChild(buttonsContainer);
1056
-
1057
- // Append success message to automationEl
1058
- automationEl.appendChild(successMessage);
1059
-
1060
- return automationEl;
1061
- }
1062
-
1063
- const createAutomationButtonEl = document.getElementById("create-automation-button");
1064
- createAutomationButtonEl.addEventListener("click", function(event) {
1065
- event.preventDefault();
1066
-
1067
- // Insert automationEl into the DOM
1068
- let placeholderId = `automation_${Date.now()}`;
1069
- let automationEl = createAutomationEl(placeholderId);
1070
- document.getElementById("automations").insertBefore(automationEl, document.getElementById("automations").firstChild);
1071
-
1072
- setupScheduleViewListener("* * * * *", placeholderId);
1073
- })
1074
-
1075
- function createPreFilledAutomation(subject, crontime, query) {
1076
- document.getElementById('overlay').style.display = 'block';
1077
-
1078
- let placeholderId = `automation_${Date.now()}`;
1079
- let automationEl = createAutomationEl(placeholderId);
1080
-
1081
- // Configure automationEl with pre-filled values
1082
- automationEl.classList.add(`${placeholderId}`);
1083
- automationEl.getElementsByClassName(`automation-queryToRun-${placeholderId}`)[0].value = query;
1084
-
1085
- // Create input for subject
1086
- let subjectEl = document.createElement("input");
1087
- subjectEl.type = "text";
1088
- subjectEl.id = `automation-subject-${placeholderId}`;
1089
- subjectEl.value = subject;
1090
-
1091
- // Insert subjectEl after label for subject
1092
- let subjectLabel = automationEl.querySelector(`label[for="automation-subject-${placeholderId}"]`);
1093
- automationEl.firstChild.insertAdjacentElement('afterend', subjectEl);
1094
- automationEl.firstChild.label = "subject";
1095
-
1096
- // Insert automationEl into the DOM
1097
- document.getElementById("automations").insertBefore(automationEl, document.getElementById("automations").firstChild);
1098
-
1099
- setupScheduleViewListener(crontime, placeholderId);
1100
- }
1101
-
1102
- </script>
1103
- {% endblock %}