Reduce Desktop App UX Save, Sync Confusion (#538)

- Show next sync time to make users aware of data sync is automated
- Keep a single Save button to reduce confusion. It does what Save All
  previously did. Intent to manual sync should Save All
- Default to using app.khoj.dev as default Khoj URL to ease Cloud sync setup
- Add detailed chat intro message, mention download desktop app for docs sync
- Only show search in web app nav pane if user has documents indexed
- Hide download desktop app message in web app if synced files exist
- Mark generated profile pic with subscription circle in web app
This commit is contained in:
Debanjum
2023-11-10 00:57:45 -08:00
committed by GitHub
14 changed files with 74 additions and 43 deletions

View File

@@ -24,7 +24,7 @@ BASE_DIR = Path(__file__).resolve().parent.parent.parent
SECRET_KEY = os.getenv("KHOJ_DJANGO_SECRET_KEY") SECRET_KEY = os.getenv("KHOJ_DJANGO_SECRET_KEY")
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.getenv("DJANGO_DEBUG", "False") == "True" DEBUG = os.getenv("KHOJ_DEBUG", "False") == "True"
ALLOWED_HOSTS = [".khoj.dev", "localhost", "127.0.0.1", "[::1]", "beta.khoj.dev"] ALLOWED_HOSTS = [".khoj.dev", "localhost", "127.0.0.1", "[::1]", "beta.khoj.dev"]

View File

@@ -134,10 +134,11 @@ async def set_user_subscription(
return None return None
def get_user_subscription_state(user_subscription: Subscription) -> str: def get_user_subscription_state(email: str) -> str:
"""Get subscription state of user """Get subscription state of user
Valid state transitions: trial -> subscribed <-> unsubscribed OR expired Valid state transitions: trial -> subscribed <-> unsubscribed OR expired
""" """
user_subscription = Subscription.objects.filter(user__email=email).first()
if not user_subscription: if not user_subscription:
return "trial" return "trial"
elif user_subscription.type == Subscription.Type.TRIAL: elif user_subscription.type == Subscription.Type.TRIAL:
@@ -370,8 +371,8 @@ class EntryAdapters:
) )
@staticmethod @staticmethod
async def user_has_entries(user: KhojUser): def user_has_entries(user: KhojUser):
return await Entry.objects.filter(user=user).aexists() return Entry.objects.filter(user=user).exists()
@staticmethod @staticmethod
async def adelete_entry_by_file(user: KhojUser, file_path: str): async def adelete_entry_by_file(user: KhojUser, file_path: str):
@@ -450,5 +451,5 @@ class EntryAdapters:
return Entry.objects.filter(user=user).values_list("file_type", flat=True).distinct() return Entry.objects.filter(user=user).values_list("file_type", flat=True).distinct()
@staticmethod @staticmethod
def get_unique_file_source(user: KhojUser): def get_unique_file_sources(user: KhojUser):
return Entry.objects.filter(user=user).values_list("file_source", flat=True).distinct() return Entry.objects.filter(user=user).values_list("file_source", flat=True).distinct().all()

View File

@@ -91,10 +91,7 @@
</div> </div>
<div class="section-action-row"> <div class="section-action-row">
<div class="card-description-row"> <div class="card-description-row">
<button id="sync-data" class="sync-data">💾 Save</button> <button id="sync-force" class="sync-data">💾 Save</button>
</div>
<div class="card-description-row">
<button id="sync-force" class="sync-data">💾 Save All</button>
</div> </div>
<div class="card-description-row"> <div class="card-description-row">
<button id="delete-all" class="sync-data">🗑️ Delete All</button> <button id="delete-all" class="sync-data">🗑️ Delete All</button>

View File

@@ -10,7 +10,7 @@ const {dialog} = require('electron');
const cron = require('cron').CronJob; const cron = require('cron').CronJob;
const axios = require('axios'); const axios = require('axios');
const KHOJ_URL = 'http://127.0.0.1:42110' const KHOJ_URL = 'https://app.khoj.dev';
const Store = require('electron-store'); const Store = require('electron-store');
@@ -67,7 +67,7 @@ const schema = {
} }
}; };
const syncing = false; let syncing = false;
var state = {} var state = {}
const store = new Store({ schema }); const store = new Store({ schema });

View File

