diff --git a/src/interface/desktop/main.js b/src/interface/desktop/main.js
index d561a2d5..1610a5e7 100644
--- a/src/interface/desktop/main.js
+++ b/src/interface/desktop/main.js
@@ -351,15 +351,28 @@ async function deleteAllFiles () {
}
}
+// Fetch user info from Khoj server
+async function getUserInfo() {
+ const getUserInfoURL = `${store.get('hostURL') || KHOJ_URL}/api/v1/user?client=desktop`;
+ const headers = { 'Authorization': `Bearer ${store.get('khojToken')}` };
+ try {
+ let response = await axios.get(getUserInfoURL, { headers });
+ return response.data;
+ } catch (err) {
+ console.error(err);
+ }
+}
let firstRun = true;
let win = null;
+let titleBarStyle = process.platform === 'win32' ? 'default' : 'hidden';
const createWindow = (tab = 'chat.html') => {
win = new BrowserWindow({
width: 800,
height: 800,
show: false,
- titleBarStyle: 'hidden',
+ titleBarStyle: titleBarStyle,
+ autoHideMenuBar: true,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
nodeIntegration: true,
@@ -464,13 +477,15 @@ app.whenReady().then(() => {
ipcMain.handle('setToken', setToken);
ipcMain.handle('getToken', getToken);
+ ipcMain.handle('getUserInfo', getUserInfo);
ipcMain.handle('syncData', (event, regenerate) => {
syncData(regenerate);
});
ipcMain.handle('deleteAllFiles', deleteAllFiles);
- createWindow()
+ createWindow();
+
app.setAboutPanelOptions({
applicationName: "Khoj",
@@ -515,7 +530,8 @@ function openAboutWindow() {
aboutWindow = new BrowserWindow({
width: 400,
height: 400,
- titleBarStyle: 'hidden',
+ titleBarStyle: titleBarStyle,
+ autoHideMenuBar: true,
show: false,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
diff --git a/src/interface/desktop/preload.js b/src/interface/desktop/preload.js
index 8d5152b7..24dc9919 100644
--- a/src/interface/desktop/preload.js
+++ b/src/interface/desktop/preload.js
@@ -53,6 +53,10 @@ contextBridge.exposeInMainWorld('syncDataAPI', {
deleteAllFiles: () => ipcRenderer.invoke('deleteAllFiles')
})
+contextBridge.exposeInMainWorld('userInfoAPI', {
+ getUserInfo: () => ipcRenderer.invoke('getUserInfo')
+})
+
contextBridge.exposeInMainWorld('tokenAPI', {
setToken: (token) => ipcRenderer.invoke('setToken', token),
getToken: () => ipcRenderer.invoke('getToken')
diff --git a/src/interface/desktop/search.html b/src/interface/desktop/search.html
index ffc1bf64..9a3e0e0b 100644
--- a/src/interface/desktop/search.html
+++ b/src/interface/desktop/search.html
@@ -246,6 +246,16 @@
window.history.pushState({}, "", url.href);
}
+ window.addEventListener("DOMContentLoaded", async() => {
+ // Setup the header pane
+ document.getElementById("khoj-header").innerHTML = await populateHeaderPane();
+ // Setup the nav menu
+ document.getElementById("profile-picture").addEventListener("click", toggleNavMenu);
+ // Set the active nav pane
+ document.getElementById("search-nav")?.classList.add("khoj-nav-selected");
+ })
+
+
window.addEventListener("load", async function() {
// Dynamically populate type dropdown based on enabled content types and type passed as URL query parameter
await populate_type_dropdown();
@@ -259,16 +269,7 @@
-
+
diff --git a/src/interface/desktop/utils.js b/src/interface/desktop/utils.js
index 8f9c0aeb..df05f1c0 100644
--- a/src/interface/desktop/utils.js
+++ b/src/interface/desktop/utils.js
@@ -24,3 +24,59 @@ window.appInfoAPI.getInfo((_, info) => {
khojTitleElement.innerHTML = '
Khoj for ' + (info.platform === 'win32' ? 'Windows' : info.platform === 'darwin' ? 'macOS' : 'Linux') + '';
}
});
+
+function toggleNavMenu() {
+ let menu = document.getElementById("khoj-nav-menu");
+ menu.classList.toggle("show");
+}
+
+// Close the dropdown menu if the user clicks outside of it
+document.addEventListener('click', function(event) {
+ let menu = document.getElementById("khoj-nav-menu");
+ let menuContainer = document.getElementById("khoj-nav-menu-container");
+ let isClickOnMenu = menuContainer.contains(event.target) || menuContainer === event.target;
+ if (isClickOnMenu === false && menu.classList.contains("show")) {
+ menu.classList.remove("show");
+ }
+});
+
+async function populateHeaderPane() {
+ let userInfo = null;
+ try {
+ userInfo = await window.userInfoAPI.getUserInfo();
+ } catch (error) {
+ console.log("User not logged in");
+ }
+
+ let username = userInfo?.username ?? "?";
+ let user_photo = userInfo?.photo;
+ let is_active = userInfo?.is_active;
+ let has_documents = userInfo?.has_documents;
+
+ // Populate the header element with the navigation pane
+ return `
+
+
+
+
+ `;
+}
diff --git a/src/khoj/database/adapters/__init__.py b/src/khoj/database/adapters/__init__.py
index 72e2a360..95a75b7d 100644
--- a/src/khoj/database/adapters/__init__.py
+++ b/src/khoj/database/adapters/__init__.py
@@ -196,6 +196,17 @@ def get_user_name(user: KhojUser):
return None
+def get_user_photo(user: KhojUser):
+ full_name = user.get_full_name()
+ if not is_none_or_empty(full_name):
+ return full_name
+ google_profile: GoogleUser = GoogleUser.objects.filter(user=user).first()
+ if google_profile:
+ return google_profile.picture
+
+ return None
+
+
def get_user_subscription(email: str) -> Optional[Subscription]:
return Subscription.objects.filter(user__email=email).first()
@@ -796,7 +807,7 @@ class EntryAdapters:
return await Entry.objects.filter(user=user, file_path=file_path).adelete()
@staticmethod
- def aget_all_filenames_by_source(user: KhojUser, file_source: str):
+ def get_all_filenames_by_source(user: KhojUser, file_source: str):
return (
Entry.objects.filter(user=user, file_source=file_source)
.distinct("file_path")
diff --git a/src/khoj/interface/web/assets/icons/agents.svg b/src/khoj/interface/web/assets/icons/agents.svg
new file mode 100644
index 00000000..f3974293
--- /dev/null
+++ b/src/khoj/interface/web/assets/icons/agents.svg
@@ -0,0 +1,6 @@
+
diff --git a/src/khoj/interface/web/assets/icons/chat.svg b/src/khoj/interface/web/assets/icons/chat.svg
index ca42280c..a5cbf3af 100644
--- a/src/khoj/interface/web/assets/icons/chat.svg
+++ b/src/khoj/interface/web/assets/icons/chat.svg
@@ -1,693 +1,24 @@
-
-
-
-
+
diff --git a/src/khoj/interface/web/assets/icons/copy-button-success.svg b/src/khoj/interface/web/assets/icons/copy-button-success.svg
new file mode 100644
index 00000000..7a00dcd0
--- /dev/null
+++ b/src/khoj/interface/web/assets/icons/copy-button-success.svg
@@ -0,0 +1,6 @@
+
+
diff --git a/src/khoj/interface/web/assets/icons/copy-button.svg b/src/khoj/interface/web/assets/icons/copy-button.svg
new file mode 100644
index 00000000..36fc8971
--- /dev/null
+++ b/src/khoj/interface/web/assets/icons/copy-button.svg
@@ -0,0 +1,5 @@
+
+
diff --git a/src/khoj/interface/web/assets/icons/copy_button.svg b/src/khoj/interface/web/assets/icons/copy_button.svg
deleted file mode 100644
index fadf344a..00000000
--- a/src/khoj/interface/web/assets/icons/copy_button.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/src/khoj/interface/web/assets/icons/search.svg b/src/khoj/interface/web/assets/icons/search.svg
new file mode 100644
index 00000000..f0e463f2
--- /dev/null
+++ b/src/khoj/interface/web/assets/icons/search.svg
@@ -0,0 +1,25 @@
+
diff --git a/src/khoj/interface/web/assets/khoj.css b/src/khoj/interface/web/assets/khoj.css
index 3d7e7d4a..3c3536a7 100644
--- a/src/khoj/interface/web/assets/khoj.css
+++ b/src/khoj/interface/web/assets/khoj.css
@@ -76,6 +76,9 @@
padding: 16px 0;
margin: 0;
}
+.khoj-header {
+ align-items: center;
+}
.khoj-footer {
margin: 16px 0 0 0;
}
@@ -112,6 +115,18 @@ a.khoj-logo {
.khoj-nav-selected {
background-color: var(--primary);
}
+.nav-icon {
+ width: 24px;
+ height: 24px;
+}
+#agents-icon {
+ width: 30px;
+ height: 30px;
+}
+span.khoj-nav-item-text {
+ padding-top: 6px;
+ padding-left: 8px;
+}
img.khoj-logo {
width: min(60vw, 90px);
max-width: 100%;
@@ -177,7 +192,8 @@ img.khoj-logo {
font-size: 20px;
box-sizing: unset;
width: 40px;
- height: 40px;
+ height: 35px;
+ padding-top: 8px
}
.subscribed {
border: 3px solid var(--primary-hover);
@@ -212,4 +228,10 @@ img.khoj-logo {
grid-gap: 0px;
justify-content: space-between;
}
+ a.khoj-nav {
+ padding: 0 16px;
+ }
+ span.khoj-nav-item-text {
+ display: none;
+ }
}
diff --git a/src/khoj/interface/web/chat.html b/src/khoj/interface/web/chat.html
index e5c0ce1b..87d42fd6 100644
--- a/src/khoj/interface/web/chat.html
+++ b/src/khoj/interface/web/chat.html
@@ -30,20 +30,21 @@ To get started, just start typing below. You can also type / to see a list of co
const allowedExtensions = ['text/org', 'text/markdown', 'text/plain', 'text/html', 'application/pdf'];
const allowedFileEndings = ['org', 'md', 'txt', 'html', 'pdf'];
let chatOptions = [];
- function copyProgrammaticOutput(event) {
- // Remove the first 4 characters which are the "Copy" button
+ function copyParentText(event) {
const button = event.currentTarget;
- const programmaticOutput = button.parentNode.textContent.trim();
- navigator.clipboard.writeText(programmaticOutput).then(() => {
- button.textContent = "✅ Copied to clipboard!";
+ const textContent = button.parentNode.textContent.trim();
+ navigator.clipboard.writeText(textContent).then(() => {
+ button.firstChild.src = "/static/assets/icons/copy-button-success.svg";
setTimeout(() => {
- button.textContent = "✅";
+ button.firstChild.src = "/static/assets/icons/copy-button.svg";
}, 1000);
}).catch((error) => {
console.error("Error copying programmatic output to clipboard:", error);
- button.textContent = "⛔️ Failed to copy!";
+ const originalButtonText = button.innerHTML;
+ button.innerHTML = "⛔️";
setTimeout(() => {
- button.textContent = "⛔️";
+ button.innerHTML = originalButtonText;
+ button.firstChild.src = "/static/assets/icons/copy-button.svg";
}, 1000);
});
}
@@ -323,7 +324,7 @@ To get started, just start typing below. You can also type / to see a list of co
renderMessage(message, by, dt, references);
}
- function formatHTMLMessage(htmlMessage, raw=false) {
+ function formatHTMLMessage(htmlMessage, raw=false, willReplace=true) {
var md = window.markdownit();
let newHTML = htmlMessage;
@@ -348,6 +349,19 @@ To get started, just start typing below. You can also type / to see a list of co
element.innerHTML = newHTML;
element.className = "chat-message-text-response";
+ // Add a copy button to each chat message, if it doesn't already exist
+ if (willReplace === true) {
+ let copyButton = document.createElement('button');
+ copyButton.classList.add("copy-button");
+ copyButton.title = "Copy Message";
+ let copyIcon = document.createElement("img");
+ copyIcon.src = "/static/assets/icons/copy-button.svg";
+ copyIcon.classList.add("copy-icon");
+ copyButton.appendChild(copyIcon);
+ copyButton.addEventListener('click', copyParentText);
+ element.append(copyButton);
+ }
+
// Get any elements with a class that starts with "language"
let codeBlockElements = element.querySelectorAll('[class^="language-"]');
// For each element, add a parent div with the class "programmatic-output"
@@ -359,15 +373,18 @@ To get started, just start typing below. You can also type / to see a list of co
codeElement.parentNode.insertBefore(parentDiv, codeElement);
// Move the code element into the parent div
parentDiv.appendChild(codeElement);
- // Add a copy button to each element
- let copyButton = document.createElement('button');
- copyButton.classList.add("copy-button");
- let copyIcon = document.createElement("img");
- copyIcon.src = "/static/assets/icons/copy_button.svg";
- copyIcon.classList.add("copy-icon");
- copyButton.appendChild(copyIcon);
- copyButton.addEventListener('click', copyProgrammaticOutput);
- codeElement.prepend(copyButton);
+ // Add a copy button to each code block, if it doesn't already exist
+ if (willReplace === true) {
+ let copyButton = document.createElement('button');
+ copyButton.classList.add("copy-button");
+ copyButton.title = "Copy Code";
+ let copyIcon = document.createElement("img");
+ copyIcon.src = "/static/assets/icons/copy-button.svg";
+ copyIcon.classList.add("copy-icon");
+ copyButton.appendChild(copyIcon);
+ copyButton.addEventListener('click', copyParentText);
+ codeElement.prepend(copyButton);
+ }
});
// Get all code elements that have no class.
@@ -573,7 +590,7 @@ To get started, just start typing below. You can also type / to see a list of co
if (replace) {
newResponseElement.innerHTML = "";
}
- newResponseElement.appendChild(formatHTMLMessage(rawResponse));
+ newResponseElement.appendChild(formatHTMLMessage(rawResponse, false, replace));
document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight;
}
@@ -1098,7 +1115,6 @@ To get started, just start typing below. You can also type / to see a list of co
agentMetadataElement.style.display = "none";
}
- let chatBodyWrapper = document.getElementById("chat-body-wrapper");
const fullChatLog = response.chat || [];
fullChatLog.forEach(chat_log => {
@@ -1116,6 +1132,7 @@ To get started, just start typing below. You can also type / to see a list of co
});
// Add fade out animation to loading screen and remove it after the animation ends
+ let chatBodyWrapper = document.getElementById("chat-body-wrapper");
chatBodyWrapperHeight = chatBodyWrapper.clientHeight;
chatBody.style.height = chatBodyWrapperHeight;
setTimeout(() => {
@@ -2164,7 +2181,7 @@ To get started, just start typing below. You can also type / to see a list of co
}
button.copy-button:hover {
- background-color: black;
+ background-color: var(--primary-hover);
color: #f5f5f5;
}
@@ -2411,7 +2428,6 @@ To get started, just start typing below. You can also type / to see a list of co
width: 80px;
height: 80px;
}
-
.loading-spinner div {
position: absolute;
border: 4px solid var(--primary-hover);
@@ -2419,10 +2435,30 @@ To get started, just start typing below. You can also type / to see a list of co
border-radius: 50%;
animation: lds-ripple 0.5s cubic-bezier(0, 0.2, 0.8, 1) infinite;
}
-
.loading-spinner div:nth-child(2) {
animation-delay: -0.5s;
}
+ @keyframes lds-ripple {
+ 0% {
+ top: 36px;
+ left: 36px;
+ width: 0;
+ height: 0;
+ opacity: 1;
+ border-color: var(--primary-hover);
+ }
+ 50% {
+ border-color: var(--flower);
+ }
+ 100% {
+ top: 0px;
+ left: 0px;
+ width: 72px;
+ height: 72px;
+ opacity: 0;
+ border-color: var(--water);
+ }
+ }
#agent-metadata-content {
display: grid;
@@ -2588,28 +2624,6 @@ To get started, just start typing below. You can also type / to see a list of co
}
- @keyframes lds-ripple {
- 0% {
- top: 36px;
- left: 36px;
- width: 0;
- height: 0;
- opacity: 1;
- border-color: var(--primary-hover);
- }
- 50% {
- border-color: var(--flower);
- }
- 100% {
- top: 0px;
- left: 0px;
- width: 72px;
- height: 72px;
- opacity: 0;
- border-color: var(--water);
- }
- }
-
.lds-ellipsis {
display: inline-block;
position: relative;
diff --git a/src/khoj/interface/web/utils.html b/src/khoj/interface/web/utils.html
index 8a33be89..82bc6518 100644
--- a/src/khoj/interface/web/utils.html
+++ b/src/khoj/interface/web/utils.html
@@ -4,9 +4,16 @@