Fix and Improve Chat UI in Web, Desktop apps (#655)

### Improvements to Chat UI on Web, Desktop apps
- Improve styling of chat session side panel
- Improve styling of chat message bubble in Desktop, Web app
- Add frosted, minimal chat UI to background of Login screen
- Improve PWA install experience of Khoj

### Fixes to Chat UI on Web, Desktop apps
- Fix creating new chat sessions from the Desktop app
- Only show 3 starter questions even when consecutive chat sessions created

### Other Improvements
- Update Khoj cloud trial period to a fortnight instead of a week
- Document using venv to handle dependency conflict on khoj pip install

Resolves #276
This commit is contained in:
Debanjum
2024-02-23 19:27:02 +05:30
committed by GitHub
19 changed files with 153 additions and 71 deletions

View File

@@ -15,7 +15,7 @@ import TabItem from '@theme/TabItem';
``` ```
## Setup ## Setup
These are the general setup instructions for Khoj. These are the general setup instructions for self-hosted Khoj.
- Make sure [python](https://realpython.com/installing-python/) and [pip](https://pip.pypa.io/en/stable/installation/) are installed on your machine - Make sure [python](https://realpython.com/installing-python/) and [pip](https://pip.pypa.io/en/stable/installation/) are installed on your machine
- Check the [Khoj Emacs docs](/clients/emacs#setup) to setup Khoj with Emacs<br /> - Check the [Khoj Emacs docs](/clients/emacs#setup) to setup Khoj with Emacs<br />
@@ -23,7 +23,7 @@ These are the general setup instructions for Khoj.
- Check the [Khoj Obsidian docs](/clients/obsidian#setup) to setup Khoj with Obsidian<br /> - Check the [Khoj Obsidian docs](/clients/obsidian#setup) to setup Khoj with Obsidian<br />
Its simpler as it can skip the *configure* step below. Its simpler as it can skip the *configure* step below.
For Installation, you can either use Docker or install Khoj locally. For Installation, you can either use Docker or install the Khoj server locally.
### Installation Option 1 (Docker) ### Installation Option 1 (Docker)
@@ -267,6 +267,18 @@ You can head to http://localhost:42110 to use the web interface. You can also us
## Troubleshoot ## Troubleshoot
#### Dependency conflict when trying to install Khoj python package with pip
- **Reason**: When conflicting dependency versions are required by Khoj vs other python packages installed on your system
- **Fix**: Install Khoj in a python virtual environment using [venv](https://docs.python.org/3/library/venv.html) or [pipx](https://pypa.github.io/pipx) to avoid this dependency conflicts
- **Process**:
1. Install [pipx](https://pypa.github.io/pipx/#install-pipx)
2. Use `pipx` to install Khoj to avoid dependency conflicts with other python packages.
```shell
pipx install khoj-assistant
```
3. Now start `khoj` using the standard steps described earlier
#### Install fails while building Tokenizer dependency #### Install fails while building Tokenizer dependency
- **Details**: `pip install khoj-assistant` fails while building the `tokenizers` dependency. Complains about Rust. - **Details**: `pip install khoj-assistant` fails while building the `tokenizers` dependency. Complains about Rust.
- **Fix**: Install Rust to build the tokenizers package. For example on Mac run: - **Fix**: Install Rust to build the tokenizers package. For example on Mac run:

View File

@@ -5,7 +5,7 @@
<title>Khoj - About</title> <title>Khoj - About</title>
<link rel="icon" type="image/png" sizes="128x128" href="./assets/icons/favicon-128x128.png"> <link rel="icon" type="image/png" sizes="128x128" href="./assets/icons/favicon-128x128.png">
<link rel="manifest" href="/static/khoj_chat.webmanifest"> <link rel="manifest" href="/static/khoj.webmanifest">
<link rel="stylesheet" href="./assets/khoj.css"> <link rel="stylesheet" href="./assets/khoj.css">
</head> </head>
<script type="text/javascript" src="./utils.js"></script> <script type="text/javascript" src="./utils.js"></script>

View File

@@ -1,6 +1,6 @@
/* Amber Light scheme (Default) */ /* Amber Light scheme (Default) */
/* Can be forced with data-theme="light" */ /* Can be forced with data-theme="light" */
@import url('https://fonts.googleapis.com/css2?family=Tajawal&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Tajawal:wght@300;500;700&display=swap');
[data-theme="light"], [data-theme="light"],
:root:not([data-theme="dark"]) { :root:not([data-theme="dark"]) {

View File

@@ -5,7 +5,7 @@
<title>Khoj - Chat</title> <title>Khoj - Chat</title>
<link rel="icon" type="image/png" sizes="128x128" href="./assets/icons/favicon-128x128.png"> <link rel="icon" type="image/png" sizes="128x128" href="./assets/icons/favicon-128x128.png">
<link rel="manifest" href="/static/khoj_chat.webmanifest"> <link rel="manifest" href="/static/khoj.webmanifest">
<link rel="stylesheet" href="./assets/khoj.css"> <link rel="stylesheet" href="./assets/khoj.css">
</head> </head>
<script type="text/javascript" src="./assets/markdown-it.min.js"></script> <script type="text/javascript" src="./assets/markdown-it.min.js"></script>
@@ -104,7 +104,7 @@
linkElement.classList.add("inline-chat-link"); linkElement.classList.add("inline-chat-link");
linkElement.classList.add("reference-link"); linkElement.classList.add("reference-link");
linkElement.setAttribute('title', title); linkElement.setAttribute('title', title);
linkElement.innerHTML = title; linkElement.textContent = title;
let referenceButton = document.createElement('button'); let referenceButton = document.createElement('button');
referenceButton.innerHTML = linkElement.outerHTML; referenceButton.innerHTML = linkElement.outerHTML;
@@ -133,7 +133,6 @@
let message_time = formatDate(dt ?? new Date()); let message_time = formatDate(dt ?? new Date());
let by_name = by == "khoj" ? "🏮 Khoj" : "🤔 You"; let by_name = by == "khoj" ? "🏮 Khoj" : "🤔 You";
let formattedMessage = formatHTMLMessage(message, raw); let formattedMessage = formatHTMLMessage(message, raw);
let chatBody = document.getElementById("chat-body");
// Create a new div for the chat message // Create a new div for the chat message
let chatMessage = document.createElement('div'); let chatMessage = document.createElement('div');
@@ -152,6 +151,7 @@
} }
// Append chat message div to chat body // Append chat message div to chat body
let chatBody = document.getElementById("chat-body");
chatBody.appendChild(chatMessage); chatBody.appendChild(chatMessage);
// Scroll to bottom of chat-body element // Scroll to bottom of chat-body element
@@ -285,9 +285,12 @@
// Render markdown // Render markdown
newHTML = raw ? newHTML : md.render(newHTML); newHTML = raw ? newHTML : md.render(newHTML);
// Get any elements with a class that starts with "language" // Set rendered markdown to HTML DOM element
let element = document.createElement('div'); let element = document.createElement('div');
element.innerHTML = newHTML; element.innerHTML = newHTML;
element.className = "chat-message-text-response";
// Get any elements with a class that starts with "language"
let codeBlockElements = element.querySelectorAll('[class^="language-"]'); let codeBlockElements = element.querySelectorAll('[class^="language-"]');
// For each element, add a parent div with the class "programmatic-output" // For each element, add a parent div with the class "programmatic-output"
codeBlockElements.forEach((codeElement) => { codeBlockElements.forEach((codeElement) => {
@@ -341,22 +344,20 @@
let chat_body = document.getElementById("chat-body"); let chat_body = document.getElementById("chat-body");
let conversationID = chat_body.dataset.conversationId; let conversationID = chat_body.dataset.conversationId;
let hostURL = await window.hostURLAPI.getURL(); let hostURL = await window.hostURLAPI.getURL();
const khojToken = await window.tokenAPI.getToken();
const headers = { 'Authorization': `Bearer ${khojToken}` };
if (!conversationID) { if (!conversationID) {
let response = await fetch(`${hostURL}/api/chat/sessions`, { method: "POST" }); let response = await fetch(`${hostURL}/api/chat/sessions`, { method: "POST", headers });
let data = await response.json(); let data = await response.json();
conversationID = data.conversation_id; conversationID = data.conversation_id;
chat_body.dataset.conversationId = conversationID; chat_body.dataset.conversationId = conversationID;
await refreshChatSessionsPanel(); await refreshChatSessionsPanel();
} }
// Generate backend API URL to execute query // Generate backend API URL to execute query
let url = `${hostURL}/api/chat?q=${encodeURIComponent(query)}&n=${resultsCount}&client=web&stream=true&conversation_id=${conversationID}&region=${region}&city=${city}&country=${countryName}`; let chatApi = `${hostURL}/api/chat?q=${encodeURIComponent(query)}&n=${resultsCount}&client=web&stream=true&conversation_id=${conversationID}&region=${region}&city=${city}&country=${countryName}`;
const khojToken = await window.tokenAPI.getToken();
const headers = { 'Authorization': `Bearer ${khojToken}` };
let new_response = document.createElement("div"); let new_response = document.createElement("div");
new_response.classList.add("chat-message", "khoj"); new_response.classList.add("chat-message", "khoj");
@@ -379,8 +380,8 @@
let chatInput = document.getElementById("chat-input"); let chatInput = document.getElementById("chat-input");
chatInput.classList.remove("option-enabled"); chatInput.classList.remove("option-enabled");
// Call specified Khoj API // Call Khoj chat API
let response = await fetch(url, { headers }); let response = await fetch(chatApi, { headers });
let rawResponse = ""; let rawResponse = "";
const contentType = response.headers.get("content-type"); const contentType = response.headers.get("content-type");
@@ -540,6 +541,7 @@
chatInput.value = chatInput.value.trimStart(); chatInput.value = chatInput.value.trimStart();
let questionStarterSuggestions = document.getElementById("question-starters"); let questionStarterSuggestions = document.getElementById("question-starters");
questionStarterSuggestions.innerHTML = "";
questionStarterSuggestions.style.display = "none"; questionStarterSuggestions.style.display = "none";
if (chatInput.value.startsWith("/") && chatInput.value.split(" ").length === 1) { if (chatInput.value.startsWith("/") && chatInput.value.split(" ").length === 1) {
@@ -591,6 +593,7 @@
const headers = { 'Authorization': `Bearer ${khojToken}` }; const headers = { 'Authorization': `Bearer ${khojToken}` };
let chatBody = document.getElementById("chat-body"); let chatBody = document.getElementById("chat-body");
chatBody.innerHTML = "";
let conversationId = chatBody.dataset.conversationId; let conversationId = chatBody.dataset.conversationId;
let chatHistoryUrl = `/api/chat/history?client=desktop`; let chatHistoryUrl = `/api/chat/history?client=desktop`;
if (conversationId) { if (conversationId) {
@@ -671,11 +674,11 @@
fetch(`${hostURL}/api/chat/starters?client=desktop`, { headers }) fetch(`${hostURL}/api/chat/starters?client=desktop`, { headers })
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
// Render chat options, if any // Render conversation starters, if any
if (data.length > 0) { if (data.length > 0) {
let questionStarterSuggestions = document.getElementById("question-starters"); let questionStarterSuggestions = document.getElementById("question-starters");
for (let index in data) { questionStarterSuggestions.innerHTML = "";
let questionStarter = data[index]; data.forEach((questionStarter) => {
let questionStarterButton = document.createElement('button'); let questionStarterButton = document.createElement('button');
questionStarterButton.innerHTML = questionStarter; questionStarterButton.innerHTML = questionStarter;
questionStarterButton.classList.add("question-starter"); questionStarterButton.classList.add("question-starter");
@@ -685,7 +688,7 @@
chat(); chat();
}); });
questionStarterSuggestions.appendChild(questionStarterButton); questionStarterSuggestions.appendChild(questionStarterButton);
} });
questionStarterSuggestions.style.display = "grid"; questionStarterSuggestions.style.display = "grid";
} }
}) })
@@ -785,7 +788,7 @@
let conversationButton = document.createElement('div'); let conversationButton = document.createElement('div');
let incomingConversationId = conversation["conversation_id"]; let incomingConversationId = conversation["conversation_id"];
const conversationTitle = conversation["slug"] || `New conversation 🌱`; const conversationTitle = conversation["slug"] || `New conversation 🌱`;
conversationButton.innerHTML = conversationTitle; conversationButton.textContent = conversationTitle;
conversationButton.classList.add("conversation-button"); conversationButton.classList.add("conversation-button");
if (incomingConversationId == conversationId) { if (incomingConversationId == conversationId) {
conversationButton.classList.add("selected-conversation"); conversationButton.classList.add("selected-conversation");
@@ -883,7 +886,7 @@
fetch(`${hostURL}${editURL}` , { method: "PATCH" }) fetch(`${hostURL}${editURL}` , { method: "PATCH" })
.then(response => response.ok ? response.json() : Promise.reject(response)) .then(response => response.ok ? response.json() : Promise.reject(response))
.then(data => { .then(data => {
conversationButton.innerHTML = newTitle; conversationButton.textContent = newTitle;
}) })
.catch(err => { .catch(err => {
return; return;
@@ -1014,6 +1017,7 @@
document.getElementById('side-panel').classList.toggle('collapsed'); document.getElementById('side-panel').classList.toggle('collapsed');
document.getElementById('new-conversation').classList.toggle('collapsed'); document.getElementById('new-conversation').classList.toggle('collapsed');
document.getElementById('existing-conversations').classList.toggle('collapsed'); document.getElementById('existing-conversations').classList.toggle('collapsed');
document.getElementById('side-panel-collapse').style.transform = document.getElementById('side-panel').classList.contains('collapsed') ? 'rotate(0deg)' : 'rotate(180deg)';
document.getElementById('chat-section-wrapper').classList.toggle('mobile-friendly'); document.getElementById('chat-section-wrapper').classList.toggle('mobile-friendly');
} }
@@ -1058,7 +1062,7 @@
id="collapse-side-panel-button" id="collapse-side-panel-button"
onclick="handleCollapseSidePanel()" onclick="handleCollapseSidePanel()"
> >
<svg class="side-panel-collapse" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg id="side-panel-collapse" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.82054 20.7313C8.21107 21.1218 8.84423 21.1218 9.23476 20.7313L15.8792 14.0868C17.0505 12.9155 17.0508 11.0167 15.88 9.84497L9.3097 3.26958C8.91918 2.87905 8.28601 2.87905 7.89549 3.26958C7.50497 3.6601 7.50497 4.29327 7.89549 4.68379L14.4675 11.2558C14.8581 11.6464 14.8581 12.2795 14.4675 12.67L7.82054 19.317C7.43002 19.7076 7.43002 20.3407 7.82054 20.7313Z" fill="#0F0F0F"/> <path d="M7.82054 20.7313C8.21107 21.1218 8.84423 21.1218 9.23476 20.7313L15.8792 14.0868C17.0505 12.9155 17.0508 11.0167 15.88 9.84497L9.3097 3.26958C8.91918 2.87905 8.28601 2.87905 7.89549 3.26958C7.50497 3.6601 7.50497 4.29327 7.89549 4.68379L14.4675 11.2558C14.8581 11.6464 14.8581 12.2795 14.4675 12.67L7.82054 19.317C7.43002 19.7076 7.43002 20.3407 7.82054 20.7313Z" fill="#0F0F0F"/>
</svg> </svg>
</button> </button>
@@ -1161,6 +1165,7 @@
} }
#side-panel { #side-panel {
width: 250px;
padding: 10px; padding: 10px;
background: var(--background-color); background: var(--background-color);
border-radius: 5px; border-radius: 5px;
@@ -1168,11 +1173,11 @@
overflow-y: scroll; overflow-y: scroll;
text-align: left; text-align: left;
transition: width 0.3s ease-in-out; transition: width 0.3s ease-in-out;
width: 250px;
} }
div#side-panel.collapsed { div#side-panel.collapsed {
width: 1px; width: 0;
padding: 0;
display: block; display: block;
overflow: hidden; overflow: hidden;
} }
@@ -1227,6 +1232,7 @@
display: inline-block; display: inline-block;
max-width: 80%; max-width: 80%;
text-align: left; text-align: left;
white-space: pre-line;
} }
/* color chat bubble by khoj blue */ /* color chat bubble by khoj blue */
.chat-message-text.khoj { .chat-message-text.khoj {
@@ -1234,6 +1240,15 @@
background: var(--primary); background: var(--primary);
margin-left: auto; margin-left: auto;
} }
.chat-message-text ol,
.chat-message-text ul {
white-space: normal;
margin: 0;
}
.chat-message-text-response {
margin-bottom: -16px;
}
/* Spinner symbol when the chat message is loading */ /* Spinner symbol when the chat message is loading */
.spinner { .spinner {
border: 4px solid #f3f3f3; border: 4px solid #f3f3f3;
@@ -1350,7 +1365,7 @@
font-size: large; font-size: large;
} }
svg.side-panel-collapse { svg#side-panel-collapse {
width: 30px; width: 30px;
height: 30px; height: 30px;
} }
@@ -1604,7 +1619,7 @@
padding: 0; padding: 0;
} }
svg.side-panel-collapse { svg#side-panel-collapse {
width: 24px; width: 24px;
height: 24px; height: 24px;
} }

