mirror of
https://github.com/khoaliber/khoj.git
synced 2026-03-03 05:29:12 +00:00
530 lines
19 KiB
HTML
530 lines
19 KiB
HTML
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0 maximum-scale=1.0">
|
|
<title>Khoj - Search</title>
|
|
|
|
<link rel="icon" type="image/png" sizes="128x128" href="/static/assets/icons/favicon-128x128.png">
|
|
<link rel="manifest" href="/static/khoj.webmanifest">
|
|
<link rel="stylesheet" href="/static/assets/khoj.css">
|
|
</head>
|
|
<script type="text/javascript" src="/static/assets/org.min.js"></script>
|
|
<script type="text/javascript" src="/static/assets/markdown-it.min.js"></script>
|
|
|
|
<script>
|
|
function render_image(item) {
|
|
return `
|
|
<div class="results-image">
|
|
<a href="${item.entry}" class="image-link">
|
|
<img id=${item.score} src="${item.entry}?${Math.random()}"
|
|
title="Effective Score: ${item.score}, Meta: ${item.additional.metadata_score}, Image: ${item.additional.image_score}"
|
|
class="image">
|
|
</a>
|
|
</div>`;
|
|
}
|
|
|
|
function render_org(query, data, classPrefix="") {
|
|
return data.map(function (item) {
|
|
var orgParser = new Org.Parser();
|
|
var orgDocument = orgParser.parse(item.entry);
|
|
var orgHTMLDocument = orgDocument.convert(Org.ConverterHTML, { htmlClassPrefix: classPrefix });
|
|
return `<div class="results-org">` + orgHTMLDocument.toString() + `</div>`;
|
|
}).join("\n");
|
|
}
|
|
|
|
function render_markdown(query, data) {
|
|
var md = window.markdownit();
|
|
return data.map(function (item) {
|
|
let rendered = "";
|
|
if (item.additional.file.startsWith("http")) {
|
|
lines = item.entry.split("\n");
|
|
rendered = md.render(`${lines[0]}\t[*](${item.additional.file})\n${lines.slice(1).join("\n")}`);
|
|
}
|
|
else {
|
|
rendered = md.render(`${item.entry}`);
|
|
}
|
|
return `<div class="results-markdown">` + rendered + `</div>`;
|
|
}).join("\n");
|
|
}
|
|
|
|
function render_ledger(query, data) {
|
|
return data.map(function (item) {
|
|
return `<div class="results-ledger">` + `<p>${item.entry}</p>` + `</div>`;
|
|
}).join("\n");
|
|
}
|
|
|
|
function render_pdf(query, data) {
|
|
return data.map(function (item) {
|
|
let compiled_lines = item.additional.compiled.split("\n");
|
|
let filename = compiled_lines.shift();
|
|
let text_match = compiled_lines.join("\n")
|
|
return `<div class="results-pdf">` + `<h2>${filename}</h2>\n<p>${text_match}</p>` + `</div>`;
|
|
}).join("\n");
|
|
}
|
|
|
|
function render_multiple(query, data, type) {
|
|
let html = "";
|
|
data.forEach(item => {
|
|
if (item.additional.file.endsWith(".org")) {
|
|
html += render_org(query, [item], "org-");
|
|
} else if (
|
|
item.additional.file.endsWith(".md") ||
|
|
item.additional.file.endsWith(".markdown") ||
|
|
(item.additional.file.includes("issues") && item.additional.file.includes("github.com"))
|
|
)
|
|
{
|
|
html += render_markdown(query, [item]);
|
|
} else if (item.additional.file.endsWith(".pdf")) {
|
|
html += render_pdf(query, [item]);
|
|
}
|
|
});
|
|
return html;
|
|
}
|
|
|
|
function render_results(data, query, type) {
|
|
let results = "";
|
|
if (type === "markdown") {
|
|
results = render_markdown(query, data);
|
|
} else if (type === "org") {
|
|
results = render_org(query, data, "org-");
|
|
} else if (type === "music") {
|
|
results = render_org(query, data, "music-");
|
|
} else if (type === "image") {
|
|
results = data.map(render_image).join('');
|
|
} else if (type === "ledger") {
|
|
results = render_ledger(query, data);
|
|
} else if (type === "pdf") {
|
|
results = render_pdf(query, data);
|
|
} else if (type === "github" || type === "all") {
|
|
results = render_multiple(query, data, type);
|
|
} else {
|
|
results = data.map((item) => `<div class="results-plugin">` + `<p>${item.entry}</p>` + `</div>`).join("\n")
|
|
}
|
|
|
|
// Any POST rendering goes here.
|
|
|
|
let renderedResults = document.createElement("div");
|
|
renderedResults.id = `results-${type}`;
|
|
renderedResults.innerHTML = results;
|
|
|
|
// For all elements that are of type img in the results html and have a src with 'avatar' in the URL, add the class 'avatar'
|
|
// This is used to make the avatar images round
|
|
let images = renderedResults.querySelectorAll("img[src*='avatar']");
|
|
for (let i = 0; i < images.length; i++) {
|
|
images[i].classList.add("avatar");
|
|
}
|
|
|
|
return renderedResults.outerHTML;
|
|
}
|
|
|
|
function search(rerank=false) {
|
|
// Extract required fields for search from form
|
|
query = document.getElementById("query").value.trim();
|
|
type = document.getElementById("type").value;
|
|
results_count = document.getElementById("results-count").value || 6;
|
|
console.log(`Query: ${query}, Type: ${type}`);
|
|
|
|
// Short circuit on empty query
|
|
if (query.length === 0)
|
|
return;
|
|
|
|
// If set query field in url query param on rerank
|
|
if (rerank)
|
|
setQueryFieldInUrl(query);
|
|
|
|
// Execute Search and Render Results
|
|
url = createRequestUrl(query, type, results_count, rerank);
|
|
fetch(url)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
console.log(data);
|
|
document.getElementById("results").innerHTML = render_results(data, query, type);
|
|
});
|
|
}
|
|
|
|
function updateIndex() {
|
|
type = document.getElementById("type").value;
|
|
fetch(`/api/update?t=${type}&client=web`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
console.log(data);
|
|
document.getElementById("results").innerHTML =
|
|
render_results(data);
|
|
});
|
|
}
|
|
|
|
function incrementalSearch(event) {
|
|
type = document.getElementById("type").value;
|
|
// Search with reranking on 'Enter'
|
|
if (event.key === 'Enter') {
|
|
search(rerank=true);
|
|
}
|
|
// Limit incremental search to text types
|
|
else if (type !== "image") {
|
|
search(rerank=false);
|
|
}
|
|
}
|
|
|
|
function populate_type_dropdown() {
|
|
// Populate type dropdown field with enabled content types only
|
|
fetch("/api/config/types")
|
|
.then(response => response.json())
|
|
.then(enabled_types => {
|
|
document.getElementById("type").innerHTML =
|
|
enabled_types
|
|
.map(type => `<option value="${type}">${type.slice(0,1).toUpperCase() + type.slice(1)}</option>`)
|
|
.join('');
|
|
|
|
return enabled_types;
|
|
})
|
|
.then(enabled_types => {
|
|
// Set type field to content type passed in URL query parameter, if valid
|
|
var type_via_url = new URLSearchParams(window.location.search).get("t");
|
|
if (type_via_url && enabled_types.includes(type_via_url))
|
|
document.getElementById("type").value = type_via_url;
|
|
});
|
|
}
|
|
|
|
function createRequestUrl(query, type, results_count, rerank) {
|
|
// Generate Backend API URL to execute Search
|
|
let url = `/api/search?q=${encodeURIComponent(query)}&n=${results_count}&client=web`;
|
|
// If type is not 'all', append type to URL
|
|
if (type !== 'all')
|
|
url += `&t=${type}`;
|
|
// Rerank is only supported by text types
|
|
if (type !== "image")
|
|
url += `&r=${rerank}`;
|
|
return url;
|
|
}
|
|
|
|
function setTypeFieldInUrl(type) {
|
|
var url = new URL(window.location.href);
|
|
url.searchParams.set("t", type.value);
|
|
window.history.pushState({}, "", url.href);
|
|
}
|
|
|
|
function setCountFieldInUrl(results_count) {
|
|
var url = new URL(window.location.href);
|
|
url.searchParams.set("n", results_count.value);
|
|
window.history.pushState({}, "", url.href);
|
|
}
|
|
|
|
function setQueryFieldInUrl(query) {
|
|
var url = new URL(window.location.href);
|
|
url.searchParams.set("q", query);
|
|
window.history.pushState({}, "", url.href);
|
|
}
|
|
|
|
window.onload = function () {
|
|
// Dynamically populate type dropdown based on enabled content types and type passed as URL query parameter
|
|
populate_type_dropdown();
|
|
|
|
// Set results count field with value passed in URL query parameters, if any.
|
|
var results_count = new URLSearchParams(window.location.search).get("n");
|
|
if (results_count)
|
|
document.getElementById("results-count").value = results_count;
|
|
|
|
// Fill query field with value passed in URL query parameters, if any.
|
|
var query_via_url = new URLSearchParams(window.location.search).get("q");
|
|
if (query_via_url)
|
|
document.getElementById("query").value = query_via_url;
|
|
}
|
|
</script>
|
|
|
|
<body>
|
|
<!--Add Header Logo and Nav Pane-->
|
|
<div class="khoj-header">
|
|
{% if demo %}
|
|
<!-- Banner linking to https://khoj.dev -->
|
|
<div class="khoj-banner-container">
|
|
<a class="khoj-banner" href="https://khoj.dev" target="_blank">
|
|
<p id="khoj-banner" class="khoj-banner">
|
|
Enroll in Khoj cloud to get your own Github assistant
|
|
</p>
|
|
</a>
|
|
<input type="text" id="khoj-banner-email" placeholder="email" class="khoj-banner-email"></input>
|
|
<button id="khoj-banner-submit" class="khoj-banner-button">Submit</button>
|
|
</div>
|
|
<a class="khoj-logo" href="https://khoj.dev" target="_blank">
|
|
<img class="khoj-logo" src="/static/assets/icons/khoj-logo-sideways.svg" alt="Khoj"></img>
|
|
</a>
|
|
{% else %}
|
|
<a class="khoj-logo" href="https://lantern.khoj.dev" target="_blank">
|
|
<img class="khoj-logo" src="/static/assets/icons/khoj-logo-sideways.svg" alt="Khoj"></img>
|
|
</a>
|
|
{% endif %}
|
|
<nav class="khoj-nav">
|
|
<a href="/chat">Chat</a>
|
|
<a class="khoj-nav-selected" href="/">Search</a>
|
|
{% if not demo %}
|
|
<a href="/config">Settings</a>
|
|
{% endif %}
|
|
</nav>
|
|
</div>
|
|
|
|
<!--Add Text Box To Enter Query, Trigger Incremental Search OnChange -->
|
|
<input type="text" id="query" class="option" onkeyup=incrementalSearch(event) autofocus="autofocus" placeholder="What is the meaning of life?">
|
|
|
|
<div id="options">
|
|
<!--Add Dropdown to Select Query Type -->
|
|
<select id="type" class="option" onchange="setTypeFieldInUrl(this)"></select>
|
|
|
|
<!--Add Button To Regenerate -->
|
|
<button id="update" class="option" onclick="updateIndex()">Update</button>
|
|
|
|
<!--Add Results Count Input To Set Results Count -->
|
|
<input type="number" id="results-count" min="1" max="100" value="6" placeholder="results count" onchange="setCountFieldInUrl(this)">
|
|
</div>
|
|
|
|
<!-- Section to Render Results -->
|
|
<div id="results"></div>
|
|
</body>
|
|
|
|
<style>
|
|
@media only screen and (max-width: 600px) {
|
|
body {
|
|
display: grid;
|
|
grid-template-columns: 1fr;
|
|
grid-template-rows: 1fr auto auto minmax(80px, 100%);
|
|
}
|
|
body > * {
|
|
grid-column: 1;
|
|
}
|
|
}
|
|
@media only screen and (min-width: 600px) {
|
|
body {
|
|
display: grid;
|
|
grid-template-columns: 1fr min(70vw, 100%) 1fr;
|
|
grid-template-rows: 1fr auto auto minmax(80px, 100%);
|
|
padding-top: 60vw;
|
|
}
|
|
body > * {
|
|
grid-column: 2;
|
|
}
|
|
}
|
|
body {
|
|
padding: 0px;
|
|
margin: 0px;
|
|
background: #f8fafc;
|
|
color: #475569;
|
|
font-family: roboto, karma, segoe ui, sans-serif;
|
|
font-size: 20px;
|
|
font-weight: 300;
|
|
line-height: 1.5em;
|
|
}
|
|
body > * {
|
|
padding: 10px;
|
|
margin: 10px;
|
|
}
|
|
#options {
|
|
padding: 0;
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr minmax(70px, 0.5fr);
|
|
}
|
|
#options > * {
|
|
padding: 15px;
|
|
border-radius: 5px;
|
|
border: 1px solid #475569;
|
|
background: #f9fafc
|
|
}
|
|
.option:hover {
|
|
box-shadow: 0 0 11px #aaa;
|
|
}
|
|
#options > select {
|
|
margin-right: 10px;
|
|
}
|
|
#options > button {
|
|
margin-right: 10px;
|
|
}
|
|
|
|
#query {
|
|
font-size: larger;
|
|
}
|
|
#results {
|
|
font-size: medium;
|
|
margin: 0px;
|
|
line-height: 20px;
|
|
}
|
|
.results-image {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
}
|
|
.image-link {
|
|
place-self: center;
|
|
}
|
|
.image {
|
|
width: 20vw;
|
|
border-radius: 10px;
|
|
border: 1px solid #475569;
|
|
}
|
|
#json {
|
|
white-space: pre-wrap;
|
|
}
|
|
.results-pdf,
|
|
.results-plugin,
|
|
.results-ledger {
|
|
text-align: left;
|
|
white-space: pre-line;
|
|
}
|
|
.results-markdown,
|
|
.results-github {
|
|
text-align: left;
|
|
}
|
|
.results-music,
|
|
.results-org {
|
|
text-align: left;
|
|
white-space: pre-line;
|
|
}
|
|
.results-music h3,
|
|
.results-org h3 {
|
|
margin: 20px 0 0 0;
|
|
font-size: larger;
|
|
}
|
|
span.music-task-status,
|
|
span.org-task-status {
|
|
color: white;
|
|
padding: 3.5px 3.5px 0;
|
|
margin-right: 5px;
|
|
border-radius: 5px;
|
|
background-color: #eab308;
|
|
font-size: medium;
|
|
}
|
|
span.music-task-status.todo,
|
|
span.org-task-status.todo {
|
|
background-color: #3b82f6
|
|
}
|
|
span.music-task-status.done,
|
|
span.org-task-status.done {
|
|
background-color: #22c55e;
|
|
}
|
|
span.music-task-tag,
|
|
span.org-task-tag {
|
|
color: white;
|
|
padding: 3.5px 3.5px 0;
|
|
margin-right: 5px;
|
|
border-radius: 5px;
|
|
border: 1px solid #475569;
|
|
background-color: #ef4444;
|
|
font-size: small;
|
|
}
|
|
|
|
pre {
|
|
max-width: 100;
|
|
}
|
|
|
|
a {
|
|
color: #3b82f6;
|
|
text-decoration: none;
|
|
}
|
|
|
|
img.avatar {
|
|
width: 20px;
|
|
height: 20px;
|
|
border-radius: 50%;
|
|
}
|
|
|
|
div.results-markdown,
|
|
div.results-org,
|
|
div.results-pdf {
|
|
text-align: left;
|
|
box-shadow: 2px 2px 2px var(--primary-hover);
|
|
border-radius: 5px;
|
|
padding: 10px;
|
|
margin: 10px 0;
|
|
border: 4px solid rgb(229, 229, 229);
|
|
}
|
|
|
|
img {
|
|
max-width: 90%;
|
|
}
|
|
|
|
div.khoj-banner-container {
|
|
background: linear-gradient(-45deg, #FFC107, #FF9800, #FF5722, #FF9800, #FFC107);
|
|
background-size: 400% 400%;
|
|
animation: gradient 15s ease infinite;
|
|
text-align: center;
|
|
padding: 10px;
|
|
}
|
|
|
|
@keyframes gradient {
|
|
0% {
|
|
background-position: 0% 50%;
|
|
}
|
|
50% {
|
|
background-position: 100% 50%;
|
|
}
|
|
100% {
|
|
background-position: 0% 50%;
|
|
}
|
|
}
|
|
|
|
a.khoj-banner {
|
|
color: black;
|
|
}
|
|
|
|
a.khoj-logo {
|
|
text-align: center;
|
|
}
|
|
|
|
p.khoj-banner {
|
|
margin: 0;
|
|
padding: 10px;
|
|
}
|
|
|
|
button#khoj-banner-submit,
|
|
input#khoj-banner-email {
|
|
padding: 10px;
|
|
border-radius: 5px;
|
|
border: 1px solid #475569;
|
|
background: #f9fafc;
|
|
}
|
|
|
|
button#khoj-banner-submit:hover,
|
|
input#khoj-banner-email:hover {
|
|
box-shadow: 0 0 11px #aaa;
|
|
}
|
|
|
|
p#khoj-banner {
|
|
display: inline;
|
|
}
|
|
|
|
@media only screen and (max-width: 600px) {
|
|
a.khoj-banner {
|
|
display: block;
|
|
}
|
|
}
|
|
|
|
</style>
|
|
<script>
|
|
var khojBannerSubmit = document.getElementById("khoj-banner-submit");
|
|
|
|
khojBannerSubmit.addEventListener("click", function(event) {
|
|
event.preventDefault();
|
|
var email = document.getElementById("khoj-banner-email").value;
|
|
fetch("https://lantern.khoj.dev/beta/users/", {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
email: email
|
|
}),
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
}
|
|
}).then(function(response) {
|
|
return response.json();
|
|
}).then(function(data) {
|
|
console.log(data);
|
|
if (data.user != null) {
|
|
document.getElementById("khoj-banner").innerHTML = "Thanks for signing up. We'll be in touch soon! 🚀";
|
|
document.getElementById("khoj-banner-submit").remove();
|
|
} else {
|
|
document.getElementById("khoj-banner").innerHTML = "There was an error signing up. Please contact team@khoj.dev";
|
|
}
|
|
}).catch(function(error) {
|
|
console.log(error);
|
|
document.getElementById("khoj-banner").innerHTML = "There was an error signing up. Please contact team@khoj.dev";
|
|
});
|
|
});
|
|
</script>
|
|
|
|
</html>
|