@@ -155,11 +155,14 @@ window.updateStateAPI.onUpdateState((event, state) => {
loadingBar.style.display = 'none'; loadingBar.style.display = 'none';
let syncStatusElement = document.getElementById("sync-status"); let syncStatusElement = document.getElementById("sync-status");
const currentTime = new Date(); const currentTime = new Date();
nextSyncTime = new Date();
nextSyncTime.setMinutes(Math.ceil((nextSyncTime.getMinutes() + 1) / 10) * 10);
if (state.completed == false) { if (state.completed == false) {
syncStatusElement.innerHTML = `Sync was unsuccessful at ${currentTime.toLocaleTimeString()}. Contact team@khoj.dev to report this issue.`; syncStatusElement.innerHTML = `Sync was unsuccessful at ${currentTime.toLocaleTimeString()}. Contact team@khoj.dev to report this issue.`;
return; return;
} }
syncStatusElement.innerHTML = `Last synced at ${currentTime.toLocaleTimeString()}`; const options = { hour: '2-digit', minute: '2-digit' };
syncStatusElement.innerHTML = `⏱️ Synced at ${currentTime.toLocaleTimeString(undefined, options)}. Next sync at ${nextSyncTime.toLocaleTimeString(undefined, options)}.`;
}); });
const urlInput = document.getElementById('khoj-host-url'); const urlInput = document.getElementById('khoj-host-url');

View File