View File

@@ -219,7 +219,7 @@ def subscription_to_state(subscription: Subscription) -> str:
return SubscriptionState.INVALID.value return SubscriptionState.INVALID.value
elif subscription.type == Subscription.Type.TRIAL: elif subscription.type == Subscription.Type.TRIAL:
# Trial subscription is valid for 7 days # Trial subscription is valid for 7 days
if datetime.now(tz=timezone.utc) - subscription.created_at > timedelta(days=7): if datetime.now(tz=timezone.utc) - subscription.created_at > timedelta(days=14):
return SubscriptionState.EXPIRED.value return SubscriptionState.EXPIRED.value
return SubscriptionState.TRIAL.value return SubscriptionState.TRIAL.value
@@ -573,7 +573,7 @@ class ConversationAdapters:
return await SpeechToTextModelOptions.objects.filter().afirst() return await SpeechToTextModelOptions.objects.filter().afirst()
@staticmethod @staticmethod
async def aget_conversation_starters(user: KhojUser): async def aget_conversation_starters(user: KhojUser, max_results=3):
all_questions = [] all_questions = []
if await ReflectiveQuestion.objects.filter(user=user).aexists(): if await ReflectiveQuestion.objects.filter(user=user).aexists():
all_questions = await sync_to_async(ReflectiveQuestion.objects.filter(user=user).values_list)( all_questions = await sync_to_async(ReflectiveQuestion.objects.filter(user=user).values_list)(
@@ -584,7 +584,6 @@ class ConversationAdapters:
"question", flat=True "question", flat=True
) )
max_results = 3
all_questions = await sync_to_async(list)(all_questions) # type: ignore all_questions = await sync_to_async(list)(all_questions) # type: ignore
if len(all_questions) < max_results: if len(all_questions) < max_results:
return all_questions return all_questions

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -1,6 +1,6 @@
/* Amber Light scheme (Default) */ /* Amber Light scheme (Default) */
/* Can be forced with data-theme="light" */ /* Can be forced with data-theme="light" */
@import url('https://fonts.googleapis.com/css2?family=Tajawal&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Tajawal:wght@300;500;700&display=swap');
[data-theme="light"], [data-theme="light"],
:root:not([data-theme="dark"]) { :root:not([data-theme="dark"]) {
@@ -9,6 +9,7 @@
--primary-focus: rgba(255, 179, 0, 0.125); --primary-focus: rgba(255, 179, 0, 0.125);
--primary-inverse: rgba(0, 0, 0, 0.75); --primary-inverse: rgba(0, 0, 0, 0.75);
--background-color: #f5f4f3; --background-color: #f5f4f3;
--frosted-background-color: rgba(245, 244, 243, 0.75);
--main-text-color: #475569; --main-text-color: #475569;
--summer-sun: #fcc50b; --summer-sun: #fcc50b;
--water: #44b9da; --water: #44b9da;
@@ -26,6 +27,7 @@
--primary-focus: rgba(255, 179, 0, 0.25); --primary-focus: rgba(255, 179, 0, 0.25);
--primary-inverse: rgba(0, 0, 0, 0.75); --primary-inverse: rgba(0, 0, 0, 0.75);
--background-color: #f5f4f3; --background-color: #f5f4f3;
--frosted-background-color: rgba(245, 244, 243, 0.75);
--main-text-color: #475569; --main-text-color: #475569;
--summer-sun: #fcc50b; --summer-sun: #fcc50b;
--water: #44b9da; --water: #44b9da;
@@ -42,6 +44,7 @@
--primary-focus: rgba(255, 179, 0, 0.25); --primary-focus: rgba(255, 179, 0, 0.25);
--primary-inverse: rgba(0, 0, 0, 0.75); --primary-inverse: rgba(0, 0, 0, 0.75);
--background-color: #f5f4f3; --background-color: #f5f4f3;
--frosted-background-color: rgba(245, 244, 243, 0.75);
--main-text-color: #475569; --main-text-color: #475569;
--summer-sun: #fcc50b; --summer-sun: #fcc50b;
--water: #44b9da; --water: #44b9da;

Binary file not shown.

After

Width:  |  Height:  |  Size: 577 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 397 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

View File

@@ -5,7 +5,7 @@
<title>Khoj - Chat</title> <title>Khoj - Chat</title>
<link rel="icon" type="image/png" sizes="128x128" href="/static/assets/icons/favicon-128x128.png"> <link rel="icon" type="image/png" sizes="128x128" href="/static/assets/icons/favicon-128x128.png">
<link rel="manifest" href="/static/khoj_chat.webmanifest"> <link rel="manifest" href="/static/khoj.webmanifest">
<link rel="stylesheet" href="/static/assets/khoj.css"> <link rel="stylesheet" href="/static/assets/khoj.css">
</head> </head>
<script type="text/javascript" src="/static/assets/utils.js"></script> <script type="text/javascript" src="/static/assets/utils.js"></script>
@@ -116,7 +116,7 @@ To get started, just start typing below. You can also type / to see a list of co
linkElement.classList.add("inline-chat-link"); linkElement.classList.add("inline-chat-link");
linkElement.classList.add("reference-link"); linkElement.classList.add("reference-link");
linkElement.setAttribute('title', title); linkElement.setAttribute('title', title);
linkElement.innerHTML = title; linkElement.textContent = title;
let referenceButton = document.createElement('button'); let referenceButton = document.createElement('button');
referenceButton.innerHTML = linkElement.outerHTML; referenceButton.innerHTML = linkElement.outerHTML;
@@ -145,7 +145,6 @@ To get started, just start typing below. You can also type / to see a list of co
let message_time = formatDate(dt ?? new Date()); let message_time = formatDate(dt ?? new Date());
let by_name = by == "khoj" ? "🏮 Khoj" : "🤔 You"; let by_name = by == "khoj" ? "🏮 Khoj" : "🤔 You";
let formattedMessage = formatHTMLMessage(message, raw); let formattedMessage = formatHTMLMessage(message, raw);
let chatBody = document.getElementById("chat-body");
// Create a new div for the chat message // Create a new div for the chat message
let chatMessage = document.createElement('div'); let chatMessage = document.createElement('div');
@@ -164,6 +163,7 @@ To get started, just start typing below. You can also type / to see a list of co
} }
// Append chat message div to chat body // Append chat message div to chat body
let chatBody = document.getElementById("chat-body");
chatBody.appendChild(chatMessage); chatBody.appendChild(chatMessage);
// Scroll to bottom of chat-body element // Scroll to bottom of chat-body element
@@ -297,9 +297,12 @@ To get started, just start typing below. You can also type / to see a list of co
// Render markdown // Render markdown
newHTML = raw ? newHTML : md.render(newHTML); newHTML = raw ? newHTML : md.render(newHTML);
// Get any elements with a class that starts with "language" // Set rendered markdown to HTML DOM element
let element = document.createElement('div'); let element = document.createElement('div');
element.innerHTML = newHTML; element.innerHTML = newHTML;
element.className = "chat-message-text-response";
// Get any elements with a class that starts with "language"
let codeBlockElements = element.querySelectorAll('[class^="language-"]'); let codeBlockElements = element.querySelectorAll('[class^="language-"]');
// For each element, add a parent div with the class "programmatic-output" // For each element, add a parent div with the class "programmatic-output"
codeBlockElements.forEach((codeElement) => { codeBlockElements.forEach((codeElement) => {
@@ -520,6 +523,7 @@ To get started, just start typing below. You can also type / to see a list of co
chatInput.value = chatInput.value.trimStart(); chatInput.value = chatInput.value.trimStart();
let questionStarterSuggestions = document.getElementById("question-starters"); let questionStarterSuggestions = document.getElementById("question-starters");
questionStarterSuggestions.innerHTML = "";
questionStarterSuggestions.style.display = "none"; questionStarterSuggestions.style.display = "none";
if (chatInput.value.startsWith("/") && chatInput.value.split(" ").length === 1) { if (chatInput.value.startsWith("/") && chatInput.value.split(" ").length === 1) {
@@ -565,6 +569,7 @@ To get started, just start typing below. You can also type / to see a list of co
function loadChat() { function loadChat() {
let chatBody = document.getElementById("chat-body"); let chatBody = document.getElementById("chat-body");
chatBody.innerHTML = "";
let conversationId = chatBody.dataset.conversationId; let conversationId = chatBody.dataset.conversationId;
let chatHistoryUrl = `/api/chat/history?client=web`; let chatHistoryUrl = `/api/chat/history?client=web`;
if (conversationId) { if (conversationId) {
@@ -647,8 +652,8 @@ To get started, just start typing below. You can also type / to see a list of co
// Render chat options, if any // Render chat options, if any
if (data.length > 0) { if (data.length > 0) {
let questionStarterSuggestions = document.getElementById("question-starters"); let questionStarterSuggestions = document.getElementById("question-starters");
for (let index in data) { questionStarterSuggestions.innerHTML = "";
let questionStarter = data[index]; data.forEach((questionStarter) => {
let questionStarterButton = document.createElement('button'); let questionStarterButton = document.createElement('button');
questionStarterButton.innerHTML = questionStarter; questionStarterButton.innerHTML = questionStarter;
questionStarterButton.classList.add("question-starter"); questionStarterButton.classList.add("question-starter");
@@ -658,7 +663,7 @@ To get started, just start typing below. You can also type / to see a list of co
chat(); chat();
}); });
questionStarterSuggestions.appendChild(questionStarterButton); questionStarterSuggestions.appendChild(questionStarterButton);
} });
questionStarterSuggestions.style.display = "grid"; questionStarterSuggestions.style.display = "grid";
} }
}) })
@@ -713,7 +718,7 @@ To get started, just start typing below. You can also type / to see a list of co
let conversationButton = document.createElement('div'); let conversationButton = document.createElement('div');
let incomingConversationId = conversation["conversation_id"]; let incomingConversationId = conversation["conversation_id"];
const conversationTitle = conversation["slug"] || `New conversation 🌱`; const conversationTitle = conversation["slug"] || `New conversation 🌱`;
conversationButton.innerHTML = conversationTitle; conversationButton.textContent = conversationTitle;
conversationButton.classList.add("conversation-button"); conversationButton.classList.add("conversation-button");
if (incomingConversationId == conversationId) { if (incomingConversationId == conversationId) {
conversationButton.classList.add("selected-conversation"); conversationButton.classList.add("selected-conversation");
@@ -789,7 +794,7 @@ To get started, just start typing below. You can also type / to see a list of co
fetch(editURL , { method: "PATCH" }) fetch(editURL , { method: "PATCH" })
.then(response => response.ok ? response.json() : Promise.reject(response)) .then(response => response.ok ? response.json() : Promise.reject(response))
.then(data => { .then(data => {
conversationButton.innerHTML = newTitle; conversationButton.textContent = newTitle;
}) })
.catch(err => { .catch(err => {
return; return;
@@ -966,6 +971,7 @@ To get started, just start typing below. You can also type / to see a list of co
document.getElementById('side-panel').classList.toggle('collapsed'); document.getElementById('side-panel').classList.toggle('collapsed');
document.getElementById('new-conversation').classList.toggle('collapsed'); document.getElementById('new-conversation').classList.toggle('collapsed');
document.getElementById('existing-conversations').classList.toggle('collapsed'); document.getElementById('existing-conversations').classList.toggle('collapsed');
document.getElementById('side-panel-collapse').style.transform = document.getElementById('side-panel').classList.contains('collapsed') ? 'rotate(0deg)' : 'rotate(180deg)';
document.getElementById('chat-section-wrapper').classList.toggle('mobile-friendly'); document.getElementById('chat-section-wrapper').classList.toggle('mobile-friendly');
} }
@@ -1001,7 +1007,7 @@ To get started, just start typing below. You can also type / to see a list of co
id="collapse-side-panel-button" id="collapse-side-panel-button"
onclick="handleCollapseSidePanel()" onclick="handleCollapseSidePanel()"
> >
<svg class="side-panel-collapse" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg id="side-panel-collapse" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.82054 20.7313C8.21107 21.1218 8.84423 21.1218 9.23476 20.7313L15.8792 14.0868C17.0505 12.9155 17.0508 11.0167 15.88 9.84497L9.3097 3.26958C8.91918 2.87905 8.28601 2.87905 7.89549 3.26958C7.50497 3.6601 7.50497 4.29327 7.89549 4.68379L14.4675 11.2558C14.8581 11.6464 14.8581 12.2795 14.4675 12.67L7.82054 19.317C7.43002 19.7076 7.43002 20.3407 7.82054 20.7313Z" fill="#0F0F0F"/> <path d="M7.82054 20.7313C8.21107 21.1218 8.84423 21.1218 9.23476 20.7313L15.8792 14.0868C17.0505 12.9155 17.0508 11.0167 15.88 9.84497L9.3097 3.26958C8.91918 2.87905 8.28601 2.87905 7.89549 3.26958C7.50497 3.6601 7.50497 4.29327 7.89549 4.68379L14.4675 11.2558C14.8581 11.6464 14.8581 12.2795 14.4675 12.67L7.82054 19.317C7.43002 19.7076 7.43002 20.3407 7.82054 20.7313Z" fill="#0F0F0F"/>
</svg> </svg>
</button> </button>
@@ -1226,6 +1232,7 @@ To get started, just start typing below. You can also type / to see a list of co
} }
#side-panel { #side-panel {
width: 250px;
padding: 10px; padding: 10px;
background: var(--background-color); background: var(--background-color);
border-radius: 5px; border-radius: 5px;
@@ -1233,11 +1240,11 @@ To get started, just start typing below. You can also type / to see a list of co
overflow-y: scroll; overflow-y: scroll;
text-align: left; text-align: left;
transition: width 0.3s ease-in-out; transition: width 0.3s ease-in-out;
width: 250px;
} }
div#side-panel.collapsed { div#side-panel.collapsed {
width: 1px; width: 0;
padding: 0;
display: block; display: block;
overflow: hidden; overflow: hidden;
} }
@@ -1291,6 +1298,7 @@ To get started, just start typing below. You can also type / to see a list of co
display: inline-block; display: inline-block;
max-width: 80%; max-width: 80%;
text-align: left; text-align: left;
white-space: pre-line;
} }
/* color chat bubble by khoj blue */ /* color chat bubble by khoj blue */
.chat-message-text.khoj { .chat-message-text.khoj {
@@ -1298,6 +1306,15 @@ To get started, just start typing below. You can also type / to see a list of co
background: var(--primary); background: var(--primary);
margin-left: auto; margin-left: auto;
} }
.chat-message-text ol,
.chat-message-text ul {
white-space: normal;
margin: 0;
}
.chat-message-text-response {
margin-bottom: -16px;
}
/* Spinner symbol when the chat message is loading */ /* Spinner symbol when the chat message is loading */
.spinner { .spinner {
border: 4px solid #f3f3f3; border: 4px solid #f3f3f3;
@@ -1412,7 +1429,7 @@ To get started, just start typing below. You can also type / to see a list of co
font-size: large; font-size: large;
} }
svg.side-panel-collapse { svg#side-panel-collapse {
width: 30px; width: 30px;
height: 30px; height: 30px;
} }
@@ -1551,7 +1568,7 @@ To get started, just start typing below. You can also type / to see a list of co
padding: 0; padding: 0;
} }
svg.side-panel-collapse { svg#side-panel-collapse {
width: 24px; width: 24px;
height: 24px; height: 24px;
} }

