mirror of
https://github.com/khoj-ai/khoj.git
synced 2024-11-27 17:35:07 +01:00
Improve the UX of automations (#737)
* Improve the automations UX - Add suggested jobs to elimiinate some of the cold start problem - Make each of the tasks cards that are clickable/editable * Hide suggested automations that have already been added * Add a footer and reapply styling when a save action is taken on a card
This commit is contained in:
parent
70d0ee4310
commit
fbd76f8ebe
3 changed files with 241 additions and 39 deletions
|
@ -400,7 +400,6 @@ To get started, just start typing below. You can also type / to see a list of co
|
|||
// • auto-render specific keys, e.g.:
|
||||
delimiters: [
|
||||
{left: '$$', right: '$$', display: true},
|
||||
{left: '$', right: '$', display: false},
|
||||
{left: '\\(', right: '\\)', display: false},
|
||||
{left: '\\[', right: '\\]', display: true}
|
||||
],
|
||||
|
|
|
@ -6,17 +6,29 @@
|
|||
<img class="card-icon" src="/static/assets/icons/automation.svg?v={{ khoj_version }}" alt="Automate">
|
||||
<span class="card-title-text">Automate (Preview)</span>
|
||||
<div class="instructions">
|
||||
You can automate queries to run on a schedule using Khoj's automations for smart reminders. Results will be sent straight to your inbox. This is an experimental feature, so your results may vary. Report any issues to <a class="inline-link-light" href="mailto:team@khoj.dev">team@khoj.dev</a>.
|
||||
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>.
|
||||
</div>
|
||||
<div class="instructions notice">
|
||||
Sending automation results to <a class="inline-link-light" href="mailto:{{ username}}">{{ username }}</a>.
|
||||
</div>
|
||||
</h2>
|
||||
<div class="section-body">
|
||||
<button id="create-automation-button" type="button" class="positive-button">
|
||||
<img class="automation-action-icon" src="/static/assets/icons/new.svg" alt="Automations">
|
||||
<span id="create-automation-button-text">Build</span>
|
||||
<span id="create-automation-button-text">Build Your Own</span>
|
||||
</button>
|
||||
<div id="automations" class="section-cards"></div>
|
||||
<div id="suggested-automations">
|
||||
<h2 class="section-title">
|
||||
<span class="card-title-text">Suggested Automations</span>
|
||||
</h2>
|
||||
<div id="suggested-automations-list" class="section-cards"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="footer">
|
||||
<a href="/">Back to Chat</a>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/static/assets/natural-cron.min.js"></script>
|
||||
<style>
|
||||
|
@ -27,21 +39,59 @@
|
|||
width: 100%;
|
||||
height: 100%;
|
||||
grid-template-rows: none;
|
||||
background-color: var(--frosted-background-color);
|
||||
padding: 12px;
|
||||
background-color: white;
|
||||
border-radius: 20px;
|
||||
padding: 20px;
|
||||
box-shadow: rgba(3, 3, 3, 0.08) 0px 1px 12px;
|
||||
transition: box-shadow 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
|
||||
div#footer {
|
||||
width: auto;
|
||||
padding: 10px;
|
||||
background-color: var(--background-color);
|
||||
border-top: 1px solid var(--main-text-color);
|
||||
text-align: left;
|
||||
margin-top: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
div#footer a {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
textarea.fake-input,
|
||||
input.fake-input {
|
||||
height: auto;
|
||||
padding-top: 0px;
|
||||
padding-bottom: 0px;
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
#create-automation-button {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
div.notice {
|
||||
border-top: 1px solid black;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
div#suggested-automations-list,
|
||||
div#automations {
|
||||
margin-bottom: 12px;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
button.negative-button {
|
||||
background-color: gainsboro;
|
||||
}
|
||||
.positive-button {
|
||||
background-color: var(--primary-hover);
|
||||
background-color: var(--primary-hover)
|
||||
}
|
||||
.positive-button:hover {
|
||||
background-color: var(--summer-sun);
|
||||
|
@ -50,25 +100,31 @@
|
|||
div.automation-buttons {
|
||||
display: grid;
|
||||
grid-gap: 8px;
|
||||
grid-template-columns: 1fr 3fr;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
}
|
||||
|
||||
button.save-automation-button {
|
||||
background-color: var(--summer-sun);
|
||||
}
|
||||
|
||||
button.save-automation-button,
|
||||
button.delete-automation-button {
|
||||
padding: 8px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
button.save-automation-button:hover {
|
||||
background-color: var(--primary-hover);
|
||||
}
|
||||
|
||||
div.new-automation {
|
||||
background-color: var(--frosted-background-color);
|
||||
border-radius: 10px;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 4px 6px 0 hsla(0, 0%, 0%, 0.2);
|
||||
margin-bottom: 20px;
|
||||
transition: box-shadow 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
|
||||
div.automation:hover,
|
||||
div.new-automation:hover {
|
||||
box-shadow: 0 10px 15px 0 hsla(0, 0%, 0%, 0.1);
|
||||
transform: translateY(-5px);
|
||||
|
@ -80,13 +136,42 @@
|
|||
|
||||
div.card-header {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
grid-template-rows: auto 1fr;
|
||||
grid-gap: 8px;
|
||||
align-items: baseline;
|
||||
padding: 8px;
|
||||
background-color: var(--frosted-background-color);
|
||||
}
|
||||
input.schedule {
|
||||
font-size: medium;
|
||||
height: auto;
|
||||
font-weight: lighter !important;
|
||||
}
|
||||
|
||||
h2.section-title {
|
||||
font-size: larger;
|
||||
}
|
||||
|
||||
div.card-header input {
|
||||
font-weight: bold;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
div.automation textarea {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
border-radius: 4px;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
img.promo-image {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
|
||||
div.card-header textarea,
|
||||
div.card-header input,
|
||||
div.card-header:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
@ -106,6 +191,16 @@
|
|||
animation: confirmation 1s;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
div#automations,
|
||||
div#suggested-automations-list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
div.automation-buttons {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
<script>
|
||||
function deleteAutomation(automationId) {
|
||||
|
@ -143,11 +238,13 @@
|
|||
function onClickAutomationCard(automationId) {
|
||||
const automationIDElements = document.querySelectorAll(`.${automationId}`);
|
||||
automationIDElements.forEach(el => {
|
||||
el.classList.toggle("hide-details");
|
||||
// Only toggle the first time the button is clicked
|
||||
el.classList.remove("hide-details");
|
||||
el.classList.remove("fake-input");
|
||||
});
|
||||
}
|
||||
|
||||
function generateAutomationRow(automation) {
|
||||
function generateAutomationRow(automation, isSuggested=false) {
|
||||
let automationId = automation.id;
|
||||
let automationNextRun = `Next run at ${automation.next}\nCron: ${automation.crontime}`;
|
||||
let automationEl = document.createElement("div");
|
||||
|
@ -156,47 +253,106 @@
|
|||
<div class="card-header" onclick="onClickAutomationCard('${automationId}')">
|
||||
<input type="text"
|
||||
id="automation-subject-${automationId}"
|
||||
class="${automationId} fake-input"
|
||||
name="subject"
|
||||
data-original="${automation.subject}"
|
||||
value="${automation.subject}">
|
||||
<div class="toggle-icon">
|
||||
<img src="/static/assets/icons/collapse.svg" alt="Toggle" class="toggle-icon">
|
||||
</div>
|
||||
</div>
|
||||
<label for="query-to-run" class="hide-details ${automationId}">Your automation</label>
|
||||
<textarea id="automation-queryToRun-${automationId}"
|
||||
class="hide-details ${automationId}"
|
||||
data-original="${automation.query_to_run}"
|
||||
name="query-to-run">${automation.query_to_run}</textarea>
|
||||
<label for="schedule" class="hide-details">Schedule</label>
|
||||
<input type="text"
|
||||
class="hide-details ${automationId}"
|
||||
<input type="text"
|
||||
id="automation-schedule-${automationId}"
|
||||
name="schedule"
|
||||
class="schedule ${automationId} fake-input"
|
||||
data-cron="${automation.crontime}"
|
||||
data-original="${automation.schedule}"
|
||||
title="${automationNextRun}"
|
||||
value="${automation.schedule}">
|
||||
<div class="hide-details automation-buttons ${automationId}">
|
||||
<button type="button"
|
||||
class="delete-automation-button negative-button"
|
||||
id="delete-automation-button-${automationId}">Delete</button>
|
||||
<button type="button"
|
||||
class="save-automation-button positive-button"
|
||||
id="save-automation-button-${automationId}">Save</button>
|
||||
<textarea id="automation-queryToRun-${automationId}"
|
||||
class="automation-instructions ${automationId} fake-input"
|
||||
data-original="${automation.query_to_run}"
|
||||
name="query-to-run">${automation.query_to_run}</textarea>
|
||||
${isSuggested ?
|
||||
`<img class=promo-image src="${automation.promoImage}" alt="Promo Image">`:
|
||||
""
|
||||
}
|
||||
</div>
|
||||
<div id="automation-buttons-wrapper">
|
||||
<div class="automation-buttons">
|
||||
<div id="empty-div"></div>
|
||||
${isSuggested ?
|
||||
`<div id="empty-div"></div>`:
|
||||
`
|
||||
<button type="button"
|
||||
class="delete-automation-button negative-button"
|
||||
id="delete-automation-button-${automationId}">Delete</button>
|
||||
`
|
||||
}
|
||||
<button type="button"
|
||||
class="save-automation-button positive-button"
|
||||
id="save-automation-button-${automationId}">
|
||||
${isSuggested ? "Add" : "Save"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="automation-success-${automationId}" style="display: none;"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
let automationButtonsSection = automationEl.querySelector(".automation-buttons");
|
||||
if (!isSuggested) {
|
||||
automationButtonsSection.classList.add("hide-details");
|
||||
automationButtonsSection.classList.add(automationId);
|
||||
}
|
||||
|
||||
let saveAutomationButtonEl = automationEl.querySelector(`#save-automation-button-${automation.id}`);
|
||||
saveAutomationButtonEl.addEventListener("click", async () => { await saveAutomation(automation.id); });
|
||||
saveAutomationButtonEl.addEventListener("click", async () => { await saveAutomation(automation.id, isSuggested); });
|
||||
let deleteAutomationButtonEl = automationEl.querySelector(`#delete-automation-button-${automation.id}`);
|
||||
deleteAutomationButtonEl.addEventListener("click", () => { deleteAutomation(automation.id); });
|
||||
if (deleteAutomationButtonEl) {
|
||||
deleteAutomationButtonEl.addEventListener("click", () => { deleteAutomation(automation.id); });
|
||||
}
|
||||
|
||||
return automationEl.firstElementChild;
|
||||
}
|
||||
|
||||
let timestamp = Math.floor(Date.now() / 1000);
|
||||
|
||||
let suggestedAutomationsMetadata = [
|
||||
{
|
||||
"subject": "Weekly Newsletter",
|
||||
"query_to_run": "Share a Newsletter including: 1. Weather forecast for this Week. 2. Recap news from last week 3. A nice quote to start the week.",
|
||||
"schedule": "9AM every Monday",
|
||||
"next": "Next run at 9AM on Monday",
|
||||
"crontime": "0 9 * * 1",
|
||||
"id": "suggested-automation" + timestamp,
|
||||
"promoImage": "https://khoj-generated-images.s3.amazonaws.com/98aef1b2-5493-41ba-a252-2ab7ab122901/f4cde8a5-522d-4515-9d2e-52565171b1b8.webp",
|
||||
},
|
||||
{
|
||||
"subject": "Daily Weather Update",
|
||||
"query_to_run": "Get the weather forecast for today and tomorrow",
|
||||
"schedule": "9AM every morning",
|
||||
"next": "Next run at 9AM today",
|
||||
"crontime": "0 9 * * *",
|
||||
"id": "suggested-automation" + (timestamp + 1),
|
||||
"promoImage": "https://khoj-generated-images.s3.amazonaws.com/98aef1b2-5493-41ba-a252-2ab7ab122901/94d9c576-b37a-45d6-bc2e-59879c15ab79.webp",
|
||||
},
|
||||
{
|
||||
"subject": "Front Page of Hacker News",
|
||||
"query_to_run": "Summarize the top 5 posts from https://news.ycombinator.com/best and share them with me, including links",
|
||||
"schedule": "9PM on every Wednesday",
|
||||
"next": "Next run at 9PM on Wednesday",
|
||||
"crontime": "0 21 * * 3",
|
||||
"id": "suggested-automation" + (timestamp + 2),
|
||||
"promoImage": "https://khoj-generated-images.s3.amazonaws.com/98aef1b2-5493-41ba-a252-2ab7ab122901/17d22551-6b8c-4628-b12d-c767d8e8bec1.webp",
|
||||
},
|
||||
{
|
||||
"subject": "Market Summary",
|
||||
"query_to_run": "Get the market summary for today and share it with me. Focus on tech stocks and the S&P 500.",
|
||||
"schedule": "9AM on every weekday",
|
||||
"next": "Next run at 9AM on Monday",
|
||||
"crontime": "0 9 * * 1-5",
|
||||
"id": "suggested-automation" + (timestamp + 3),
|
||||
"promoImage": "https://khoj-generated-images.s3.amazonaws.com/98aef1b2-5493-41ba-a252-2ab7ab122901/fd7dbfe7-c7f8-4bc9-ac18-652199fc07d9.webp",
|
||||
}
|
||||
];
|
||||
|
||||
function listAutomations() {
|
||||
const AutomationsList = document.getElementById("automations");
|
||||
fetch('/api/automations')
|
||||
|
@ -204,11 +360,37 @@
|
|||
.then(automations => {
|
||||
if (!automations?.length > 0) return;
|
||||
AutomationsList.innerHTML = ''; // Clear existing content
|
||||
AutomationsList.append(...automations.map(automation => generateAutomationRow(automation)))
|
||||
AutomationsList.append(...automations.map(automation => generateAutomationRow(automation)));
|
||||
// Check if any of the automations 'query-to-run' fields match the suggested automations
|
||||
automations.forEach(automation => {
|
||||
suggestedAutomationsMetadata.forEach(suggestedAutomation => {
|
||||
if (automation.query_to_run === suggestedAutomation.query_to_run) {
|
||||
let suggestedAutomationEl = document.getElementById(`automation-card-${suggestedAutomation.id}`);
|
||||
suggestedAutomationEl.remove();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
listAutomations();
|
||||
|
||||
if (suggestedAutomationsMetadata.length > 0) {
|
||||
suggestedAutomationsMetadata.forEach(automation => {
|
||||
automation.id = "suggested-automation" + timestamp;
|
||||
timestamp++;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
function listSuggestedAutomations() {
|
||||
const SuggestedAutomationsList = document.getElementById("suggested-automations-list");
|
||||
SuggestedAutomationsList.innerHTML = ''; // Clear existing content
|
||||
SuggestedAutomationsList.append(...suggestedAutomationsMetadata.map(automation => generateAutomationRow(automation, true)));
|
||||
}
|
||||
listSuggestedAutomations();
|
||||
|
||||
function enableSaveOnlyWhenInputsChanged() {
|
||||
const inputs = document.querySelectorAll('input[name="schedule"], textarea[name="query-to-run"], input[name="subject"]');
|
||||
inputs.forEach(input => {
|
||||
|
@ -297,6 +479,15 @@
|
|||
// Create a more interesting confirmation animation.
|
||||
automationEl.classList.add("confirmation")
|
||||
setTimeout(function() {
|
||||
|
||||
// Check if automationEl is a child of #automations or #suggested-automations-list
|
||||
// If #suggested-automations-list, remove the element from the list and add it to #automations
|
||||
let parentEl = automationEl.parentElement;
|
||||
let isSuggested = parentEl.id === "suggested-automations-list";
|
||||
if (isSuggested) {
|
||||
parentEl.removeChild(automationEl);
|
||||
document.getElementById("automations").prepend(automationEl);
|
||||
}
|
||||
automationEl.replaceWith(generateAutomationRow(automation));
|
||||
}, 1000);
|
||||
} else {
|
||||
|
@ -306,6 +497,19 @@
|
|||
notificationEl.style.display = "none";
|
||||
saveButtonEl.textContent = `✅ Automation ${actOn}d`;
|
||||
setTimeout(function() {
|
||||
const automationIDElements = document.querySelectorAll(`.${automationId}`);
|
||||
automationIDElements.forEach(el => {
|
||||
// If it has the class automation-buttons, turn on the hide-details class
|
||||
if (el.classList.contains("automation-buttons"))
|
||||
{
|
||||
el.classList.add("hide-details");
|
||||
}
|
||||
// If it has the class automationId, turn on the fake-input class
|
||||
else if (el.classList.contains(automationId))
|
||||
{
|
||||
el.classList.add("fake-input");
|
||||
}
|
||||
});
|
||||
saveButtonEl.textContent = "Save";
|
||||
}, 2000);
|
||||
})
|
||||
|
@ -330,7 +534,7 @@
|
|||
const placeholderId = Date.now();
|
||||
automationEl.id = "automation-card-" + placeholderId;
|
||||
automationEl.innerHTML = `
|
||||
<label for="query-to-run">Your new automation</label>
|
||||
<label for="query-to-run">Instructions</label>
|
||||
<textarea id="automation-queryToRun-${placeholderId}" placeholder="Share a Newsletter including: 1. Weather forecast for this Week. 2. A Book Highlight from my Notes. 3. Recap News from Last Week"></textarea>
|
||||
<label for="schedule">Schedule</label>
|
||||
<input type="text"
|
||||
|
|
|
@ -365,7 +365,6 @@ To get started, just start typing below. You can also type / to see a list of co
|
|||
// • auto-render specific keys, e.g.:
|
||||
delimiters: [
|
||||
{left: '$$', right: '$$', display: true},
|
||||
{left: '$', right: '$', display: false},
|
||||
{left: '\\(', right: '\\)', display: false},
|
||||
{left: '\\[', right: '\\]', display: true}
|
||||
],
|
||||
|
|
Loading…
Reference in a new issue