Update the config UI to show all files indexed with option to delete

- Given the separation of the client and server now, the web UI will no longer support configuration of local file paths of data to index
- Expose a way to show all the files that are currently set for indexing, along with an option to delete all or specific files
This commit is contained in:
sabaimran
2023-11-04 19:03:34 -07:00
parent 800bb4f458
commit fdfab39942
4 changed files with 226 additions and 158 deletions

View File

@@ -291,7 +291,10 @@ class EntryAdapters:
return deleted_count return deleted_count
@staticmethod @staticmethod
def delete_all_entries(user: KhojUser, file_type: str): def delete_all_entries(user: KhojUser, file_type: str = None):
if file_type is None:
deleted_count, _ = Entry.objects.filter(user=user).delete()
else:
deleted_count, _ = Entry.objects.filter(user=user, file_type=file_type).delete() deleted_count, _ = Entry.objects.filter(user=user, file_type=file_type).delete()
return deleted_count return deleted_count
@@ -314,6 +317,18 @@ class EntryAdapters:
async def user_has_entries(user: KhojUser): async def user_has_entries(user: KhojUser):
return await Entry.objects.filter(user=user).aexists() return await Entry.objects.filter(user=user).aexists()
@staticmethod
async def adelete_entry_by_file(user: KhojUser, file_path: str):
return await Entry.objects.filter(user=user, file_path=file_path).adelete()
@staticmethod
def aget_all_filenames(user: KhojUser):
return Entry.objects.filter(user=user).distinct("file_path").values_list("file_path", flat=True)
@staticmethod
async def adelete_all_entries(user: KhojUser):
return await Entry.objects.filter(user=user).adelete()
@staticmethod @staticmethod
def apply_filters(user: KhojUser, query: str, file_type_filter: str = None): def apply_filters(user: KhojUser, query: str, file_type_filter: str = None):
q_filter_terms = Q() q_filter_terms = Q()

View File