View File

@@ -1,16 +1,51 @@
{ {
"name": "Khoj", "name": "Khoj",
"short_name": "Khoj", "short_name": "Khoj",
"description": "An AI search assistant for your digital brain", "display": "standalone",
"start_url": "/",
"description": "The open, personal AI for your digital brain. You can ask Khoj to draft a message, paint your imagination, find information on the internet and even answer questions from your documents.",
"theme_color": "#ffffff",
"background_color": "#ffffff",
"icons": [ "icons": [
{ {
"src": "/static/assets/icons/favicon-128x128.png", "src": "/static/assets/icons/favicon-128x128.png",
"sizes": "128x128", "sizes": "128x128",
"type": "image/png" "type": "image/png"
},
{
"src": "/static/assets/icons/favicon-256x256.png",
"sizes": "256x256",
"type": "image/png"
} }
], ],
"theme_color": "#ffffff", "screenshots": [
"background_color": "#ffffff", {
"display": "standalone", "src": "/static/assets/samples/phone-remember-plan-sample.png",
"start_url": "/" "sizes": "419x900",
"type": "image/png",
"form_factor": "narrow",
"label": "Remember and Plan"
},
{
"src": "/static/assets/samples/phone-browse-draw-sample.png",
"sizes": "419x900",
"type": "image/png",
"form_factor": "narrow",
"label": "Browse and Draw"
},
{
"src": "/static/assets/samples/desktop-remember-plan-sample.png",
"sizes": "1260x742",
"type": "image/png",
"form_factor": "wide",
"label": "Remember and Plan"
},
{
"src": "/static/assets/samples/desktop-browse-draw-sample.png",
"sizes": "1260x742",
"type": "image/png",
"form_factor": "wide",
"label": "Browse and Draw"
}
]
} }

