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")
# 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"]

View File

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

View File

@@ -91,10 +91,7 @@
</div>
<div class="section-action-row">
<div class="card-description-row">
<button id="sync-data" class="sync-data">💾 Save</button>
</div>
<div class="card-description-row">
<button id="sync-force" class="sync-data">💾 Save All</button>
<button id="sync-force" class="sync-data">💾 Save</button>
</div>
<div class="card-description-row">
<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 axios = require('axios');
const KHOJ_URL = 'http://127.0.0.1:42110'
const KHOJ_URL = 'https://app.khoj.dev';
const Store = require('electron-store');
@@ -67,7 +67,7 @@ const schema = {
}
};
const syncing = false;
let syncing = false;
var state = {}
const store = new Store({ schema });

View File

@@ -155,11 +155,14 @@ window.updateStateAPI.onUpdateState((event, state) => {
loadingBar.style.display = 'none';
let syncStatusElement = document.getElementById("sync-status");
const currentTime = new Date();
nextSyncTime = new Date();
nextSyncTime.setMinutes(Math.ceil((nextSyncTime.getMinutes() + 1) / 10) * 10);
if (state.completed == false) {
syncStatusElement.innerHTML = `Sync was unsuccessful at ${currentTime.toLocaleTimeString()}. Contact team@khoj.dev to report this issue.`;
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');

View File

@@ -15,7 +15,7 @@
<!--Add Header Logo and Nav Pane-->
{% 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>
@@ -26,9 +26,6 @@
</body>
<script>
document.getElementById("settings-nav").classList.add("khoj-nav-selected");
{% if is_active %}
document.getElementById("profile-picture").classList.add("subscribed");
{% endif %}
</script>
<style>
html, body {

View File

@@ -10,6 +10,16 @@
</head>
<script type="text/javascript" src="/static/assets/utils.js"></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 = [];
function copyProgrammaticOutput(event) {
// 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");
} else {
// Set welcome message on load
renderMessage("Hey 👋🏾, what's up?", "khoj");
renderMessage(welcome_message, "khoj");
}
return data.response;
})
@@ -363,7 +373,7 @@
<!--Add Header Logo and Nav Pane-->
{% import 'utils.html' as utils %}
{{ utils.heading_pane(user_photo, username) }}
{{ utils.heading_pane(user_photo, username, is_active, has_documents) }}
<!-- Chat Body -->
<div id="chat-body"></div>
@@ -376,9 +386,6 @@
</body>
<script>
document.getElementById("chat-nav").classList.add("khoj-nav-selected");
{% if is_active %}
document.getElementById("profile-picture").classList.add("subscribed");
{% endif %}
</script>
<style>
html, body {

View File

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

View File

@@ -272,7 +272,7 @@
<body>
<!--Add Header Logo and Nav Pane-->
{% 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 -->
<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>
<script>
document.getElementById("search-nav").classList.add("khoj-nav-selected");
{% if is_active %}
document.getElementById("profile-picture").classList.add("subscribed");
{% endif %}
</script>
<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">
<a class="khoj-logo" href="/" target="_blank">
<img class="khoj-logo" src="/static/assets/icons/khoj-logo-sideways-500.png" alt="Khoj"></img>
</a>
<nav class="khoj-nav">
<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>
{% endif %}
<!-- Dropdown Menu -->
<div id="khoj-nav-menu-container" class="khoj-nav dropdown">
{% 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">
{% endif %}
{% 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 %}
<div id="khoj-nav-menu" class="khoj-nav-dropdown-content">
<div class="khoj-nav-username"> {{ username }} </div>

View File

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

View File

@@ -326,9 +326,7 @@ def get_config_types(
request: Request,
):
user = request.user.object
enabled_file_types = EntryAdapters.get_unique_file_types(user)
configured_content_types = list(enabled_file_types)
if state.config and state.config.content_type:
@@ -665,7 +663,7 @@ async def extract_references_and_questions(
if conversation_type == ConversationCommand.General:
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(
"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):
user = request.user.object
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(
"chat.html",
@@ -44,6 +46,8 @@ def index(request: Request):
"request": request,
"username": user.username,
"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):
user = request.user.object
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(
"chat.html",
@@ -60,6 +66,8 @@ def index_post(request: Request):
"request": request,
"username": user.username,
"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):
user = request.user.object
user_picture = request.session.get("user", {}).get("picture")
user_subscription = adapters.get_user_subscription(user.email)
user_subscription_state = get_user_subscription_state(user_subscription)
user_subscription_state = get_user_subscription_state(user.email)
has_documents = EntryAdapters.user_has_entries(user=user)
return templates.TemplateResponse(
"search.html",
@@ -79,6 +87,7 @@ def search_page(request: Request):
"username": user.username,
"user_photo": user_picture,
"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):
user = request.user.object
user_picture = request.session.get("user", {}).get("picture")
user_subscription = adapters.get_user_subscription(user.email)
user_subscription_state = get_user_subscription_state(user_subscription)
user_subscription_state = get_user_subscription_state(user.email)
has_documents = EntryAdapters.user_has_entries(user=user)
return templates.TemplateResponse(
"chat.html",
@@ -98,6 +107,7 @@ def chat_page(request: Request):
"username": user.username,
"user_photo": user_picture,
"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):
user: KhojUser = request.user.object
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_state = get_user_subscription_state(user_subscription)
subscription_renewal_date = (
user_subscription.renewal_date.strftime("%d %b %Y")
if user_subscription and user_subscription.renewal_date
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 = {
"computer": ("computer" in enabled_content_source),
"github": ("github" 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()
all_conversation_options = list()
for conversation_option in conversation_options:
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(
"config.html",
context={
@@ -161,6 +172,7 @@ def config_page(request: Request):
"subscription_renewal_date": subscription_renewal_date,
"khoj_cloud_subscription_url": os.getenv("KHOJ_CLOUD_SUBSCRIPTION_URL"),
"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):
user = request.user.object
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)
if current_github_config:
@@ -198,6 +212,8 @@ def github_config_page(request: Request):
"current_config": current_config,
"username": user.username,
"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):
user = request.user.object
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_config = NotionContentConfig(
@@ -222,6 +240,8 @@ def notion_config_page(request: Request):
"current_config": current_config,
"username": user.username,
"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):
user = request.user.object
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(
"content_source_computer_input.html",
@@ -238,5 +260,7 @@ def computer_config_page(request: Request):
"request": request,
"username": user.username,
"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:
args = run_migrations(args)
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
return args