@@ -53,10 +53,10 @@
justify-self: center; justify-self: center;
} }
.api-settings { div.section-manage-files,
div.api-settings {
display: grid; display: grid;
grid-template-columns: 1fr; grid-template-columns: 1fr;
grid-template-rows: 1fr 1fr auto;
justify-items: start; justify-items: start;
gap: 8px; gap: 8px;
padding: 24px 24px; padding: 24px 24px;
@@ -65,9 +65,19 @@
border-radius: 4px; border-radius: 4px;
box-shadow: 0px 1px 3px 0px rgba(0,0,0,0.1),0px 1px 2px -1px rgba(0,0,0,0.8); box-shadow: 0px 1px 3px 0px rgba(0,0,0,0.1),0px 1px 2px -1px rgba(0,0,0,0.8);
} }
div.section-manage-files {
width: 640px;
}
div.api-settings {
grid-template-rows: 1fr 1fr auto;
}
#api-settings-card-description { #api-settings-card-description {
margin: 8px 0 0 0; margin: 8px 0 0 0;
} }
#api-settings-keys-table { #api-settings-keys-table {
margin-bottom: 16px; margin-bottom: 16px;
} }
@@ -184,6 +194,37 @@
text-align: left; text-align: left;
} }
button.remove-file-button:hover {
background-color: rgb(255 235 235);
border-radius: 3px;
border: none;
color: var(--flower);
padding: 4px;
cursor: pointer;
}
button.remove-file-button {
background-color: rgb(253 214 214);
border-radius: 3px;
border: none;
color: var(--flower);
padding: 4px;
}
div.file-element {
display: grid;
grid-template-columns: 1fr auto;
border: 1px solid rgb(229, 229, 229);
border-radius: 4px;
box-shadow: 0px 1px 3px 0px rgba(0,0,0,0.1),0px 1px 2px -1px rgba(0,0,0,0.8);
padding: 4px;
margin-bottom: 8px;
}
div.remove-button-container {
text-align: right;
}
button.card-button.happy { button.card-button.happy {
color: var(--leaf); color: var(--leaf);
} }
@@ -246,6 +287,11 @@
cursor: pointer; cursor: pointer;
} }
a {
color: #3b82f6;
text-decoration: none;
}
@media screen and (max-width: 700px) { @media screen and (max-width: 700px) {
.section-cards { .section-cards {
grid-template-columns: 1fr; grid-template-columns: 1fr;
@@ -255,7 +301,7 @@
body { body {
display: grid; display: grid;
grid-template-columns: 1fr; grid-template-columns: 1fr;
grid-template-rows: 1fr auto auto auto minmax(80px, 100%); grid-template-rows: 1fr auto auto auto auto;
} }
body > * { body > * {
grid-column: 1; grid-column: 1;
@@ -281,9 +327,14 @@
grid-template-columns: auto; grid-template-columns: auto;
} }
div.section-manage-files,
div.api-settings { div.api-settings {
width: auto; width: auto;
} }
div.finalize-buttons {
padding: 0;
}
} }
</style> </style>
</html> </html>

View File

@@ -67,130 +67,6 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
<div class="card">
<div class="card-title-row">
<img class="card-icon" src="/static/assets/icons/markdown.svg" alt="markdown">
<h3 class="card-title">
Markdown
{% if current_model_state.markdown == True%}
<img id="configured-icon-markdown" class="configured-icon" src="/static/assets/icons/confirm-icon.svg" alt="Configured">
{% endif %}
</h3>
</div>
<div class="card-description-row">
<p class="card-description">Set markdown files to index</p>
</div>
<div class="card-action-row">
<a class="card-button" href="/config/content_type/markdown">
{% if current_model_state.markdown %}
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.markdown %}
<div id="clear-markdown" class="card-action-row">
<button class="card-button" onclick="clearContentType('markdown')">
Disable
</button>
</div>
{% endif %}
</div>
<div class="card">
<div class="card-title-row">
<img class="card-icon" src="/static/assets/icons/org.svg" alt="org">
<h3 class="card-title">
Org
{% if current_model_state.org == True %}
<img id="configured-icon-org" class="configured-icon" src="/static/assets/icons/confirm-icon.svg" alt="Configured">
{% endif %}
</h3>
</div>
<div class="card-description-row">
<p class="card-description">Set org files to index</p>
</div>
<div class="card-action-row">
<a class="card-button" href="/config/content_type/org">
{% if current_model_state.org %}
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.org %}
<div id="clear-org" class="card-action-row">
<button class="card-button" onclick="clearContentType('org')">
Disable
</button>
</div>
{% endif %}
</div>
<div class="card">
<div class="card-title-row">
<img class="card-icon" src="/static/assets/icons/pdf.svg" alt="PDF">
<h3 class="card-title">
PDF
{% if current_model_state.pdf == True %}
<img id="configured-icon-pdf" class="configured-icon" src="/static/assets/icons/confirm-icon.svg" alt="Configured">
{% endif %}
</h3>
</div>
<div class="card-description-row">
<p class="card-description">Set PDF files to index</p>
</div>
<div class="card-action-row">
<a class="card-button" href="/config/content_type/pdf">
{% if current_model_state.pdf %}
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.pdf %}
<div id="clear-pdf" class="card-action-row">
<button class="card-button" onclick="clearContentType('pdf')">
Disable
</button>
</div>
{% endif %}
</div>
<div class="card">
<div class="card-title-row">
<img class="card-icon" src="/static/assets/icons/plaintext.svg" alt="Plaintext">
<h3 class="card-title">
Plaintext
{% if current_model_state.plaintext == True %}
<img id="configured-icon-plaintext" class="configured-icon" src="/static/assets/icons/confirm-icon.svg" alt="Configured">
{% endif %}
</h3>
</div>
<div class="card-description-row">
<p class="card-description">Set Plaintext files to index</p>
</div>
<div class="card-action-row">
<a class="card-button" href="/config/content_type/plaintext">
{% if current_model_state.plaintext %}
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.plaintext %}
<div id="clear-plaintext" class="card-action-row">
<button class="card-button" onclick="clearContentType('plaintext')">
Disable
</button>
</div>
{% endif %}
</div>
</div> </div>
</div> </div>
<div class="section"> <div class="section">
@@ -246,6 +122,16 @@
</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>
@@ -291,8 +177,8 @@
}; };
function clearContentType(content_type) { function clearContentType(content_type) {
fetch('/api/delete/config/data/content_type/' + content_type, { fetch('/api/config/data/content_type/' + content_type, {
method: 'POST', method: 'DELETE',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
} }
@@ -462,5 +348,84 @@
// List user's API keys on page load // List user's API keys on page load
listApiKeys(); listApiKeys();
function removeFile(path) {
fetch('/api/config/data/file?filename=' + path, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.status == "ok") {
getAllFilenames();
}
})
}
// 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

@@ -45,7 +45,15 @@ from fastapi.requests import Request
from database import adapters from database import adapters
from database.adapters import EntryAdapters, ConversationAdapters from database.adapters import EntryAdapters, ConversationAdapters
from database.models import LocalMarkdownConfig, LocalOrgConfig, LocalPdfConfig, LocalPlaintextConfig, KhojUser from database.models import (
LocalMarkdownConfig,
LocalOrgConfig,
LocalPdfConfig,
LocalPlaintextConfig,
KhojUser,
GithubConfig,
NotionConfig,
)
# Initialize Router # Initialize Router
@@ -54,14 +62,10 @@ logger = logging.getLogger(__name__)
def map_config_to_object(content_type: str): def map_config_to_object(content_type: str):
if content_type == "org": if content_type == "github":
return LocalOrgConfig return GithubConfig
if content_type == "markdown": if content_type == "notion":
return LocalMarkdownConfig return NotionConfig
if content_type == "pdf":
return LocalPdfConfig
if content_type == "plaintext":
return LocalPlaintextConfig
async def map_config_to_db(config: FullConfig, user: KhojUser): async def map_config_to_db(config: FullConfig, user: KhojUser):
@@ -215,7 +219,7 @@ async def set_content_config_notion_data(
return {"status": "ok"} return {"status": "ok"}
@api.post("/delete/config/data/content_type/{content_type}", status_code=200) @api.delete("/config/data/content_type/{content_type}", status_code=200)
@requires(["authenticated"]) @requires(["authenticated"])
async def remove_content_config_data( async def remove_content_config_data(
request: Request, request: Request,
@@ -243,29 +247,62 @@ async def remove_content_config_data(
return {"status": "ok"} return {"status": "ok"}
@api.post("/config/data/content_type/{content_type}", status_code=200) @api.delete("/config/data/file", status_code=200)
@requires(["authenticated"]) @requires(["authenticated"])
async def set_content_config_data( async def remove_file_data(
request: Request, request: Request,
content_type: str, filename: str,
updated_config: Union[TextContentConfig, None],
client: Optional[str] = None, client: Optional[str] = None,
): ):
_initialize_config()
user = request.user.object user = request.user.object
content_object = map_config_to_object(content_type)
await adapters.set_text_content_config(user, content_object, updated_config)
update_telemetry_state( update_telemetry_state(
request=request, request=request,
telemetry_type="api", telemetry_type="api",
api="set_content_config", api="delete_file",
client=client, client=client,
metadata={"content_type": content_type},
) )
await EntryAdapters.adelete_entry_by_file(user, filename)
return {"status": "ok"}
@api.get("/config/data/all", response_model=List[str])
@requires(["authenticated"])
async def get_all_filenames(
request: Request,
client: Optional[str] = None,
):
user = request.user.object
update_telemetry_state(
request=request,
telemetry_type="api",
api="get_all_filenames",
client=client,
)
return await sync_to_async(list)(EntryAdapters.aget_all_filenames(user))
@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"} return {"status": "ok"}