View File

@@ -1,16 +0,0 @@
{
"name": "Khoj Chat",
"short_name": "Khoj Chat",
"description": "An AI personal assistant for your digital brain",
"icons": [
{
"src": "/static/assets/icons/favicon-128x128.png",
"sizes": "128x128",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone",
"start_url": "/chat"
}

View File

@@ -65,13 +65,24 @@
padding: 0px; padding: 0px;
margin: 0px; margin: 0px;
height: 100%; height: 100%;
background: var(--background-color); background: url('/static/assets/samples/desktop-plain-chat-sample.png') no-repeat center center fixed;
background-size: contain;
color: var(--main-text-color); color: var(--main-text-color);
font-family: var(--font-family); font-family: var(--font-family);
font-size: 20px; font-size: 20px;
font-weight: 300; font-weight: 300;
line-height: 1.5em; line-height: 1.5em;
} }
body::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--frosted-background-color);
backdrop-filter: blur(10px);
}
body > * { body > * {
padding: 10px; padding: 10px;
margin: 10px; margin: 10px;
@@ -100,9 +111,9 @@
grid-template-rows: 1fr auto auto 1fr; grid-template-rows: 1fr auto auto 1fr;
gap: 32px; gap: 32px;
min-height: 300px; min-height: 300px;
background: var(--background-color);
margin-left: 25%; margin-left: 25%;
margin-right: 25%; margin-right: 25%;
z-index: 1;
} }
div.g_id_signin { div.g_id_signin {
@@ -118,9 +129,14 @@
} }
@media only screen and (max-width: 700px) { @media only screen and (max-width: 700px) {
body{
background: url('/static/assets/samples/phone-plain-chat-sample.png') no-repeat center center fixed;
background-size: contain;
}
div#login-modal { div#login-modal {
margin-left: 10%; margin-left: 10%;
margin-right: 10%; margin-right: 10%;
z-index: 1;
} }
} }