@@ -15,7 +15,7 @@
<!--Add Header Logo and Nav Pane--> <!--Add Header Logo and Nav Pane-->
{% import 'utils.html' as utils %} {% import 'utils.html' as utils %}
{{ utils.heading_pane(user_photo, username) }} {{ utils.heading_pane(user_photo, username, is_active, has_documents) }}
<div class="filler"></div> <div class="filler"></div>
</div> </div>
@@ -26,9 +26,6 @@
</body> </body>
<script> <script>
document.getElementById("settings-nav").classList.add("khoj-nav-selected"); document.getElementById("settings-nav").classList.add("khoj-nav-selected");
{% if is_active %}
document.getElementById("profile-picture").classList.add("subscribed");
{% endif %}
</script> </script>
<style> <style>
html, body { html, body {

View File

@@ -10,6 +10,16 @@
</head> </head>
<script type="text/javascript" src="/static/assets/utils.js"></script> <script type="text/javascript" src="/static/assets/utils.js"></script>
<script> <script>
let welcome_message = `
Hi, I am Khoj, your open, personal AI 👋🏽. I can help:
• 🧠 Answer general knowledge questions
• 💡 Be a sounding board for your ideas
• 📜 Chat with your notes & documents
Download the <a class='inline-chat-link' href='https://khoj.dev/downloads'>🖥️ Desktop app</a> to chat with your computer docs.
To get started, just start typing below. You can also type / to see a list of commands.
`.trim()
let chatOptions = []; let chatOptions = [];
function copyProgrammaticOutput(event) { function copyProgrammaticOutput(event) {
// Remove the first 4 characters which are the "Copy" button // Remove the first 4 characters which are the "Copy" button
@@ -322,7 +332,7 @@
document.getElementById("chat-input").setAttribute("placeholder", "Configure Khoj to enable chat"); document.getElementById("chat-input").setAttribute("placeholder", "Configure Khoj to enable chat");
} else { } else {
// Set welcome message on load // Set welcome message on load
renderMessage("Hey 👋🏾, what's up?", "khoj"); renderMessage(welcome_message, "khoj");
} }
return data.response; return data.response;
}) })
@@ -363,7 +373,7 @@
<!--Add Header Logo and Nav Pane--> <!--Add Header Logo and Nav Pane-->
{% import 'utils.html' as utils %} {% import 'utils.html' as utils %}
{{ utils.heading_pane(user_photo, username) }} {{ utils.heading_pane(user_photo, username, is_active, has_documents) }}
<!-- Chat Body --> <!-- Chat Body -->
<div id="chat-body"></div> <div id="chat-body"></div>
@@ -376,9 +386,6 @@
</body> </body>
<script> <script>
document.getElementById("chat-nav").classList.add("khoj-nav-selected"); document.getElementById("chat-nav").classList.add("khoj-nav-selected");
{% if is_active %}
document.getElementById("profile-picture").classList.add("subscribed");
{% endif %}
</script> </script>
<style> <style>
html, body { html, body {

View File

@@ -7,7 +7,7 @@
<span class="card-title-text">Files</span> <span class="card-title-text">Files</span>
<div class="instructions"> <div class="instructions">
<p class="card-description">Manage files from your computer</p> <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> <p id="get-desktop-client" class="card-description">Download the <a href="https://download.khoj.dev">Khoj Desktop app</a> to sync documents from your computer</p>
</div> </div>
</h2> </h2>
<div class="section-manage-files"> <div class="section-manage-files">
@@ -56,8 +56,9 @@
if (data.length == 0) { if (data.length == 0) {
document.getElementById("delete-all-files").style.display = "none"; 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>"; indexedFiles.innerHTML = "<div class='card-description'>No documents synced with Khoj</div>";
} else { } else {
document.getElementById("get-desktop-client").style.display = "none";
document.getElementById("delete-all-files").style.display = "block"; document.getElementById("delete-all-files").style.display = "block";
} }

View File

@@ -272,7 +272,7 @@
<body> <body>
<!--Add Header Logo and Nav Pane--> <!--Add Header Logo and Nav Pane-->
{% import 'utils.html' as utils %} {% import 'utils.html' as utils %}
{{ utils.heading_pane(user_photo, username) }} {{ utils.heading_pane(user_photo, username, is_active, has_documents) }}
<!--Add Text Box To Enter Query, Trigger Incremental Search OnChange --> <!--Add Text Box To Enter Query, Trigger Incremental Search OnChange -->
<input type="text" id="query" class="option" onkeyup=incrementalSearch(event) autofocus="autofocus" placeholder="Search your knowledge base using natural language"> <input type="text" id="query" class="option" onkeyup=incrementalSearch(event) autofocus="autofocus" placeholder="Search your knowledge base using natural language">
@@ -287,9 +287,6 @@
</body> </body>
<script> <script>
document.getElementById("search-nav").classList.add("khoj-nav-selected"); document.getElementById("search-nav").classList.add("khoj-nav-selected");
{% if is_active %}
document.getElementById("profile-picture").classList.add("subscribed");
{% endif %}
</script> </script>
<style> <style>

View File

@@ -1,11 +1,13 @@
{% macro heading_pane(user_photo, username) -%} {% macro heading_pane(user_photo, username, is_active, has_documents) -%}
<div class="khoj-header"> <div class="khoj-header">
<a class="khoj-logo" href="/" target="_blank"> <a class="khoj-logo" href="/" target="_blank">
<img class="khoj-logo" src="/static/assets/icons/khoj-logo-sideways-500.png" alt="Khoj"></img> <img class="khoj-logo" src="/static/assets/icons/khoj-logo-sideways-500.png" alt="Khoj"></img>
</a> </a>
<nav class="khoj-nav"> <nav class="khoj-nav">
<a id="chat-nav" class="khoj-nav" href="/chat">💬 Chat</a> <a id="chat-nav" class="khoj-nav" href="/chat">💬 Chat</a>
{% if has_documents %}
<a id="search-nav" class="khoj-nav" href="/search">🔎 Search</a> <a id="search-nav" class="khoj-nav" href="/search">🔎 Search</a>
{% endif %}
<!-- Dropdown Menu --> <!-- Dropdown Menu -->
<div id="khoj-nav-menu-container" class="khoj-nav dropdown"> <div id="khoj-nav-menu-container" class="khoj-nav dropdown">
{% if user_photo and user_photo != "None" %} {% if user_photo and user_photo != "None" %}
@@ -15,7 +17,11 @@
<img id="profile-picture" class="circle" src="{{ user_photo }}" alt="{{ username[0].upper() }}" onclick="toggleMenu()" referrerpolicy="no-referrer"> <img id="profile-picture" class="circle" src="{{ user_photo }}" alt="{{ username[0].upper() }}" onclick="toggleMenu()" referrerpolicy="no-referrer">
{% endif %} {% endif %}
{% else %} {% else %}
<div class="circle user-initial" alt="{{ username[0].upper() }}" onclick="toggleMenu()">{{ username[0].upper() }}</div> {% if is_active %}
<div id="profile-picture" class="circle user-initial subscribed" alt="{{ username[0].upper() }}" onclick="toggleMenu()">{{ username[0].upper() }}</div>
{% else %}
<div id="profile-picture" class="circle user-initial" alt="{{ username[0].upper() }}" onclick="toggleMenu()">{{ username[0].upper() }}</div>
{% endif %}
{% endif %} {% endif %}
<div id="khoj-nav-menu" class="khoj-nav-dropdown-content"> <div id="khoj-nav-menu" class="khoj-nav-dropdown-content">
<div class="khoj-nav-username"> {{ username }} </div> <div class="khoj-nav-username"> {{ username }} </div>

View File

@@ -74,7 +74,7 @@ logger = logging.getLogger(__name__)
def migrate_server_pg(args): def migrate_server_pg(args):
schema_version = "0.14.0" schema_version = "0.15.0"
raw_config = load_config_from_file(args.config_file) raw_config = load_config_from_file(args.config_file)
previous_version = raw_config.get("version") previous_version = raw_config.get("version")

View File

@@ -326,9 +326,7 @@ def get_config_types(
request: Request, request: Request,
): ):
user = request.user.object user = request.user.object
enabled_file_types = EntryAdapters.get_unique_file_types(user) enabled_file_types = EntryAdapters.get_unique_file_types(user)
configured_content_types = list(enabled_file_types) configured_content_types = list(enabled_file_types)
if state.config and state.config.content_type: if state.config and state.config.content_type:
@@ -665,7 +663,7 @@ async def extract_references_and_questions(
if conversation_type == ConversationCommand.General: if conversation_type == ConversationCommand.General:
return compiled_references, inferred_queries, q return compiled_references, inferred_queries, q
if not await EntryAdapters.user_has_entries(user=user): if not sync_to_async(EntryAdapters.user_has_entries)(user=user):
logger.warning( logger.warning(
"No content index loaded, so cannot extract references from knowledge base. Please configure your data sources and update the index to chat with your notes." "No content index loaded, so cannot extract references from knowledge base. Please configure your data sources and update the index to chat with your notes."
) )

View File

@@ -37,6 +37,8 @@ templates = Jinja2Templates(directory=constants.web_directory)
def index(request: Request): def index(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")
user_subscription_state = get_user_subscription_state(user.email)
has_documents = EntryAdapters.user_has_entries(user=user)
return templates.TemplateResponse( return templates.TemplateResponse(
"chat.html", "chat.html",
@@ -44,6 +46,8 @@ def index(request: Request):
"request": request, "request": request,
"username": user.username, "username": user.username,
"user_photo": user_picture, "user_photo": user_picture,
"is_active": user_subscription_state == "subscribed" or user_subscription_state == "unsubscribed",
"has_documents": has_documents,
}, },
) )
@@ -53,6 +57,8 @@ def index(request: Request):
def index_post(request: Request): def index_post(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")
user_subscription_state = get_user_subscription_state(user.email)
has_documents = EntryAdapters.user_has_entries(user=user)
return templates.TemplateResponse( return templates.TemplateResponse(
"chat.html", "chat.html",
@@ -60,6 +66,8 @@ def index_post(request: Request):
"request": request, "request": request,
"username": user.username, "username": user.username,
"user_photo": user_picture, "user_photo": user_picture,
"is_active": user_subscription_state == "subscribed" or user_subscription_state == "unsubscribed",
"has_documents": has_documents,
}, },
) )
@@ -69,8 +77,8 @@ def index_post(request: Request):
def search_page(request: Request): def search_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")
user_subscription = adapters.get_user_subscription(user.email) user_subscription_state = get_user_subscription_state(user.email)
user_subscription_state = get_user_subscription_state(user_subscription) has_documents = EntryAdapters.user_has_entries(user=user)
return templates.TemplateResponse( return templates.TemplateResponse(
"search.html", "search.html",
@@ -79,6 +87,7 @@ def search_page(request: Request):
"username": user.username, "username": user.username,
"user_photo": user_picture, "user_photo": user_picture,
"is_active": user_subscription_state == "subscribed" or user_subscription_state == "unsubscribed", "is_active": user_subscription_state == "subscribed" or user_subscription_state == "unsubscribed",
"has_documents": has_documents,
}, },
) )
@@ -88,8 +97,8 @@ def search_page(request: Request):
def chat_page(request: Request): def chat_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")
user_subscription = adapters.get_user_subscription(user.email) user_subscription_state = get_user_subscription_state(user.email)
user_subscription_state = get_user_subscription_state(user_subscription) has_documents = EntryAdapters.user_has_entries(user=user)
return templates.TemplateResponse( return templates.TemplateResponse(
"chat.html", "chat.html",
@@ -98,6 +107,7 @@ def chat_page(request: Request):
"username": user.username, "username": user.username,
"user_photo": user_picture, "user_photo": user_picture,
"is_active": user_subscription_state == "subscribed" or user_subscription_state == "unsubscribed", "is_active": user_subscription_state == "subscribed" or user_subscription_state == "unsubscribed",
"has_documents": has_documents,
}, },
) )
@@ -124,28 +134,29 @@ def login_page(request: Request):
def config_page(request: Request): def config_page(request: Request):
user: KhojUser = request.user.object user: KhojUser = request.user.object
user_picture = request.session.get("user", {}).get("picture") user_picture = request.session.get("user", {}).get("picture")
has_documents = EntryAdapters.user_has_entries(user=user)
user_subscription_state = get_user_subscription_state(user.email)
user_subscription = adapters.get_user_subscription(user.email) user_subscription = adapters.get_user_subscription(user.email)
user_subscription_state = get_user_subscription_state(user_subscription)
subscription_renewal_date = ( subscription_renewal_date = (
user_subscription.renewal_date.strftime("%d %b %Y") user_subscription.renewal_date.strftime("%d %b %Y")
if user_subscription and user_subscription.renewal_date if user_subscription and user_subscription.renewal_date
else None else None
) )
enabled_content_source = set(EntryAdapters.get_unique_file_source(user).all())
enabled_content_source = set(EntryAdapters.get_unique_file_sources(user))
successfully_configured = { successfully_configured = {
"computer": ("computer" in enabled_content_source), "computer": ("computer" in enabled_content_source),
"github": ("github" in enabled_content_source), "github": ("github" in enabled_content_source),
"notion": ("notion" in enabled_content_source), "notion": ("notion" in enabled_content_source),
} }
selected_conversation_config = ConversationAdapters.get_conversation_config(user)
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:
all_conversation_options.append({"chat_model": conversation_option.chat_model, "id": conversation_option.id}) all_conversation_options.append({"chat_model": conversation_option.chat_model, "id": conversation_option.id})
selected_conversation_config = ConversationAdapters.get_conversation_config(user)
return templates.TemplateResponse( return templates.TemplateResponse(
"config.html", "config.html",
context={ context={
@@ -161,6 +172,7 @@ def config_page(request: Request):
"subscription_renewal_date": subscription_renewal_date, "subscription_renewal_date": subscription_renewal_date,
"khoj_cloud_subscription_url": os.getenv("KHOJ_CLOUD_SUBSCRIPTION_URL"), "khoj_cloud_subscription_url": os.getenv("KHOJ_CLOUD_SUBSCRIPTION_URL"),
"is_active": user_subscription_state == "subscribed" or user_subscription_state == "unsubscribed", "is_active": user_subscription_state == "subscribed" or user_subscription_state == "unsubscribed",
"has_documents": has_documents,
}, },
) )
@@ -170,6 +182,8 @@ def config_page(request: Request):
def github_config_page(request: Request): def github_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")
user_subscription_state = get_user_subscription_state(user.email)
has_documents = EntryAdapters.user_has_entries(user=user)
current_github_config = get_user_github_config(user) current_github_config = get_user_github_config(user)
if current_github_config: if current_github_config:
@@ -198,6 +212,8 @@ def github_config_page(request: Request):
"current_config": current_config, "current_config": current_config,
"username": user.username, "username": user.username,
"user_photo": user_picture, "user_photo": user_picture,
"is_active": user_subscription_state == "subscribed" or user_subscription_state == "unsubscribed",
"has_documents": has_documents,
}, },
) )
@@ -207,6 +223,8 @@ def github_config_page(request: Request):
def notion_config_page(request: Request): def notion_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")
user_subscription_state = adapters.get_user_subscription(user.email)
has_documents = EntryAdapters.user_has_entries(user=user)
current_notion_config = get_user_notion_config(user) current_notion_config = get_user_notion_config(user)
current_config = NotionContentConfig( current_config = NotionContentConfig(
@@ -222,6 +240,8 @@ def notion_config_page(request: Request):
"current_config": current_config, "current_config": current_config,
"username": user.username, "username": user.username,
"user_photo": user_picture, "user_photo": user_picture,
"is_active": user_subscription_state == "subscribed" or user_subscription_state == "unsubscribed",
"has_documents": has_documents,
}, },
) )
@@ -231,6 +251,8 @@ def notion_config_page(request: Request):
def computer_config_page(request: Request): def computer_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")
user_subscription_state = get_user_subscription_state(user.email)
has_documents = EntryAdapters.user_has_entries(user=user)
return templates.TemplateResponse( return templates.TemplateResponse(
"content_source_computer_input.html", "content_source_computer_input.html",
@@ -238,5 +260,7 @@ def computer_config_page(request: Request):
"request": request, "request": request,
"username": user.username, "username": user.username,
"user_photo": user_picture, "user_photo": user_picture,
"is_active": user_subscription_state == "subscribed" or user_subscription_state == "unsubscribed",
"has_documents": has_documents,
}, },
) )

View File

@@ -71,7 +71,7 @@ def cli(args=None):
else: else:
args = run_migrations(args) args = run_migrations(args)
args.config = parse_config_from_file(args.config_file) args.config = parse_config_from_file(args.config_file)
if os.environ.get("DEBUG"): if os.environ.get("KHOJ_DEBUG"):
args.config.app.should_log_telemetry = False args.config.app.should_log_telemetry = False
return args return args