Create config page on web app to manage computer files indexed by Khoj

Remove the table of all files indexed by Khoj. This seems overkill and
doesn't match the UI semantics of the other data sources like Github,
Notion.

Create instead a data source card for computer files with the same
update, disable semantics of the Github and Notion data source cards

Users can disable each data source from its card on the main config page.

They can see/delete individual files indexed from the computer data source
once they click into the computer files data source card on the config page
This commit is contained in:
Debanjum Singh Solanky 2023-11-07 02:08:06 -08:00
parent d527b644f4
commit 6e957584ac
5 changed files with 165 additions and 116 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View file

@ -3,9 +3,40 @@
<div class="page"> <div class="page">
<div class="section"> <div class="section">
<h2 class="section-title">Plugins</h2> <h2 class="section-title">Content</h2>
<div class="section-cards"> <div class="section-cards">
<div class="card"> <div class="card">
<div class="card-title-row">
<img class="card-icon" src="/static/assets/icons/computer.png" alt="Computer">
<h3 class="card-title">
Files
{% if current_model_state.computer == True %}
<img id="configured-icon-github" class="configured-icon" src="/static/assets/icons/confirm-icon.svg" alt="Configured">
{% endif %}
</h3>
</div>
<div class="card-description-row">
<p class="card-description">Manage files from your computer</p>
</div>
<div class="card-action-row">
<a class="card-button" href="/config/content-source/computer">
{% if current_model_state.computer %}
Update
{% else %}
Setup
{% endif %}
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5l7 7-7 7"></path></svg>
</a>
</div>
{% if current_model_state.computer %}
<div id="clear-desktop" class="card-action-row">
<button class="card-button" onclick="clearContentType('computer')">
Disable
</button>
</div>
{% endif %}
</div>
<div class="card">
<div class="card-title-row"> <div class="card-title-row">
<img class="card-icon" src="/static/assets/icons/github.svg" alt="Github"> <img class="card-icon" src="/static/assets/icons/github.svg" alt="Github">
<h3 class="card-title"> <h3 class="card-title">
@ -47,11 +78,11 @@
</h3> </h3>
</div> </div>
<div class="card-description-row"> <div class="card-description-row">
<p class="card-description">Configure your settings from Notion</p> <p class="card-description">Sync your Notion pages</p>
</div> </div>
<div class="card-action-row"> <div class="card-action-row">
<a class="card-button" href="/config/content-source/notion"> <a class="card-button" href="/config/content-source/notion">
{% if current_model_state.content %} {% if current_model_state.notion %}
Update Update
{% else %} {% else %}
Setup Setup
@ -77,7 +108,7 @@
<div class="card-title-row"> <div class="card-title-row">
<img class="card-icon" src="/static/assets/icons/chat.svg" alt="Chat"> <img class="card-icon" src="/static/assets/icons/chat.svg" alt="Chat">
<h3 class="card-title"> <h3 class="card-title">
Chat Model Chat
</h3> </h3>
</div> </div>
<div class="card-description-row"> <div class="card-description-row">
@ -122,16 +153,6 @@
</div> </div>
</div> </div>
</div> </div>
<div class="section">
<h2 class="section-title">Manage Data</h2>
<div class="section-manage-files">
<div id="delete-all-files" class="delete-all=files">
<button id="delete-all-files" type="submit" title="Delete all indexed files">🗑️ Remove All</button>
</div>
<div class="indexed-files">
</div>
</div>
</div>
<div class="section general-settings"> <div class="section general-settings">
<div id="results-count" title="Number of items to show in search and use for chat response"> <div id="results-count" title="Number of items to show in search and use for chat response">
<label for="results-count-slider">Results Count: <span id="results-count-value">5</span></label> <label for="results-count-slider">Results Count: <span id="results-count-value">5</span></label>
@ -363,70 +384,5 @@
} }
}) })
} }
// Get all currently indexed files
function getAllFilenames() {
fetch('/api/config/data/all')
.then(response => response.json())
.then(data => {
var indexedFiles = document.getElementsByClassName("indexed-files")[0];
indexedFiles.innerHTML = "";
if (data.length == 0) {
document.getElementById("delete-all-files").style.display = "none";
indexedFiles.innerHTML = "<div>Use the <a href='https://download.khoj.dev'>Khoj Desktop client</a> to index files.</div>";
} else {
document.getElementById("delete-all-files").style.display = "block";
}
for (var filename of data) {
let fileElement = document.createElement("div");
fileElement.classList.add("file-element");
let fileNameElement = document.createElement("div");
fileNameElement.classList.add("content-name");
fileNameElement.innerHTML = filename;
fileElement.appendChild(fileNameElement);
let buttonContainer = document.createElement("div");
buttonContainer.classList.add("remove-button-container");
let removeFileButton = document.createElement("button");
removeFileButton.classList.add("remove-file-button");
removeFileButton.innerHTML = "🗑️";
removeFileButton.addEventListener("click", ((filename) => {
return () => {
removeFile(filename);
};
})(filename));
buttonContainer.appendChild(removeFileButton);
fileElement.appendChild(buttonContainer);
indexedFiles.appendChild(fileElement);
}
})
.catch((error) => {
console.error('Error:', error);
});
}
// Get all currently indexed files on page load
getAllFilenames();
let deleteAllFilesButton = document.getElementById("delete-all-files");
deleteAllFilesButton.addEventListener("click", function(event) {
event.preventDefault();
fetch('/api/config/data/all', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.status == "ok") {
getAllFilenames();
}
})
});
</script> </script>
{% endblock %} {% endblock %}

View file