View File

@@ -122,6 +122,7 @@ def save_to_conversation_log(
conversation_id=conversation_id, conversation_id=conversation_id,
user_message=q, user_message=q,
) )
logger.info(f'Saved Conversation Turn\nYou ({user.username}): "{q}"\n\nKhoj: "{chat_response}"')
def generate_chatml_messages_with_context( def generate_chatml_messages_with_context(

View File

@@ -430,14 +430,14 @@ class ApiUserRateLimiter:
if subscribed and count_requests >= self.subscribed_requests: if subscribed and count_requests >= self.subscribed_requests:
raise HTTPException(status_code=429, detail="Slow down! Too Many Requests") raise HTTPException(status_code=429, detail="Slow down! Too Many Requests")
if not subscribed and count_requests >= self.requests: if not subscribed and count_requests >= self.requests:
if self.subscribed_requests == self.requests: if self.requests >= self.subscribed_requests:
raise HTTPException( raise HTTPException(
status_code=429, status_code=429,
detail="Slow down! Too Many Requests", detail="Slow down! Too Many Requests",
) )
raise HTTPException( raise HTTPException(
status_code=429, status_code=429,
detail="We're glad you're enjoying Khoj! You've exceeded your usage limit for today. Come back tomorrow or subscribe to increase your rate limit via [your settings](https://app.khoj.dev/config).", detail="We're glad you're enjoying Khoj! You've exceeded your usage limit for today. Come back tomorrow or subscribe to increase your usage limit via [your settings](https://app.khoj.dev/config).",
) )
# Add the current request to the cache # Add the current request to the cache
@@ -476,7 +476,7 @@ class ConversationCommandRateLimiter:
if not subscribed and count_requests >= self.trial_rate_limit: if not subscribed and count_requests >= self.trial_rate_limit:
raise HTTPException( raise HTTPException(
status_code=429, status_code=429,
detail=f"We're glad you're enjoying Khoj! You've exceeded your `/{conversation_command.value}` command usage limit for today. You can increase your rate limit via [your settings](https://app.khoj.dev/config).", detail=f"We're glad you're enjoying Khoj! You've exceeded your `/{conversation_command.value}` command usage limit for today. Subscribe to increase your usage limit via [your settings](https://app.khoj.dev/config).",
) )
await UserRequests.objects.acreate(user=user, slug=command_slug) await UserRequests.objects.acreate(user=user, slug=command_slug)
return return