Merge pull request #228 from debanjum/features/pretty-config-page

Update the config page to be more usable
This commit is contained in:
sabaimran 2023-06-19 18:11:35 -07:00 committed by GitHub
commit 6224dce49d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 669 additions and 183 deletions

View file

@ -50,7 +50,7 @@ pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
if system() != 'Darwin':
# Add Splash screen to show on app launch
splash = Splash(
'src/khoj/interface/web/assets/icons/favicon-144x144.png',
'src/khoj/interface/web/assets/icons/favicon-128x128.png',
binaries=a.binaries,
datas=a.datas,
text_pos=(10, 160),
@ -82,7 +82,7 @@ if system() != 'Darwin':
target_arch='x86_64',
codesign_identity=None,
entitlements_file=None,
icon='src/khoj/interface/web/assets/icons/favicon-144x144.ico',
icon='src/khoj/interface/web/assets/icons/favicon-128x128.ico',
)
else:
exe = EXE(

View file

@ -49,7 +49,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.setFixedWidth(600)
# Set Window Icon
icon_path = constants.web_directory / "assets/icons/favicon-144x144.png"
icon_path = constants.web_directory / "assets/icons/favicon-128x128.png"
self.setWindowIcon(QtGui.QIcon(f"{icon_path.absolute()}"))
# Initialize Configure Window Layout
@ -228,6 +228,8 @@ class MainWindow(QtWidgets.QMainWindow):
# Search Type (re)-Enabled
if child.isChecked():
current_search_config = self.current_config["content-type"].get(child.search_type, {})
if current_search_config == None:
current_search_config = {}
default_search_config = self.get_default_config(search_type=child.search_type)
self.new_config["content-type"][child.search_type.value] = merge_dicts(
current_search_config, default_search_config

View file

@ -17,7 +17,7 @@ def create_system_tray(gui: QtWidgets.QApplication, main_window: MainWindow):
"""
# Create the system tray with icon
icon_path = constants.web_directory / "assets/icons/favicon-144x144.png"
icon_path = constants.web_directory / "assets/icons/favicon-128x128.png"
icon = QtGui.QIcon(f"{icon_path.absolute()}")
tray = QtWidgets.QSystemTrayIcon(icon)
tray.setVisible(True)

View file

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<head>
<title>Khoj: An AI Personal Assistant for your digital brain</title>
<link rel=”stylesheet” href=”static/styles.css”>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@1/css/pico.min.css">
</head>
<body class="not-found">
<header class=”header”>
<h1>Oops, this is awkward. That page couldn't be found.</h1>
</header>
<a href="/config">Go Home</a>
<footer class=”footer”>
</footer>
</body>
<style>
body.not-found {
padding: 0 10%
}
</style>
</html>

View file

@ -1,29 +0,0 @@
:root {
--primary-color: #ffffff;
--bold-color: #2073ee;
--complementary-color: #124408;
--accent-color-0: #57f0b5;
}
input[type=text] {
width: 40%;
}
div.config-element {
color: var(--bold-color);
margin: 8px;
}
div.config-title {
font-weight: bold;
}
span.config-element-value {
color: var(--complementary-color);
font-weight: normal;
cursor: pointer;
}
button {
cursor: pointer;
}

View file

@ -1,125 +0,0 @@
// Retrieve elements from the DOM.
var showConfig = document.getElementById("show-config");
var configForm = document.getElementById("config-form");
var regenerateButton = document.getElementById("config-regenerate");
// Global variables.
var rawConfig = {};
var emptyValueDefault = "🖊️";
/**
* Fetch the existing config file.
*/
fetch("/api/config/data")
.then(response => response.json())
.then(data => {
rawConfig = data;
configForm.style.display = "block";
processChildren(configForm, data);
var submitButton = document.createElement("button");
submitButton.type = "submit";
submitButton.innerHTML = "update";
configForm.appendChild(submitButton);
// The config form's submit handler.
configForm.addEventListener("submit", (event) => {
event.preventDefault();
console.log(rawConfig);
fetch("/api/config/data", {
method: "POST",
credentials: "same-origin",
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(rawConfig)
})
.then(response => response.json())
.then(data => console.log(data));
});
});
/**
* The click handler for the Regenerate button.
*/
regenerateButton.addEventListener("click", (event) => {
event.preventDefault();
regenerateButton.style.cursor = "progress";
regenerateButton.disabled = true;
fetch("/api/update?force=true&client=web")
.then(response => response.json())
.then(data => {
regenerateButton.style.cursor = "pointer";
regenerateButton.disabled = false;
console.log(data);
});
})
/**
* Adds config elements to the DOM representing the sub-components
* of one of the fields in the raw config file.
* @param {the parent element} element
* @param {the data to be rendered for this element and its children} data
*/
function processChildren(element, data) {
for (let key in data) {
var child = document.createElement("div");
child.id = key;
child.className = "config-element";
child.appendChild(document.createTextNode(key + ": "));
if (data[key] === Object(data[key]) && !Array.isArray(data[key])) {
child.className+=" config-title";
processChildren(child, data[key]);
} else {
child.appendChild(createValueNode(data, key));
}
element.appendChild(child);
}
}
/**
* Takes an element, and replaces it with an editable
* element with the same data in place.
* @param {the original element to be replaced} original
* @param {the source data to be rendered for the new element} data
* @param {the key for this input in the source data} key
*/
function makeElementEditable(original, data, key) {
original.addEventListener("click", () => {
var inputNewText = document.createElement("input");
inputNewText.type = "text";
inputNewText.className = "config-element-edit";
inputNewText.value = (original.textContent == emptyValueDefault) ? "" : original.textContent;
fixInputOnFocusOut(inputNewText, data, key);
original.parentNode.replaceChild(inputNewText, original);
inputNewText.focus();
});
}
/**
* Creates a node corresponding to the value of a config element.
* @param {the source data} data
* @param {the key corresponding to this node's data} key
* @returns A new element which corresponds to the value in some field.
*/
function createValueNode(data, key) {
var valueElement = document.createElement("span");
valueElement.className = "config-element-value";
valueElement.textContent = !data[key] ? emptyValueDefault : data[key];
makeElementEditable(valueElement, data, key);
return valueElement;
}
/**
* Replaces an existing input element with an element with the same data, which is not an input.
* If the input data for this element was changed, update the corresponding data in the raw config.
* @param {the original element to be replaced} original
* @param {the source data} data
* @param {the key corresponding to this node's data} key
*/
function fixInputOnFocusOut(original, data, key) {
original.addEventListener("blur", () => {
data[key] = (original.value != emptyValueDefault) ? original.value : "";
original.parentNode.replaceChild(createValueNode(data, key), original);
})
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

View file

@ -0,0 +1,44 @@
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<link rel="icon" type="image/png" sizes="128x128" href="/static/assets/icons/favicon-128x128.png">
<title>Khoj - Settings</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@1/css/pico.min.css">
</head>
<body class="khoj-configure">
<header>
<div>
<h1>Khoj Settings</h1>
<p>Check out our <a href="https://github.com/debanjum/khoj">source code on Github</a></p>
</div>
<div>
<h2>Ready?</h2>
<div id="actions">
<button onclick="window.location.href='/';" >
Search
</button>
<button onclick="window.location.href='/chat';">
Chat
</button>
</div>
</div>
</header>
<div class=”content”>
{% block content %}
{% endblock %}
</div>
</body>
<style>
header {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 12px;
}
@media screen and (max-width: 600px) {
header {
grid-template-columns: 1fr;
}
}
</style>
</html>

View file

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html>
<head>
<title>Khoj: Data Settings</title>
<link rel=”stylesheet” href=”static/styles.css”>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@1/css/pico.min.css">
</head>
<body class="data-integration">
<header class=”header”>
<h1>Configure your data integrations for Khoj</h1>
</header>
<a href="/config">Go back</a>
<div class=”content”>
{% block content %}
{% endblock %}
</div>
<footer class=”footer”>
</footer>
</body>
<style>
body.data-integration {
padding: 0 10%
}
</style>
</html>

View file

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html>
<head>
<title>Khoj: Processor Settings</title>
<link rel=”stylesheet” href=”static/styles.css”>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@1/css/pico.min.css">
</head>
<body class="data-integration">
<header class=”header”>
<h1>Configure your processor integrations for Khoj</h1>
</header>
<a href="/config">Go back</a>
<div class=”content”>
{% block content %}
{% endblock %}
</div>
<footer class=”footer”>
</footer>
</body>
<style>
body.data-integration {
padding: 0 10%
}
</style>
</html>

View file

@ -4,8 +4,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0 maximum-scale=1.0">
<title>Khoj</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 144 144%22><text y=%22.86em%22 font-size=%22144%22>🦅</text></svg>">
<link rel="icon" type="image/png" sizes="144x144" href="/static/assets/icons/favicon-144x144.png">
<link rel="icon" type="image/png" sizes="128x128" href="/static/assets/icons/favicon-128x128.png">
<link rel="manifest" href="/static/khoj_chat.webmanifest">
</head>
<script>

View file

@ -1,14 +1,71 @@
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🦅</text></svg>">
<link rel="stylesheet" href="static/assets/config.css">
<title>Khoj - Configure App</title>
</head>
<body>
<form id="config-form">
</form>
<button id="config-regenerate">regenerate</button>
</body>
<script src="static/assets/config.js"></script>
</html>
{% extends "base_config.html" %}
{% block content %}
<h2>Content Types</h2>
<div id="content-configuration">
<button onclick="window.location.href='/config/content_type/pdf';">
PDF
</button>
<button onclick="window.location.href='/config/content_type/markdown';">
Markdown
</button>
<button onclick="window.location.href='/config/content_type/org';">
Org
</button>
<button onclick="window.location.href='/config/content_type/ledger';">
Ledger
</button>
<button onclick="window.location.href='/config/content_type/github';">
GitHub
</button>
</div>
<h2>Processors</h2>
<button onclick="window.location.href='/config/processor/conversation/';">
Conversation
</button>
<h1>Finalize</h1>
<button id="regenerate" type="submit">Regenerate</button>
<style>
body.khoj-configure {
padding: 0 10%
}
div#content-configuration, div#actions {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 12px;
}
button#regenerate {
background-color: #4CAF50;
border: none;
color: white;
padding: 15px 32px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
}
</style>
<script>
var regenerate = document.getElementById("regenerate");
regenerate.addEventListener("click", function(event) {
event.preventDefault();
regenerate.disabled = true;
regenerate.innerHTML = "Regenerating...";
fetch('/api/update?force=true&client=web', {
method: 'GET',
})
.then(response => response.json())
.then(data => {
console.log('Success:', data);
alert("Regenerated!");
regenerate.disabled = false;
regenerate.innerHTML = "Regenerate";
})
.catch((error) => {
console.error('Error:', error);
alert("Regeneration was not successful. Check debug logs.");
regenerate.disabled = false;
regenerate.innerHTML = "Regenerate";
});
});
</script>
{% endblock %}

View file

@ -0,0 +1,99 @@
{% extends "base_data_integration.html" %}
{% block content %}
<h2>Github</h2>
<form id="config-form">
<div id="success" style="display: none;"></div>
<table>
<tr>
<td>
<label for="pat-token">Personal Access Token</label>
</td>
<td>
<input type="text" id="pat-token" name="pat" value="{{ current_config['pat_token'] }}">
</td>
</tr>
<tr>
<td>
<label for="repo-owner">Repository Owner</label>
</td>
<td>
<input type="text" id="repo-owner" name="repo_owner" value="{{ current_config['repo_owner'] }}">
</td>
</tr>
<tr>
<td>
<label for="repo-name">Repository Name</label>
</td>
<td>
<input type="text" id="repo-name" name="repo_name" value="{{ current_config['repo_name'] }}">
</td>
</tr>
<tr>
<td>
<label for="repo-branch">Repository Branch</label>
</td>
<td>
<input type="text" id="repo-branch" name="repo_branch" value="{{ current_config['repo_branch'] }}">
</td>
</tr>
</table>
<h4>You probably don't need to edit these.</h4>
<table>
<tr>
<td>
<label for="compressed-jsonl">Compressed JSONL (Output)</label>
</td>
<td>
<input type="text" id="compressed-jsonl" name="compressed-jsonl" value="{{ current_config['compressed_jsonl'] }}">
</td>
</tr>
<tr>
<td>
<label for="embeddings-file">Embeddings File (Output)</label>
</td>
<td>
<input type="text" id="embeddings-file" name="embeddings-file" value="{{ current_config['embeddings_file'] }}">
</td>
</tr>
</table>
<button id="submit" type="submit">Submit</button>
</form>
<script>
submit.addEventListener("click", function(event) {
event.preventDefault();
var compressed_jsonl = document.getElementById("compressed-jsonl").value;
var embeddings_file = document.getElementById("embeddings-file").value;
var pat_token = document.getElementById("pat-token").value;
var repo_owner = document.getElementById("repo-owner").value;
var repo_name = document.getElementById("repo-name").value;
var repo_branch = document.getElementById("repo-branch").value;
fetch('/api/config/data/content_type/github', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
"pat_token": pat_token,
"repo_owner": repo_owner,
"repo_name": repo_name,
"repo_branch": repo_branch,
"compressed_jsonl": compressed_jsonl,
"embeddings_file": embeddings_file,
})
})
.then(response => response.json())
.then(data => {
if (data["status"] == "ok") {
document.getElementById("success").innerHTML = "✅ Successfully updated. Go to <a href='/config'>your settings</a> to regenerate your index.";
document.getElementById("success").style.display = "block";
} else {
document.getElementById("success").innerHTML = "⚠️ Failed to update settings.";
document.getElementById("success").style.display = "block";
}
})
});
</script>
{% endblock %}

View file

@ -0,0 +1,150 @@
{% extends "base_data_integration.html" %}
{% block content %}
<h2>{{ content_type }}</h2>
<form id="config-form">
<div id="success" style="display: none;" ></div>
<table>
<tr>
<td>
<label for="input-files">Input Files</label>
</td>
<td id="input-files-cell">
{% if current_config['input_files'] is none %}
<input type="text" id="input-files" name="input-files">
{% else %}
{% for input_file in current_config['input_files'] %}
<input type="text" id="input-files" name="input-files" value="{{ input_file }}">
{% endfor %}
{% endif %}
</td>
<td>
<button type="button" id="input-files-button">Add</button>
</td>
</tr>
<tr>
<td>
<label for="input-filter">Input Filter</label>
</td>
<td id="input-filter-cell">
{% if current_config['input_filter'] is none %}
<input type="text" id="input-filter" name="input-filter">
{% else %}
{% for input_filter in current_config['input_filter'] %}
<input type="text" id="input-filter" name="input-filter" value="{{ input_filter }}">
{% endfor %}
{% endif %}
</td>
<td>
<button type="button" id="input-filter-button">Add</button>
</td>
</tr>
</table>
<h4>You probably don't need to edit these.</h4>
<table>
<tr>
<td>
<label for="compressed-jsonl">Compressed JSONL (Output)</label>
</td>
<td>
<input type="text" id="compressed-jsonl" name="compressed-jsonl" value="{{ current_config['compressed_jsonl'] }}">
</td>
</tr>
<tr>
<td>
<label for="embeddings-file">Embeddings File (Output)</label>
</td>
<td>
<input type="text" id="embeddings-file" name="embeddings-file" value="{{ current_config['embeddings_file'] }}">
</td>
</tr>
<tr>
<td>
<label for="index-heading-entries">Index Heading Entries</label>
</td>
<td>
<input type="text" id="index-heading-entries" name="index-heading-entries" value="{{ current_config['index_heading_entries'] }}">
</td>
</tr>
</table>
<button id="submit" type="submit">Submit</button>
</form>
<script>
function addButtonEventListener(fieldName) {
var button = document.getElementById(fieldName + "-button");
button.addEventListener("click", function(event) {
var cell = document.getElementById(fieldName + "-cell");
var newInput = document.createElement("input");
newInput.setAttribute("type", "text");
newInput.setAttribute("name", fieldName);
cell.appendChild(newInput);
})
}
addButtonEventListener("input-files");
addButtonEventListener("input-filter");
function getValidInputNodes(nodes) {
var validNodes = [];
for (var i = 0; i < nodes.length; i++) {
const nodeValue = nodes[i].value;
if (nodeValue === "" || nodeValue === null || nodeValue === undefined || nodeValue === "None") {
continue;
}
validNodes.push(nodes[i]);
}
return validNodes;
}
submit.addEventListener("click", function(event) {
event.preventDefault();
var inputFileNodes = document.getElementsByName("input-files");
var input_files = getValidInputNodes(inputFileNodes).map(node => node.value);
var inputFilterNodes = document.getElementsByName("input-filter");
var input_filter = getValidInputNodes(inputFilterNodes).map(node => node.value);
if (input_files.length === 0 && input_filter.length === 0) {
alert("You must specify at least one input file or input filter.");
return;
}
if (input_files.length == 0) {
input_files = null;
}
if (input_filter.length == 0) {
input_filter = null;
}
var compressed_jsonl = document.getElementById("compressed-jsonl").value;
var embeddings_file = document.getElementById("embeddings-file").value;
var index_heading_entries = document.getElementById("index-heading-entries").value;
fetch('/api/config/data/content_type/{{ content_type }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
"input_files": input_files,
"input_filter": input_filter,
"compressed_jsonl": compressed_jsonl,
"embeddings_file": embeddings_file,
"index_heading_entries": index_heading_entries
})
})
.then(response => response.json())
.then(data => {
if (data["status"] == "ok") {
document.getElementById("success").innerHTML = "✅ Successfully updated. Go to <a href='/config'>your settings</a> to regenerate your index.";
document.getElementById("success").style.display = "block";
} else {
document.getElementById("success").innerHTML = "⚠️ Failed to update settings.";
document.getElementById("success").style.display = "block";
}
})
});
</script>
{% endblock %}

View file

@ -4,8 +4,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0 maximum-scale=1.0">
<title>Khoj</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 144 144%22><text y=%22.86em%22 font-size=%22144%22>🦅</text></svg>">
<link rel="icon" type="image/png" sizes="144x144" href="/static/assets/icons/favicon-144x144.png">
<link rel="icon" type="image/png" sizes="128x128" href="/static/assets/icons/favicon-128x128.png">
<link rel="manifest" href="/static/khoj.webmanifest">
</head>
<script type="text/javascript" src="static/assets/org.min.js"></script>

View file

@ -4,8 +4,8 @@
"description": "An AI search assistant for your digital brain",
"icons": [
{
"src": "/static/assets/icons/favicon-144x144.png",
"sizes": "144x144",
"src": "/static/assets/icons/favicon-128x128.png",
"sizes": "128x128",
"type": "image/png"
}
],

View file

@ -4,8 +4,8 @@
"description": "An AI personal assistant for your digital brain",
"icons": [
{
"src": "/static/assets/icons/favicon-144x144.png",
"sizes": "144x144",
"src": "/static/assets/icons/favicon-128x128.png",
"sizes": "128x128",
"type": "image/png"
}
],

View file

@ -0,0 +1,79 @@
{% extends "base_processor_integration.html" %}
{% block content %}
<h2>Conversation</h2>
<form id="config-form">
<div id="success" style="display: none;" ></div>
<table>
<tr>
<td>
<label for="openai-api-key">OpenAI API key</label>
</td>
<td>
<input type="text" id="openai-api-key" name="openai-api-key" value="{{ current_config['openai_api_key'] }}">
</td>
</tr>
</table>
<h4>You probably don't need to edit these.</h4>
<table>
<tr>
<td>
<label for="conversation-logfile">Conversation Logfile</label>
</td>
<td>
<input type="text" id="conversation-logfile" name="conversation-logfile" value="{{ current_config['conversation_logfile'] }}">
</td>
</tr>
<tr>
<td>
<label for="model">Model</label>
</td>
<td>
<input type="text" id="model" name="model" value="{{ current_config['model'] }}">
</td>
</tr>
<tr>
<td>
<label for="chat-model">Chat Model</label>
</td>
<td>
<input type="text" id="chat-model" name="chat-model" value="{{ current_config['chat_model'] }}">
</td>
</tr>
</table>
<button id="submit" type="submit">Submit</button>
</form>
<script>
submit.addEventListener("click", function(event) {
event.preventDefault();
var openai_api_key = document.getElementById("openai-api-key").value;
var conversation_logfile = document.getElementById("conversation-logfile").value;
var model = document.getElementById("model").value;
var chat_model = document.getElementById("chat-model").value;
fetch('/api/config/data/processor/conversation', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
"openai_api_key": openai_api_key,
"conversation_logfile": conversation_logfile,
"model": model,
"chat_model": chat_model
})
})
.then(response => response.json())
.then(data => {
if (data["status"] == "ok") {
document.getElementById("success").innerHTML = "✅ Successfully updated. Go to <a href='/config'>your settings</a> to regenerate your index.";
document.getElementById("success").style.display = "block";
} else {
document.getElementById("success").innerHTML = "⚠️ Failed to update settings.";
document.getElementById("success").style.display = "block";
}
})
});
</script>
{% endblock %}

View file

@ -15,9 +15,16 @@ from khoj.processor.conversation.gpt import converse, extract_questions
from khoj.processor.conversation.utils import message_to_log, message_to_prompt
from khoj.search_type import image_search, text_search
from khoj.utils.helpers import log_telemetry, timer
from khoj.utils.rawconfig import FullConfig, SearchResponse
from khoj.utils.rawconfig import (
FullConfig,
SearchResponse,
TextContentConfig,
ConversationProcessorConfig,
GithubContentConfig,
)
from khoj.utils.state import SearchType
from khoj.utils import state, constants
from khoj.utils.yaml import save_config_to_file_updated_state
# Initialize Router
api = APIRouter()
@ -65,6 +72,36 @@ async def set_config_data(updated_config: FullConfig):
return state.config
@api.post("/config/data/content_type/github", status_code=200)
async def set_content_config_github_data(updated_config: GithubContentConfig):
state.config.content_type.github = updated_config
try:
save_config_to_file_updated_state()
return {"status": "ok"}
except Exception as e:
return {"status": "error", "message": str(e)}
@api.post("/config/data/content_type/{content_type}", status_code=200)
async def set_content_config_data(content_type: str, updated_config: TextContentConfig):
state.config.content_type[content_type] = updated_config
try:
save_config_to_file_updated_state()
return {"status": "ok"}
except Exception as e:
return {"status": "error", "message": str(e)}
@api.post("/config/data/processor/conversation", status_code=200)
async def set_processor_conversation_config_data(updated_config: ConversationProcessorConfig):
state.config.processor.conversation = updated_config
try:
save_config_to_file_updated_state()
return {"status": "ok"}
except Exception as e:
return {"status": "error", "message": str(e)}
@api.get("/search", response_model=List[SearchResponse])
def search(
q: str,

View file

@ -3,15 +3,21 @@ from fastapi import APIRouter
from fastapi import Request
from fastapi.responses import HTMLResponse, FileResponse
from fastapi.templating import Jinja2Templates
from khoj.utils.rawconfig import TextContentConfig, ConversationProcessorConfig
# Internal Packages
from khoj.utils import constants
from khoj.utils import constants, state
import logging
import json
# Initialize Router
web_client = APIRouter()
templates = Jinja2Templates(directory=constants.web_directory)
VALID_CONTENT_TYPES = ["org", "ledger", "markdown", "pdf"]
# Create Routes
@web_client.get("/", response_class=FileResponse)
@ -24,6 +30,83 @@ def config_page(request: Request):
return templates.TemplateResponse("config.html", context={"request": request})
@web_client.get("/config/content_type/github", response_class=HTMLResponse)
def github_config_page(request: Request):
default_copy = constants.default_config.copy()
default_github = default_copy["content-type"]["github"] # type: ignore
default_config = TextContentConfig(
compressed_jsonl=default_github["compressed-jsonl"],
embeddings_file=default_github["embeddings-file"],
)
current_config = (
state.config.content_type.github if state.config.content_type.github is not None else default_config
)
current_config = json.loads(current_config.json())
return templates.TemplateResponse(
"content_type_github_input.html", context={"request": request, "current_config": current_config}
)
@web_client.get("/config/content_type/{content_type}", response_class=HTMLResponse)
def content_config_page(request: Request, content_type: str):
if content_type not in VALID_CONTENT_TYPES:
return templates.TemplateResponse("config.html", context={"request": request})
default_copy = constants.default_config.copy()
default_content_type = default_copy["content-type"][content_type] # type: ignore
default_config = TextContentConfig(
compressed_jsonl=default_content_type["compressed-jsonl"],
embeddings_file=default_content_type["embeddings-file"],
)
current_config = (
state.config.content_type[content_type]
if state.config.content_type[content_type] is not None
else default_config
)
current_config = json.loads(current_config.json())
return templates.TemplateResponse(
"content_type_input.html",
context={
"request": request,
"current_config": current_config,
"content_type": content_type,
},
)
@web_client.get("/config/processor/conversation", response_class=HTMLResponse)
def conversation_processor_config_page(request: Request):
default_copy = constants.default_config.copy()
default_processor_config = default_copy["processor"]["conversation"] # type: ignore
default_processor_config = ConversationProcessorConfig(
openai_api_key="",
model=default_processor_config["model"],
conversation_logfile=default_processor_config["conversation-logfile"],
chat_model=default_processor_config["chat-model"],
)
current_processor_conversation_config = (
state.config.processor.conversation
if state.config.processor.conversation is not None
else default_processor_config
)
current_processor_conversation_config = json.loads(current_processor_conversation_config.json())
return templates.TemplateResponse(
"processor_conversation_input.html",
context={
"request": request,
"current_config": current_processor_conversation_config,
},
)
@web_client.get("/chat", response_class=FileResponse)
def chat_page():
return FileResponse(constants.web_directory / "chat.html")

View file

@ -14,7 +14,7 @@ default_config = {
"input-filter": None,
"compressed-jsonl": "~/.khoj/content/org/org.jsonl.gz",
"embeddings-file": "~/.khoj/content/org/org_embeddings.pt",
"index_heading_entries": False,
"index-heading-entries": False,
},
"markdown": {
"input-files": None,
@ -74,6 +74,7 @@ default_config = {
"openai-api-key": None,
"model": "text-davinci-003",
"conversation-logfile": "~/.khoj/processor/conversation/conversation_logs.json",
"chat-model": "gpt-3.5-turbo",
}
},
}

View file

@ -15,6 +15,12 @@ class ConfigBase(BaseModel):
alias_generator = to_snake_case_from_dash
allow_population_by_field_name = True
def __getitem__(self, item):
return getattr(self, item)
def __setitem__(self, key, value):
return setattr(self, key, value)
class TextConfigBase(ConfigBase):
compressed_jsonl: Path

View file

@ -6,12 +6,20 @@ import yaml
# Internal Packages
from khoj.utils.rawconfig import FullConfig
from khoj.utils import state
# Do not emit tags when dumping to YAML
yaml.emitter.Emitter.process_tag = lambda self, *args, **kwargs: None # type: ignore[assignment]
def save_config_to_file_updated_state():
with open(state.config_file, "w") as outfile:
yaml.dump(yaml.safe_load(state.config.json(by_alias=True)), outfile)
outfile.close()
return state.config
def save_config_to_file(yaml_config: dict, yaml_config_file: Path):
"Write config to YML file"
# Create output directory, if it doesn't exist