mirror of
https://github.com/khoj-ai/khoj.git
synced 2024-11-23 23:48:56 +01:00
Allow automations to be shareable (#790)
* Updating the API / UI to support sharing of automations * Allow people to see the automations even when not logged in, and add an overlay effect * Handle unauthenticated users taking actions * Support showing pre-filled automation details on the config automations page * Redirect user to login if they try to add an automation while unauthenticated
This commit is contained in:
parent
2667ef4544
commit
5ec641837a
5 changed files with 146 additions and 11 deletions
|
@ -32,7 +32,10 @@
|
|||
</div>
|
||||
</body>
|
||||
<script>
|
||||
document.getElementById("settings-nav").classList.add("khoj-nav-selected");
|
||||
const settingsNav = document.getElementById("settings-nav");
|
||||
if (settingsNav) {
|
||||
settingsNav.classList.add("khoj-nav-selected");
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
html, body {
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
{% block content %}
|
||||
<div class="page">
|
||||
<div class="section">
|
||||
<div id="overlay" style="display: none;"></div>
|
||||
<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 (Preview)</span>
|
||||
|
@ -35,6 +36,16 @@
|
|||
td {
|
||||
padding: 10px 0;
|
||||
}
|
||||
#overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: black;
|
||||
opacity: 0.5;
|
||||
z-index: 1;
|
||||
}
|
||||
div.automation {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
@ -62,6 +73,7 @@
|
|||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
img.automation-share-icon,
|
||||
img.automation-edit-cancel-icon,
|
||||
img.automation-edit-icon {
|
||||
width: 24px;
|
||||
|
@ -204,7 +216,7 @@
|
|||
|
||||
div.subject-wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
grid-template-columns: 1fr auto auto;
|
||||
grid-gap: 8px;
|
||||
}
|
||||
|
||||
|
@ -240,6 +252,14 @@
|
|||
<script>
|
||||
function deleteAutomation(automationId) {
|
||||
const AutomationList = document.getElementById("automations");
|
||||
if ("{{ username }}" === "None") {
|
||||
const AutomationItem = document.getElementById(`automation-card-${automationId}`);
|
||||
AutomationList.removeChild(AutomationItem);
|
||||
// Remove the Automation from the DOM and return
|
||||
AutomationItem.remove();
|
||||
document.getElementById('overlay').style.display = 'none';
|
||||
return;
|
||||
}
|
||||
fetch(`/api/automation?automation_id=${automationId}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
|
@ -247,6 +267,7 @@
|
|||
if (response.status == 200 || response.status == 204) {
|
||||
const AutomationItem = document.getElementById(`automation-card-${automationId}`);
|
||||
AutomationList.removeChild(AutomationItem);
|
||||
document.getElementById('overlay').style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -336,6 +357,35 @@
|
|||
})
|
||||
}
|
||||
|
||||
function closeShareModal(automationId) {
|
||||
const modal = document.getElementById(`share-modal-${automationId}`);
|
||||
modal.remove();
|
||||
}
|
||||
|
||||
function copyShareLink(event, automationId, subject, crontime, queryToRun) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const encodedSubject = encodeURIComponent(subject);
|
||||
const encodedCrontime = encodeURIComponent(crontime);
|
||||
const encodedQueryToRun = encodeURIComponent(queryToRun);
|
||||
|
||||
const shareLink = `${window.location.origin}/automations?subject=${encodedSubject}&crontime=${encodedCrontime}&queryToRun=${encodedQueryToRun}`;
|
||||
const button = document.getElementById(`share-link-${automationId}`);
|
||||
|
||||
navigator.clipboard.writeText(shareLink).then(() => {
|
||||
button.src = "/static/assets/icons/copy-button-success.svg";
|
||||
setTimeout(() => {
|
||||
button.src = "/static/assets/icons/share.svg";
|
||||
}, 1000);
|
||||
}).catch((error) => {
|
||||
console.error("Error copying share link output to clipboard:", error);
|
||||
setTimeout(() => {
|
||||
button.src = "/static/assets/icons/share.svg";
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
function generateAutomationRow(automation, isSuggested=false) {
|
||||
let automationId = automation.id;
|
||||
let automationNextRun = `Next run at ${automation.next}\nCron: ${automation.crontime}`;
|
||||
|
@ -350,6 +400,7 @@
|
|||
name="subject"
|
||||
data-original="${automation.subject}"
|
||||
value="${automation.subject}">
|
||||
<img class=automation-share-icon id="share-link-${automationId}" src="/static/assets/icons/share.svg" alt="Share" onclick="copyShareLink(event, '${automationId}', '${automation.subject}', '${automation.crontime}', '${automation.query_to_run}')">
|
||||
<img class="automation-edit-icon ${automationId}" src="/static/assets/icons/pencil-edit.svg" onclick="onClickEditAutomationCard('${automationId}')" alt="Automations">
|
||||
</div>
|
||||
<input type="text"
|
||||
|
@ -405,11 +456,17 @@
|
|||
saveAutomationButtonEl.addEventListener("click", async () => { await saveAutomation(automation.id, isSuggested); });
|
||||
let deleteAutomationButtonEl = automationEl.querySelector(`#delete-automation-button-${automation.id}`);
|
||||
if (deleteAutomationButtonEl) {
|
||||
deleteAutomationButtonEl.addEventListener("click", () => { deleteAutomation(automation.id); });
|
||||
deleteAutomationButtonEl.addEventListener("click", () => {
|
||||
deleteAutomation(automation.id);
|
||||
document.getElementById('overlay').style.display = 'none';
|
||||
});
|
||||
}
|
||||
let cancelEditAutomationButtonEl = automationEl.querySelector(`#cancel-edit-automation-button-${automation.id}`);
|
||||
if (cancelEditAutomationButtonEl) {
|
||||
cancelEditAutomationButtonEl.addEventListener("click", (event) => { clickCancelEdit(event, automation.id); });
|
||||
cancelEditAutomationButtonEl.addEventListener("click", (event) => {
|
||||
clickCancelEdit(event, automation.id);
|
||||
document.getElementById('overlay').style.display = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
return automationEl.firstElementChild;
|
||||
|
@ -473,9 +530,29 @@
|
|||
}
|
||||
});
|
||||
});
|
||||
// Check if subject, crontime, query_to_run are all filled out. If so, show it as a populated suggested automation.
|
||||
const subject = "{{ subject }}";
|
||||
const crontime = "{{ crontime }}";
|
||||
const query = "{{ queryToRun }}";
|
||||
|
||||
if (subject && crontime && query) {
|
||||
const preFilledAutomation = createPreFilledAutomation(subject, crontime, query);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if ("{{ username }}" !== "None") {
|
||||
listAutomations();
|
||||
} else {
|
||||
// Check if subject, crontime, query_to_run are all filled out. If so, show it as a populated suggested automation.
|
||||
const subject = "{{ subject }}";
|
||||
const crontime = "{{ crontime }}";
|
||||
const query = "{{ queryToRun }}";
|
||||
|
||||
if (subject && crontime && query) {
|
||||
const preFilledAutomation = createPreFilledAutomation(subject, crontime, query);
|
||||
}
|
||||
}
|
||||
|
||||
if (suggestedAutomationsMetadata.length > 0) {
|
||||
suggestedAutomationsMetadata.forEach(automation => {
|
||||
|
@ -720,6 +797,12 @@
|
|||
}
|
||||
|
||||
async function saveAutomation(automationId, create=false) {
|
||||
if ("{{ username }}" == "None") {
|
||||
url_encoded_href = encodeURIComponent(window.location.href);
|
||||
window.location.href = `/login?next=${url_encoded_href}`;
|
||||
return;
|
||||
}
|
||||
|
||||
const scheduleEl = document.getElementById(`automation-schedule-${automationId}`);
|
||||
const notificationEl = document.getElementById(`automation-success-${automationId}`);
|
||||
const saveButtonEl = document.getElementById(`save-automation-button-${automationId}`);
|
||||
|
@ -879,5 +962,38 @@
|
|||
document.getElementById("automations").insertBefore(automationEl, document.getElementById("automations").firstChild);
|
||||
setupScheduleViewListener("* * * * *", placeholderId);
|
||||
})
|
||||
|
||||
function createPreFilledAutomation(subject, crontime, query) {
|
||||
document.getElementById('overlay').style.display = 'block';
|
||||
var automationEl = document.createElement("div");
|
||||
automationEl.classList.add("card");
|
||||
automationEl.classList.add("automation");
|
||||
automationEl.classList.add("new-automation")
|
||||
const placeholderId = Date.now();
|
||||
automationEl.classList.add(`${placeholderId}`);
|
||||
automationEl.id = "automation-card-" + placeholderId;
|
||||
var scheduleSelector = createScheduleSelector(placeholderId);
|
||||
automationEl.innerHTML = `
|
||||
<label for="subject">New Automation</label>
|
||||
<input type="text" id="automation-subject-${placeholderId}" value="${subject}">
|
||||
${scheduleSelector.outerHTML}
|
||||
<label for="query-to-run">What would you like to receive in your automation?</label>
|
||||
<textarea id="automation-queryToRun-${placeholderId}">${query}</textarea>
|
||||
<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);
|
||||
setupScheduleViewListener(crontime, placeholderId);
|
||||
}
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
|
@ -443,6 +443,7 @@ async def post_automation(
|
|||
request: Request,
|
||||
q: str,
|
||||
crontime: str,
|
||||
subject: Optional[str] = None,
|
||||
city: Optional[str] = None,
|
||||
region: Optional[str] = None,
|
||||
country: Optional[str] = None,
|
||||
|
@ -461,11 +462,13 @@ async def post_automation(
|
|||
q = q.strip()
|
||||
if not q.startswith("/automated_task"):
|
||||
query_to_run = f"/automated_task {q}"
|
||||
|
||||
# Normalize crontime for AP Scheduler CronTrigger
|
||||
crontime = crontime.strip()
|
||||
if len(crontime.split(" ")) > 5:
|
||||
# Truncate crontime to 5 fields
|
||||
crontime = " ".join(crontime.split(" ")[:5])
|
||||
|
||||
# Convert crontime to standard unix crontime
|
||||
crontime = crontime.replace("?", "*")
|
||||
|
||||
|
@ -477,6 +480,7 @@ async def post_automation(
|
|||
status_code=400,
|
||||
)
|
||||
|
||||
if not subject:
|
||||
subject = await acreate_title_from_query(q)
|
||||
|
||||
# Create new Conversation Session associated with this new task
|
||||
|
|
|
@ -95,6 +95,10 @@ async def delete_token(request: Request, token: str):
|
|||
async def auth(request: Request):
|
||||
form = await request.form()
|
||||
next_url = request.query_params.get("next", "/")
|
||||
for q in request.query_params:
|
||||
if not q == "next":
|
||||
next_url += f"&{q}={request.query_params[q]}"
|
||||
|
||||
credential = form.get("credential")
|
||||
|
||||
csrf_token_cookie = request.cookies.get("g_csrf_token")
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import json
|
||||
import os
|
||||
from datetime import timedelta
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse
|
||||
|
@ -421,20 +422,27 @@ def view_public_conversation(request: Request):
|
|||
|
||||
|
||||
@web_client.get("/automations", response_class=HTMLResponse)
|
||||
@requires(["authenticated"], redirect="login_page")
|
||||
def automations_config_page(request: Request):
|
||||
user = request.user.object
|
||||
def automations_config_page(
|
||||
request: Request,
|
||||
subject: Optional[str] = None,
|
||||
crontime: Optional[str] = None,
|
||||
queryToRun: Optional[str] = None,
|
||||
):
|
||||
user = request.user.object if request.user.is_authenticated else None
|
||||
user_picture = request.session.get("user", {}).get("picture")
|
||||
has_documents = EntryAdapters.user_has_entries(user=user)
|
||||
has_documents = EntryAdapters.user_has_entries(user=user) if user else False
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"config_automation.html",
|
||||
context={
|
||||
"request": request,
|
||||
"username": user.username,
|
||||
"username": user.username if user else None,
|
||||
"user_photo": user_picture,
|
||||
"is_active": has_required_scope(request, ["premium"]),
|
||||
"has_documents": has_documents,
|
||||
"khoj_version": state.khoj_version,
|
||||
"subject": subject if subject else "",
|
||||
"crontime": crontime if crontime else "",
|
||||
"queryToRun": queryToRun if queryToRun else "",
|
||||
},
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue