mirror of
https://github.com/khoaliber/khoj.git
synced 2026-03-06 21:29:12 +00:00
293 lines
13 KiB
HTML
293 lines
13 KiB
HTML
{% extends "base_config.html" %}
|
|
{% block content %}
|
|
<div class="page">
|
|
<div class="section">
|
|
<h2 class="section-title">
|
|
<img class="card-icon" src="/static/assets/icons/automation.svg?v={{ khoj_version }}" alt="Automate">
|
|
<span class="card-title-text">Automate</span>
|
|
<!-- <div class="instructions">
|
|
<a href="https://docs.khoj.dev/features/automations">ⓘ Help</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">Create</span>
|
|
</button>
|
|
<div id="automations" class="section-cards"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<script src="/static/assets/natural-cron.min.js"></script>
|
|
<style>
|
|
td {
|
|
padding: 10px 0;
|
|
}
|
|
div.automation {
|
|
width: 100%;
|
|
height: 100%;
|
|
grid-template-rows: none;
|
|
}
|
|
#create-automation-button {
|
|
width: auto;
|
|
}
|
|
div#automations {
|
|
margin-bottom: 12px;
|
|
grid-template-columns: 1fr;
|
|
}
|
|
button.negative-button {
|
|
background-color: gainsboro;
|
|
}
|
|
.positive-button {
|
|
background-color: var(--primary-hover);
|
|
}
|
|
.positive-button:hover {
|
|
background-color: var(--summer-sun);
|
|
}
|
|
|
|
div.automation-buttons {
|
|
display: grid;
|
|
grid-gap: 8px;
|
|
grid-template-columns: 1fr 3fr;
|
|
}
|
|
|
|
button.save-automation-button {
|
|
background-color: var(--summer-sun);
|
|
}
|
|
|
|
button.save-automation-button:hover {
|
|
background-color: var(--primary-hover);
|
|
}
|
|
|
|
div.new-automation {
|
|
background-color: #f9f9f9;
|
|
border-radius: 10px;
|
|
box-shadow: 0 4px 6px 0 hsla(0, 0%, 0%, 0.2);
|
|
padding: 20px;
|
|
margin-bottom: 20px;
|
|
transition: box-shadow 0.3s ease, transform 0.3s ease;
|
|
}
|
|
|
|
div.new-automation:hover {
|
|
box-shadow: 0 10px 15px 0 hsla(0, 0%, 0%, 0.1);
|
|
transform: translateY(-5px);
|
|
}
|
|
|
|
</style>
|
|
<script>
|
|
function deleteAutomation(automationId) {
|
|
const AutomationList = document.getElementById("automations");
|
|
fetch(`/api/automation?automation_id=${automationId}`, {
|
|
method: 'DELETE',
|
|
})
|
|
.then(response => {
|
|
if (response.status == 200 || response.status == 204) {
|
|
const AutomationItem = document.getElementById(`automation-card-${automationId}`);
|
|
AutomationList.removeChild(AutomationItem);
|
|
}
|
|
});
|
|
}
|
|
|
|
function updateAutomationRow(automation) {
|
|
let automationId = automation.id;
|
|
let automationNextRun = `Next run at ${automation.next}\nCron: ${automation.crontime}`;
|
|
|
|
let scheduleEl = document.getElementById(`automation-schedule-${automationId}`);
|
|
scheduleEl.setAttribute('data-original', automation.schedule);
|
|
scheduleEl.setAttribute('data-cron', automation.crontime);
|
|
scheduleEl.setAttribute('title', automationNextRun);
|
|
scheduleEl.value = automation.schedule;
|
|
|
|
let subjectEl = document.getElementById(`automation-subject-${automationId}`);
|
|
subjectEl.setAttribute('data-original', automation.subject);
|
|
subjectEl.value = automation.subject;
|
|
|
|
let queryEl = document.getElementById(`automation-queryToRun-${automationId}`);
|
|
queryEl.setAttribute('data-original', automation.query_to_run);
|
|
queryEl.value = automation.query_to_run;
|
|
}
|
|
|
|
function generateAutomationRow(automation) {
|
|
let automationId = automation.id;
|
|
let automationNextRun = `Next run at ${automation.next}\nCron: ${automation.crontime}`;
|
|
let automationEl = document.createElement("div");
|
|
automationEl.innerHTML = `
|
|
<div class="card automation" id="automation-card-${automationId}">
|
|
<input type="text"
|
|
id="automation-subject-${automationId}"
|
|
name="subject"
|
|
data-original="${automation.subject}"
|
|
value="${automation.subject}">
|
|
<label for="query-to-run">Your automation</label>
|
|
<textarea id="automation-queryToRun-${automationId}"
|
|
data-original="${automation.query_to_run}"
|
|
name="query-to-run">${automation.query_to_run}</textarea>
|
|
<label for="schedule">Schedule</label>
|
|
<input type="text"
|
|
id="automation-schedule-${automationId}"
|
|
name="schedule"
|
|
data-cron="${automation.crontime}"
|
|
data-original="${automation.schedule}"
|
|
title="${automationNextRun}"
|
|
value="${automation.schedule}">
|
|
<div class="automation-buttons">
|
|
<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>
|
|
</div>
|
|
<div id="automation-success-${automationId}" style="display: none;"></div>
|
|
</div>
|
|
`;
|
|
|
|
let saveAutomationButtonEl = automationEl.querySelector(`#save-automation-button-${automation.id}`);
|
|
saveAutomationButtonEl.addEventListener("click", async () => { await saveAutomation(automation.id); });
|
|
let deleteAutomationButtonEl = automationEl.querySelector(`#delete-automation-button-${automation.id}`);
|
|
deleteAutomationButtonEl.addEventListener("click", () => { deleteAutomation(automation.id); });
|
|
|
|
return automationEl.firstElementChild;
|
|
}
|
|
|
|
function listAutomations() {
|
|
const AutomationsList = document.getElementById("automations");
|
|
fetch('/api/automations')
|
|
.then(response => response.json())
|
|
.then(automations => {
|
|
if (!automations?.length > 0) return;
|
|
AutomationsList.innerHTML = ''; // Clear existing content
|
|
AutomationsList.append(...automations.map(automation => generateAutomationRow(automation)))
|
|
});
|
|
}
|
|
listAutomations();
|
|
|
|
function enableSaveOnlyWhenInputsChanged() {
|
|
const inputs = document.querySelectorAll('input[name="schedule"], textarea[name="query-to-run"], input[name="subject"]');
|
|
inputs.forEach(input => {
|
|
input.addEventListener('change', function() {
|
|
// Get automation id by splitting the id by "-" and taking all elements after the second one
|
|
const automationId = this.id.split("-").slice(2).join("-");
|
|
let anyChanged = false;
|
|
let inputNameStubs = ["subject", "query-to-run", "schedule"]
|
|
for (let stub of inputNameStubs) {
|
|
let el = document.getElementById(`automation-${stub}-${automationId}`);
|
|
let originalValue = el.getAttribute('data-original');
|
|
let currentValue = el.value;
|
|
if (originalValue !== currentValue) {
|
|
anyChanged = true;
|
|
break;
|
|
}
|
|
}
|
|
document.getElementById(`save-automation-button-${automationId}`).disabled = !anyChanged;
|
|
});
|
|
});
|
|
}
|
|
|
|
async function saveAutomation(automationId, create=false) {
|
|
const queryToRun = encodeURIComponent(document.getElementById(`automation-queryToRun-${automationId}`).value);
|
|
const scheduleEl = document.getElementById(`automation-schedule-${automationId}`);
|
|
const notificationEl = document.getElementById(`automation-success-${automationId}`);
|
|
const saveButtonEl = document.getElementById(`save-automation-button-${automationId}`);
|
|
const actOn = create ? "Create" : "Save";
|
|
|
|
if (queryToRun == "" || scheduleEl.value == "") {
|
|
return;
|
|
}
|
|
|
|
// Get client location information from IP
|
|
const ip_response = await fetch("https://ipapi.co/json")
|
|
const ip_data = await ip_response.json();
|
|
|
|
// Get cron string from natural language user schedule, if changed
|
|
const crontime = scheduleEl.getAttribute('data-original') !== scheduleEl.value ? getCronString(scheduleEl.value) : scheduleEl.getAttribute('data-cron');
|
|
if (crontime.startsWith("ERROR:")) {
|
|
notificationEl.textContent = `⚠️ Failed to automate. Fix or simplify Schedule input field.`;
|
|
notificationEl.style.display = "block";
|
|
let originalScheduleElBorder = scheduleEl.style.border;
|
|
scheduleEl.style.border = "2px solid red";
|
|
setTimeout(function() {
|
|
scheduleEl.style.border = originalScheduleElBorder;
|
|
}, 2000);
|
|
|
|
return;
|
|
}
|
|
const encodedCrontime = encodeURIComponent(crontime);
|
|
|
|
// Construct query string and select method for API call
|
|
let query_string = `q=${queryToRun}&crontime=${encodedCrontime}&city=${ip_data.city}®ion=${ip_data.region}&country=${ip_data.country_name}&timezone=${ip_data.timezone}`;
|
|
|
|
let method = "POST";
|
|
if (!create) {
|
|
const subject = encodeURIComponent(document.getElementById(`automation-subject-${automationId}`).value);
|
|
query_string += `&automation_id=${automationId}`;
|
|
query_string += `&subject=${subject}`;
|
|
method = "PUT"
|
|
}
|
|
|
|
fetch(`/api/automation?${query_string}`, {
|
|
method: method,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
})
|
|
.then(response => response.ok ? response.json() : Promise.reject(data))
|
|
.then(automation => {
|
|
if (create) {
|
|
const automationEl = document.getElementById(`automation-card-${automationId}`);
|
|
automationEl.replaceWith(generateAutomationRow(automation));
|
|
} else {
|
|
updateAutomationRow(automation);
|
|
}
|
|
|
|
notificationEl.style.display = "none";
|
|
saveButtonEl.textContent = `✅ Automation ${actOn}d`;
|
|
setTimeout(function() {
|
|
saveButtonEl.textContent = "Save";
|
|
}, 2000);
|
|
})
|
|
.catch(error => {
|
|
notificationEl.textContent = `⚠️ Failed to ${actOn.toLowerCase()} automations.`;
|
|
notificationEl.style.display = "block";
|
|
saveButtonEl.textContent = `⚠️ Failed to ${actOn.toLowerCase()} automations`;
|
|
setTimeout(function() {
|
|
saveButtonEl.textContent = actOn;
|
|
}, 2000);
|
|
return;
|
|
});
|
|
}
|
|
|
|
const create_automation_button = document.getElementById("create-automation-button");
|
|
create_automation_button.addEventListener("click", function(event) {
|
|
event.preventDefault();
|
|
var automationEl = document.createElement("div");
|
|
automationEl.classList.add("card");
|
|
automationEl.classList.add("automation");
|
|
automationEl.classList.add("new-automation")
|
|
const placeholderId = Date.now();
|
|
automationEl.id = "automation-card-" + placeholderId;
|
|
automationEl.innerHTML = `
|
|
<label for="query-to-run">Your new automation</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"
|
|
id="automation-schedule-${placeholderId}"
|
|
name="schedule"
|
|
placeholder="9AM every morning">
|
|
<div class="automation-buttons">
|
|
<button type="button"
|
|
class="delete-automation-button negative-button"
|
|
onclick="deleteAutomation(${placeholderId}, true)"
|
|
id="delete-automation-button-${placeholderId}">Cancel</button>
|
|
<button type="button"
|
|
class="save-automation-button"
|
|
onclick="saveAutomation(${placeholderId}, true)"
|
|
id="save-automation-button-${placeholderId}">Create</button>
|
|
</div>
|
|
<div id="automation-success-${placeholderId}" style="display: none;"></div>
|
|
`;
|
|
document.getElementById("automations").insertBefore(automationEl, document.getElementById("automations").firstChild);
|
|
})
|
|
</script>
|
|
{% endblock %}
|