mirror of
https://github.com/khoj-ai/khoj.git
synced 2024-12-19 10:57:45 +00:00
44c8d09342
Wait for 300ms since stop typing before calling search API. This smooths out UI jitter when rendering search results, especially now that we're reranking for every search query on GPU enabled devices Emacs already has 300ms debounce time. More convoluted to add debounce time to Obsidian search modal, so not updating that yet
456 lines
17 KiB
HTML
456 lines
17 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="./assets/icons/favicon-128x128.png">
|
|
<link rel="manifest" href="./khoj.webmanifest">
|
|
<link rel="stylesheet" href="./assets/khoj.css">
|
|
</head>
|
|
<script type="text/javascript" src="./assets/org.min.js"></script>
|
|
<script type="text/javascript" src="./assets/markdown-it.min.js"></script>
|
|
<script src="./utils.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, suppressNewLines: true });
|
|
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_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_html(query, data) {
|
|
return data.map(function (item) {
|
|
let document = new DOMParser().parseFromString(item.entry, "text/html");
|
|
// Scrub the HTML to remove any script tags and associated content
|
|
let script_tags = document.querySelectorAll("script");
|
|
for (let i = 0; i < script_tags.length; i++) {
|
|
script_tags[i].remove();
|
|
}
|
|
// Scrub the HTML to remove any style tags and associated content
|
|
let style_tags = document.querySelectorAll("style");
|
|
for (let i = 0; i < style_tags.length; i++) {
|
|
style_tags[i].remove();
|
|
}
|
|
// Scrub the HTML to remove any noscript tags and associated content
|
|
let noscript_tags = document.querySelectorAll("noscript");
|
|
for (let i = 0; i < noscript_tags.length; i++) {
|
|
noscript_tags[i].remove();
|
|
}
|
|
// Scrub the HTML to remove any iframe tags and associated content
|
|
let iframe_tags = document.querySelectorAll("iframe");
|
|
for (let i = 0; i < iframe_tags.length; i++) {
|
|
iframe_tags[i].remove();
|
|
}
|
|
// Scrub the HTML to remove any object tags and associated content
|
|
let object_tags = document.querySelectorAll("object");
|
|
for (let i = 0; i < object_tags.length; i++) {
|
|
object_tags[i].remove();
|
|
}
|
|
// Scrub the HTML to remove any embed tags and associated content
|
|
let embed_tags = document.querySelectorAll("embed");
|
|
for (let i = 0; i < embed_tags.length; i++) {
|
|
embed_tags[i].remove();
|
|
}
|
|
let scrubbedHTML = document.body.outerHTML;
|
|
return `<div class="results-html">` + scrubbedHTML + `</div>`;
|
|
}).join("\n");
|
|
}
|
|
|
|
function render_xml(query, data) {
|
|
return data.map(function (item) {
|
|
return `<div class="results-xml">` +
|
|
`<b><a href="${item.additional.file}">${item.additional.heading}</a></b>` +
|
|
`<xml>${item.entry}</xml>` +
|
|
`</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.source === "github") ||
|
|
(item.additional.file.includes("commit") && item.additional.source === "github")
|
|
)
|
|
{
|
|
html += render_markdown(query, [item]);
|
|
} else if (item.additional.file.endsWith(".pdf")) {
|
|
html += render_pdf(query, [item]);
|
|
} else if (item.additional.source == "notion") {
|
|
html += `<div class="results-notion">` + `<b><a href="${item.additional.file}">${item.additional.heading}</a></b>` + `<p>${item.entry}</p>` + `</div>`;
|
|
} else if (item.additional.file.endsWith(".html")) {
|
|
html += render_html(query, [item]);
|
|
} else if (item.additional.file.endsWith(".xml")) {
|
|
html += render_xml(query, [item])
|
|
} else {
|
|
html += `<div class="results-plugin">` + `<b><a href="${item.additional.file}">${item.additional.heading}</a></b>` + `<p>${item.entry}</p>` + `</div>`;
|
|
}
|
|
});
|
|
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 === "image") {
|
|
results = data.map(render_image).join('');
|
|
} else if (type === "pdf") {
|
|
results = render_pdf(query, data);
|
|
} else if (type === "github" || type === "all" || type === "notion") {
|
|
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;
|
|
}
|
|
|
|
async function search(rerank=false) {
|
|
// Extract required fields for search from form
|
|
query = document.getElementById("query").value.trim();
|
|
type = 'all';
|
|
results_count = localStorage.getItem("khojResultsCount") || 5;
|
|
console.log(`Query: ${query}, Type: ${type}, Results Count: ${results_count}`);
|
|
|
|
// 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 = await createRequestUrl(query, type, results_count || 5, rerank);
|
|
const khojToken = await window.tokenAPI.getToken();
|
|
const headers = { 'Authorization': `Bearer ${khojToken}` };
|
|
|
|
fetch(url, { headers })
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
document.getElementById("results").innerHTML = render_results(data, query, type);
|
|
});
|
|
}
|
|
|
|
let debounceTimeout;
|
|
function incrementalSearch(event) {
|
|
// Run incremental search only after waitTime passed since the last key press
|
|
let waitTime = 300;
|
|
clearTimeout(debounceTimeout);
|
|
debounceTimeout = setTimeout(() => {
|
|
type = 'all';
|
|
// Search with reranking on 'Enter'
|
|
let should_rerank = event.key === 'Enter';
|
|
search(rerank=should_rerank);
|
|
}, waitTime);
|
|
}
|
|
|
|
async function populate_type_dropdown() {
|
|
const hostURL = await window.hostURLAPI.getURL();
|
|
const khojToken = await window.tokenAPI.getToken();
|
|
const headers = { 'Authorization': `Bearer ${khojToken}` };
|
|
|
|
// Populate type dropdown field with enabled content types only
|
|
fetch(`${hostURL}/api/config/types`, { headers })
|
|
.then(response => response.json())
|
|
.then(enabled_types => {
|
|
// Show warning if no content types are enabled
|
|
if (enabled_types.detail) {
|
|
document.getElementById("results").innerHTML = "<div id='results-error'>To use Khoj search, setup your content plugins on the Khoj <a class='inline-chat-link' href='/config'>settings page</a>.</div>";
|
|
document.getElementById("query").setAttribute("disabled", "disabled");
|
|
document.getElementById("query").setAttribute("placeholder", "Configure Khoj to enable search");
|
|
return [];
|
|
}
|
|
|
|
return enabled_types;
|
|
});
|
|
}
|
|
|
|
async function createRequestUrl(query, type, results_count, rerank) {
|
|
// Generate Backend API URL to execute Search
|
|
const hostURL = await window.hostURLAPI.getURL();
|
|
|
|
let url = `${hostURL}/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 setQueryFieldInUrl(query) {
|
|
var url = new URL(window.location.href);
|
|
url.searchParams.set("q", query);
|
|
window.history.pushState({}, "", url.href);
|
|
}
|
|
|
|
window.addEventListener("load", async function() {
|
|
// Dynamically populate type dropdown based on enabled content types and type passed as URL query parameter
|
|
await populate_type_dropdown();
|
|
|
|
// 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">
|
|
<a class="khoj-logo" href="./index.html">
|
|
<img class="khoj-logo" src="./assets/icons/khoj-logo-sideways-500.png" alt="Khoj"></img>
|
|
</a>
|
|
<nav class="khoj-nav">
|
|
<a class="khoj-nav" href="./chat.html">💬 Chat</a>
|
|
<a class="khoj-nav khoj-nav-selected" href="./search.html">🔎 Search</a>
|
|
<a class="khoj-nav" href="./config.html">⚙️ Settings</a>
|
|
</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="Search your knowledge base using natural language">
|
|
|
|
<!-- 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 auto minmax(80px, 100%);
|
|
font-size: small!important;
|
|
}
|
|
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 auto minmax(80px, 100%);
|
|
padding-top: 60vw;
|
|
}
|
|
body > * {
|
|
grid-column: 2;
|
|
}
|
|
}
|
|
body {
|
|
padding: 0px;
|
|
margin: 0px;
|
|
background: var(--background-color);
|
|
color: var(--main-text-color);
|
|
font-family: var(--font-family);
|
|
font-size: small;
|
|
font-weight: 300;
|
|
line-height: 1.5em;
|
|
}
|
|
body > * {
|
|
padding: 10px;
|
|
margin: 10px;
|
|
}
|
|
#options {
|
|
padding: 0;
|
|
display: grid;
|
|
grid-template-columns: 1fr;
|
|
}
|
|
#options > * {
|
|
padding: 15px;
|
|
border-radius: 5px;
|
|
border: 1px solid #475569;
|
|
background: #f9fafc
|
|
}
|
|
.option:hover {
|
|
box-shadow: 0 0 11px #aaa;
|
|
}
|
|
#options > button {
|
|
margin-right: 10px;
|
|
}
|
|
|
|
#query {
|
|
font-size: small;
|
|
}
|
|
#results {
|
|
font-size: small;
|
|
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-notion,
|
|
.results-html,
|
|
.results-plugin {
|
|
text-align: left;
|
|
white-space: pre-line;
|
|
}
|
|
.results-markdown,
|
|
.results-github {
|
|
text-align: left;
|
|
}
|
|
.results-org {
|
|
text-align: left;
|
|
/* white-space: pre-line; */
|
|
}
|
|
.results-org h3 {
|
|
margin: 20px 0 0 0;
|
|
font-size: small;
|
|
}
|
|
span.org-task-status {
|
|
color: white;
|
|
padding: 3.5px 3.5px 0;
|
|
margin-right: 5px;
|
|
border-radius: 5px;
|
|
background-color: #eab308;
|
|
font-size: small;
|
|
}
|
|
span.org-task-status.todo {
|
|
background-color: #3b82f6
|
|
}
|
|
span.org-task-status.done {
|
|
background-color: #22c55e;
|
|
}
|
|
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-error,
|
|
div.results-markdown,
|
|
div.results-notion,
|
|
div.results-org,
|
|
div.results-plugin,
|
|
div.results-html,
|
|
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);
|
|
}
|
|
|
|
div#results-error {
|
|
box-shadow: 2px 2px 2px #FF5722;
|
|
}
|
|
|
|
img {
|
|
max-width: 90%;
|
|
}
|
|
|
|
@keyframes gradient {
|
|
0% {
|
|
background-position: 0% 50%;
|
|
}
|
|
50% {
|
|
background-position: 100% 50%;
|
|
}
|
|
100% {
|
|
background-position: 0% 50%;
|
|
}
|
|
}
|
|
|
|
a.khoj-logo {
|
|
text-align: center;
|
|
}
|
|
|
|
</style>
|
|
</html>
|