mirror of
https://github.com/khoaliber/khoj.git
synced 2026-03-06 13:22:12 +00:00
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:
@@ -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"]
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 });
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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."
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user