@ -0,0 +1,107 @@
{% extends "base_config.html" %}
{% block content %}
<div class="page">
<div class="section">
<h2 class="section-title">
<img class="card-icon" src="/static/assets/icons/computer.png" alt="files">
<span class="card-title-text">Files</span>
<div class="instructions">
<p class="card-description">Manage files from your computer</p>
<p id="desktop-client" class="card-description">Download the <a href="https://download.khoj.dev">Khoj Desktop app</a> to sync files from your computer</p>
</div>
</h2>
<div class="section-manage-files">
<div id="delete-all-files" class="delete-all-files">
<button id="delete-all-files" type="submit" title="Remove all computer files from Khoj">🗑️ Delete all</button>
</div>
<div class="indexed-files">
</div>
</div>
</div>
</div>
<style>
#desktop-client {
font-weight: normal;
}
</style>
<script>
function removeFile(path) {
fetch('/api/config/data/file?filename=' + path, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.ok ? response.json() : Promise.reject(response))
.then(data => {
if (data.status == "ok") {
getAllComputerFilenames();
}
})
}
// Get all currently indexed files
function getAllComputerFilenames() {
fetch('/api/config/data/computer')
.then(response => response.json())
.then(data => {
var indexedFiles = document.getElementsByClassName("indexed-files")[0];
indexedFiles.innerHTML = "";
if (data.length == 0) {
document.getElementById("delete-all-files").style.display = "none";
indexedFiles.innerHTML = "<div class='card-description'>Use the <a href='https://download.khoj.dev'>Khoj Desktop client</a> to index files.</div>";
} else {
document.getElementById("delete-all-files").style.display = "block";
}
for (var filename of data) {
let fileElement = document.createElement("div");
fileElement.classList.add("file-element");
let fileNameElement = document.createElement("div");
fileNameElement.classList.add("content-name");
fileNameElement.innerHTML = filename;
fileElement.appendChild(fileNameElement);
let buttonContainer = document.createElement("div");
buttonContainer.classList.add("remove-button-container");
let removeFileButton = document.createElement("button");
removeFileButton.classList.add("remove-file-button");
removeFileButton.innerHTML = "🗑️";
removeFileButton.addEventListener("click", ((filename) => {
return () => {
removeFile(filename);
};
})(filename));
buttonContainer.appendChild(removeFileButton);
fileElement.appendChild(buttonContainer);
indexedFiles.appendChild(fileElement);
}
})
.catch((error) => {
console.error('Error:', error);
});
}
// Get all currently indexed files on page load
getAllComputerFilenames();
let deleteAllComputerFilesButton = document.getElementById("delete-all-files");
deleteAllComputerFilesButton.addEventListener("click", function(event) {
event.preventDefault();
fetch('/api/config/data/content-source/computer', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.status == "ok") {
getAllComputerFilenames();
}
})
});
</script>
{% endblock %}

View file

@ -270,10 +270,11 @@ async def remove_file_data(
return {"status": "ok"} return {"status": "ok"}
@api.get("/config/data/all", response_model=List[str]) @api.get("/config/data/{content_source}", response_model=List[str])
@requires(["authenticated"]) @requires(["authenticated"])
async def get_all_filenames( async def get_all_filenames(
request: Request, request: Request,
content_source: str,
client: Optional[str] = None, client: Optional[str] = None,
): ):
user = request.user.object user = request.user.object
@ -285,27 +286,7 @@ async def get_all_filenames(
client=client, client=client,
) )
return await sync_to_async(list)(EntryAdapters.aget_all_filenames(user)) return await sync_to_async(list)(EntryAdapters.aget_all_filenames_by_source(user, content_source))
@api.delete("/config/data/all", status_code=200)
@requires(["authenticated"])
async def remove_all_config_data(
request: Request,
client: Optional[str] = None,
):
user = request.user.object
update_telemetry_state(
request=request,
telemetry_type="api",
api="delete_all_config",
client=client,
)
await EntryAdapters.adelete_all_entries(user)
return {"status": "ok"}
@api.post("/config/data/conversation/model", status_code=200) @api.post("/config/data/conversation/model", status_code=200)

View file

@ -110,25 +110,14 @@ def login_page(request: Request):
def config_page(request: Request): def config_page(request: Request):
user = request.user.object user = request.user.object
user_picture = request.session.get("user", {}).get("picture") user_picture = request.session.get("user", {}).get("picture")
enabled_content = set(EntryAdapters.get_unique_file_types(user).all()) enabled_content_source = set(EntryAdapters.get_unique_file_source(user).all())
successfully_configured = { successfully_configured = {
"pdf": ("pdf" in enabled_content), "computer": ("computer" in enabled_content_source),
"markdown": ("markdown" in enabled_content), "github": ("github" in enabled_content_source),
"org": ("org" in enabled_content), "notion": ("notion" in enabled_content_source),
"image": False,
"github": ("github" in enabled_content),
"notion": ("notion" in enabled_content),
"plaintext": ("plaintext" in enabled_content),
} }
if state.content_index:
successfully_configured.update(
{
"image": state.content_index.image is not None,
}
)
conversation_options = ConversationAdapters.get_conversation_processor_options().all() conversation_options = ConversationAdapters.get_conversation_processor_options().all()
all_conversation_options = list() all_conversation_options = list()
for conversation_option in conversation_options: for conversation_option in conversation_options:
@ -209,3 +198,19 @@ def notion_config_page(request: Request):
"user_photo": user_picture, "user_photo": user_picture,
}, },
) )
@web_client.get("/config/content-source/computer", response_class=HTMLResponse)
@requires(["authenticated"], redirect="login_page")
def computer_config_page(request: Request):
user = request.user.object
user_picture = request.session.get("user", {}).get("picture")
return templates.TemplateResponse(
"content_source_computer_input.html",
context={
"request": request,
"username": user.username,
"user_photo": user_picture,
},
)