mirror of
https://github.com/khoaliber/khoj.git
synced 2026-03-03 21:29:08 +00:00
Merge branch 'master' of github.com:khoj-ai/khoj into features/chat-ui-updates-big
This commit is contained in:
@@ -19,7 +19,7 @@ const textFileTypes = [
|
||||
'org', 'md', 'markdown', 'txt', 'html', 'xml',
|
||||
// Other valid text file extensions from https://google.github.io/magika/model/config.json
|
||||
'appleplist', 'asm', 'asp', 'batch', 'c', 'cs', 'css', 'csv', 'eml', 'go', 'html', 'ini', 'internetshortcut', 'java', 'javascript', 'json', 'latex', 'lisp', 'makefile', 'markdown', 'mht', 'mum', 'pem', 'perl', 'php', 'powershell', 'python', 'rdf', 'rst', 'rtf', 'ruby', 'rust', 'scala', 'shell', 'smali', 'sql', 'svg', 'symlinktext', 'txt', 'vba', 'winregistry', 'xml', 'yaml']
|
||||
const binaryFileTypes = ['pdf']
|
||||
const binaryFileTypes = ['pdf', 'jpg', 'jpeg', 'png']
|
||||
const validFileTypes = textFileTypes.concat(binaryFileTypes);
|
||||
|
||||
const schema = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Khoj",
|
||||
"version": "1.15.0",
|
||||
"version": "1.16.0",
|
||||
"description": "An AI copilot for your Second Brain",
|
||||
"author": "Saba Imran, Debanjum Singh Solanky <team@khoj.dev>",
|
||||
"license": "GPL-3.0-or-later",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
;; Saba Imran <saba@khoj.dev>
|
||||
;; Description: An AI copilot for your Second Brain
|
||||
;; Keywords: search, chat, org-mode, outlines, markdown, pdf, image
|
||||
;; Version: 1.15.0
|
||||
;; Version: 1.16.0
|
||||
;; Package-Requires: ((emacs "27.1") (transient "0.3.0") (dash "2.19.1"))
|
||||
;; URL: https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "khoj",
|
||||
"name": "Khoj",
|
||||
"version": "1.15.0",
|
||||
"version": "1.16.0",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "An AI copilot for your Second Brain",
|
||||
"author": "Khoj Inc.",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Khoj",
|
||||
"version": "1.15.0",
|
||||
"version": "1.16.0",
|
||||
"description": "An AI copilot for your Second Brain",
|
||||
"author": "Debanjum Singh Solanky, Saba Imran <team@khoj.dev>",
|
||||
"license": "GPL-3.0-or-later",
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { ItemView, MarkdownRenderer, WorkspaceLeaf, request, requestUrl, setIcon } from 'obsidian';
|
||||
import { ItemView, MarkdownRenderer, Scope, WorkspaceLeaf, request, requestUrl, setIcon } from 'obsidian';
|
||||
import * as DOMPurify from 'dompurify';
|
||||
import { KhojSetting } from 'src/settings';
|
||||
import { KhojPaneView } from 'src/pane_view';
|
||||
import { KhojView, createCopyParentText, getLinkToEntry, pasteTextAtCursor } from 'src/utils';
|
||||
import { KhojSearchModal } from './search_modal';
|
||||
|
||||
export interface ChatJsonResult {
|
||||
image?: string;
|
||||
@@ -24,10 +25,18 @@ export class KhojChatView extends KhojPaneView {
|
||||
setting: KhojSetting;
|
||||
waitingForLocation: boolean;
|
||||
location: Location;
|
||||
keyPressTimeout: NodeJS.Timeout | null = null;
|
||||
|
||||
constructor(leaf: WorkspaceLeaf, setting: KhojSetting) {
|
||||
super(leaf, setting);
|
||||
|
||||
// Register chat view keybindings
|
||||
this.scope = new Scope(this.app.scope);
|
||||
this.scope.register(["Ctrl"], 'n', (_) => this.createNewConversation());
|
||||
this.scope.register(["Ctrl"], 'o', async (_) => await this.toggleChatSessions());
|
||||
this.scope.register(["Ctrl"], 'f', (_) => new KhojSearchModal(this.app, this.setting).open());
|
||||
this.scope.register(["Ctrl"], 'r', (_) => new KhojSearchModal(this.app, this.setting, true).open());
|
||||
|
||||
this.waitingForLocation = true;
|
||||
|
||||
fetch("https://ipapi.co/json")
|
||||
@@ -61,8 +70,7 @@ export class KhojChatView extends KhojPaneView {
|
||||
return "message-circle";
|
||||
}
|
||||
|
||||
async chat() {
|
||||
|
||||
async chat(isVoice: boolean = false) {
|
||||
// Get text in chat input element
|
||||
let input_el = <HTMLTextAreaElement>this.contentEl.getElementsByClassName("khoj-chat-input")[0];
|
||||
|
||||
@@ -72,7 +80,7 @@ export class KhojChatView extends KhojPaneView {
|
||||
this.autoResize();
|
||||
|
||||
// Get and render chat response to user message
|
||||
await this.getChatResponse(user_message);
|
||||
await this.getChatResponse(user_message, isVoice);
|
||||
}
|
||||
|
||||
async onOpen() {
|
||||
@@ -92,8 +100,9 @@ export class KhojChatView extends KhojPaneView {
|
||||
const objectSrc = `object-src 'none';`;
|
||||
const csp = `${defaultSrc} ${scriptSrc} ${connectSrc} ${styleSrc} ${imgSrc} ${childSrc} ${objectSrc}`;
|
||||
|
||||
// Add CSP meta tag to the Khoj Chat modal
|
||||
document.head.createEl("meta", { attr: { "http-equiv": "Content-Security-Policy", "content": `${csp}` } });
|
||||
// WARNING: CSP DISABLED for now as it breaks other Obsidian plugins. Enable when can scope CSP to only Khoj plugin.
|
||||
// CSP meta tag for the Khoj Chat modal
|
||||
// document.head.createEl("meta", { attr: { "http-equiv": "Content-Security-Policy", "content": `${csp}` } });
|
||||
|
||||
// Create area for chat logs
|
||||
let chatBodyEl = contentEl.createDiv({ attr: { id: "khoj-chat-body", class: "khoj-chat-body" } });
|
||||
@@ -104,9 +113,10 @@ export class KhojChatView extends KhojPaneView {
|
||||
text: "Chat Sessions",
|
||||
attr: {
|
||||
class: "khoj-input-row-button clickable-icon",
|
||||
title: "Show Conversations (^O)",
|
||||
},
|
||||
})
|
||||
chatSessions.addEventListener('click', async (_) => { await this.toggleChatSessions(chatBodyEl) });
|
||||
chatSessions.addEventListener('click', async (_) => { await this.toggleChatSessions() });
|
||||
setIcon(chatSessions, "history");
|
||||
|
||||
let chatInput = inputRow.createEl("textarea", {
|
||||
@@ -119,14 +129,20 @@ export class KhojChatView extends KhojPaneView {
|
||||
chatInput.addEventListener('input', (_) => { this.onChatInput() });
|
||||
chatInput.addEventListener('keydown', (event) => { this.incrementalChat(event) });
|
||||
|
||||
// Add event listeners for long press keybinding
|
||||
this.contentEl.addEventListener('keydown', this.handleKeyDown.bind(this));
|
||||
this.contentEl.addEventListener('keyup', this.handleKeyUp.bind(this));
|
||||
|
||||
let transcribe = inputRow.createEl("button", {
|
||||
text: "Transcribe",
|
||||
attr: {
|
||||
id: "khoj-transcribe",
|
||||
class: "khoj-transcribe khoj-input-row-button clickable-icon ",
|
||||
title: "Start Voice Chat (^S)",
|
||||
},
|
||||
})
|
||||
transcribe.addEventListener('mousedown', async (event) => { await this.speechToText(event) });
|
||||
transcribe.addEventListener('mousedown', (event) => { this.startSpeechToText(event) });
|
||||
transcribe.addEventListener('mouseup', async (event) => { await this.stopSpeechToText(event) });
|
||||
transcribe.addEventListener('touchstart', async (event) => { await this.speechToText(event) });
|
||||
transcribe.addEventListener('touchend', async (event) => { await this.speechToText(event) });
|
||||
transcribe.addEventListener('touchcancel', async (event) => { await this.speechToText(event) });
|
||||
@@ -160,6 +176,46 @@ export class KhojChatView extends KhojPaneView {
|
||||
});
|
||||
}
|
||||
|
||||
startSpeechToText(event: KeyboardEvent | MouseEvent | TouchEvent, timeout=200) {
|
||||
if (!this.keyPressTimeout) {
|
||||
this.keyPressTimeout = setTimeout(async () => {
|
||||
// Reset auto send voice message timer, UI if running
|
||||
if (this.sendMessageTimeout) {
|
||||
// Stop the auto send voice message countdown timer UI
|
||||
clearTimeout(this.sendMessageTimeout);
|
||||
const sendButton = <HTMLButtonElement>this.contentEl.getElementsByClassName("khoj-chat-send")[0]
|
||||
setIcon(sendButton, "arrow-up-circle")
|
||||
let sendImg = <SVGElement>sendButton.getElementsByClassName("lucide-arrow-up-circle")[0]
|
||||
sendImg.addEventListener('click', async (_) => { await this.chat() });
|
||||
// Reset chat input value
|
||||
const chatInput = <HTMLTextAreaElement>this.contentEl.getElementsByClassName("khoj-chat-input")[0];
|
||||
chatInput.value = "";
|
||||
}
|
||||
// Start new voice message
|
||||
await this.speechToText(event);
|
||||
}, timeout);
|
||||
}
|
||||
}
|
||||
async stopSpeechToText(event: KeyboardEvent | MouseEvent | TouchEvent) {
|
||||
if (this.mediaRecorder) {
|
||||
await this.speechToText(event);
|
||||
}
|
||||
if (this.keyPressTimeout) {
|
||||
clearTimeout(this.keyPressTimeout);
|
||||
this.keyPressTimeout = null;
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyDown(event: KeyboardEvent) {
|
||||
// Start speech to text if keyboard shortcut is pressed
|
||||
if (event.key === 's' && event.getModifierState('Control')) this.startSpeechToText(event);
|
||||
}
|
||||
|
||||
async handleKeyUp(event: KeyboardEvent) {
|
||||
// Stop speech to text if keyboard shortcut is released
|
||||
if (event.key === 's' && event.getModifierState('Control')) await this.stopSpeechToText(event);
|
||||
}
|
||||
|
||||
processOnlineReferences(referenceSection: HTMLElement, onlineContext: any) {
|
||||
let numOnlineReferences = 0;
|
||||
for (let subquery in onlineContext) {
|
||||
@@ -294,6 +350,57 @@ export class KhojChatView extends KhojPaneView {
|
||||
return referenceButton;
|
||||
}
|
||||
|
||||
textToSpeech(message: string, event: MouseEvent | null = null): void {
|
||||
// Replace the speaker with a loading icon.
|
||||
let loader = document.createElement("span");
|
||||
loader.classList.add("loader");
|
||||
|
||||
let speechButton: HTMLButtonElement;
|
||||
let speechIcon: Element;
|
||||
|
||||
if (event === null) {
|
||||
// Pick the last speech button if none is provided
|
||||
let speechButtons = document.getElementsByClassName("speech-button");
|
||||
speechButton = speechButtons[speechButtons.length - 1] as HTMLButtonElement;
|
||||
|
||||
let speechIcons = document.getElementsByClassName("speech-icon");
|
||||
speechIcon = speechIcons[speechIcons.length - 1];
|
||||
} else {
|
||||
speechButton = event.currentTarget as HTMLButtonElement;
|
||||
speechIcon = event.target as Element;
|
||||
}
|
||||
|
||||
speechButton.appendChild(loader);
|
||||
speechButton.disabled = true;
|
||||
|
||||
const context = new AudioContext();
|
||||
let textToSpeechApi = `${this.setting.khojUrl}/api/chat/speech?text=${encodeURIComponent(message)}`;
|
||||
fetch(textToSpeechApi, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
"Authorization": `Bearer ${this.setting.khojApiKey}`,
|
||||
},
|
||||
})
|
||||
.then(response => response.arrayBuffer())
|
||||
.then(arrayBuffer => context.decodeAudioData(arrayBuffer))
|
||||
.then(audioBuffer => {
|
||||
const source = context.createBufferSource();
|
||||
source.buffer = audioBuffer;
|
||||
source.connect(context.destination);
|
||||
source.start(0);
|
||||
source.onended = function() {
|
||||
speechButton.removeChild(loader);
|
||||
speechButton.disabled = false;
|
||||
};
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Error playing speech:", err);
|
||||
speechButton.removeChild(loader);
|
||||
speechButton.disabled = false; // Consider enabling the button again to allow retrying
|
||||
});
|
||||
}
|
||||
|
||||
formatHTMLMessage(message: string, raw = false, willReplace = true) {
|
||||
// Remove any text between <s>[INST] and </s> tags. These are spurious instructions for some AI chat model.
|
||||
message = message.replace(/<s>\[INST\].+(<\/s>)?/g, '');
|
||||
@@ -461,19 +568,36 @@ export class KhojChatView extends KhojPaneView {
|
||||
|
||||
renderActionButtons(message: string, chat_message_body_text_el: HTMLElement) {
|
||||
let copyButton = this.contentEl.createEl('button');
|
||||
copyButton.classList.add("copy-button");
|
||||
copyButton.classList.add("chat-action-button");
|
||||
copyButton.title = "Copy Message to Clipboard";
|
||||
setIcon(copyButton, "copy-plus");
|
||||
copyButton.addEventListener('click', createCopyParentText(message));
|
||||
chat_message_body_text_el.append(copyButton);
|
||||
|
||||
// Add button to paste into current buffer
|
||||
let pasteToFile = this.contentEl.createEl('button');
|
||||
pasteToFile.classList.add("copy-button");
|
||||
pasteToFile.classList.add("chat-action-button");
|
||||
pasteToFile.title = "Paste Message to File";
|
||||
setIcon(pasteToFile, "clipboard-paste");
|
||||
pasteToFile.addEventListener('click', (event) => { pasteTextAtCursor(createCopyParentText(message, 'clipboard-paste')(event)); });
|
||||
chat_message_body_text_el.append(pasteToFile);
|
||||
|
||||
// Only enable the speech feature if the user is subscribed
|
||||
let speechButton = null;
|
||||
|
||||
if (this.setting.userInfo?.is_active) {
|
||||
// Create a speech button icon to play the message out loud
|
||||
speechButton = this.contentEl.createEl('button');
|
||||
speechButton.classList.add("chat-action-button", "speech-button");
|
||||
speechButton.title = "Listen to Message";
|
||||
setIcon(speechButton, "speech")
|
||||
speechButton.addEventListener('click', (event) => this.textToSpeech(message, event));
|
||||
}
|
||||
|
||||
// Append buttons to parent element
|
||||
chat_message_body_text_el.append(copyButton, pasteToFile);
|
||||
|
||||
if (speechButton) {
|
||||
chat_message_body_text_el.append(speechButton);
|
||||
}
|
||||
}
|
||||
|
||||
formatDate(date: Date): string {
|
||||
@@ -483,14 +607,16 @@ export class KhojChatView extends KhojPaneView {
|
||||
return `${time_string}, ${date_string}`;
|
||||
}
|
||||
|
||||
createNewConversation(chatBodyEl: HTMLElement) {
|
||||
createNewConversation() {
|
||||
let chatBodyEl = this.contentEl.getElementsByClassName("khoj-chat-body")[0] as HTMLElement;
|
||||
chatBodyEl.innerHTML = "";
|
||||
chatBodyEl.dataset.conversationId = "";
|
||||
chatBodyEl.dataset.conversationTitle = "";
|
||||
this.renderMessage(chatBodyEl, "Hey 👋🏾, what's up?", "khoj");
|
||||
}
|
||||
|
||||
async toggleChatSessions(chatBodyEl: HTMLElement, forceShow: boolean = false): Promise<boolean> {
|
||||
async toggleChatSessions(forceShow: boolean = false): Promise<boolean> {
|
||||
let chatBodyEl = this.contentEl.getElementsByClassName("khoj-chat-body")[0] as HTMLElement;
|
||||
if (!forceShow && this.contentEl.getElementsByClassName("side-panel")?.length > 0) {
|
||||
chatBodyEl.innerHTML = "";
|
||||
return this.getChatHistory(chatBodyEl);
|
||||
@@ -504,9 +630,10 @@ export class KhojChatView extends KhojPaneView {
|
||||
const newConversationButtonEl = newConversationEl.createEl("button");
|
||||
newConversationButtonEl.classList.add("new-conversation-button");
|
||||
newConversationButtonEl.classList.add("side-panel-button");
|
||||
newConversationButtonEl.addEventListener('click', (_) => this.createNewConversation(chatBodyEl));
|
||||
newConversationButtonEl.addEventListener('click', (_) => this.createNewConversation());
|
||||
setIcon(newConversationButtonEl, "plus");
|
||||
newConversationButtonEl.innerHTML += "New";
|
||||
newConversationButtonEl.title = "New Conversation (^N)";
|
||||
|
||||
const existingConversationsEl = sidePanelEl.createDiv("existing-conversations");
|
||||
const conversationListEl = existingConversationsEl.createDiv("conversation-list");
|
||||
@@ -666,7 +793,7 @@ export class KhojChatView extends KhojPaneView {
|
||||
chatBodyEl.innerHTML = "";
|
||||
chatBodyEl.dataset.conversationId = "";
|
||||
chatBodyEl.dataset.conversationTitle = "";
|
||||
this.toggleChatSessions(chatBodyEl, true);
|
||||
this.toggleChatSessions(true);
|
||||
})
|
||||
.catch(err => {
|
||||
return;
|
||||
@@ -727,7 +854,7 @@ export class KhojChatView extends KhojPaneView {
|
||||
return true;
|
||||
}
|
||||
|
||||
async readChatStream(response: Response, responseElement: HTMLDivElement): Promise<void> {
|
||||
async readChatStream(response: Response, responseElement: HTMLDivElement, isVoice: boolean = false): Promise<void> {
|
||||
// Exit if response body is empty
|
||||
if (response.body == null) return;
|
||||
|
||||
@@ -737,8 +864,12 @@ export class KhojChatView extends KhojPaneView {
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
|
||||
// Break if the stream is done
|
||||
if (done) break;
|
||||
if (done) {
|
||||
// Automatically respond with voice if the subscribed user has sent voice message
|
||||
if (isVoice && this.setting.userInfo?.is_active) this.textToSpeech(this.result);
|
||||
// Break if the stream is done
|
||||
break;
|
||||
}
|
||||
|
||||
let responseText = decoder.decode(value);
|
||||
if (responseText.includes("### compiled references:")) {
|
||||
@@ -756,7 +887,7 @@ export class KhojChatView extends KhojPaneView {
|
||||
}
|
||||
}
|
||||
|
||||
async getChatResponse(query: string | undefined | null): Promise<void> {
|
||||
async getChatResponse(query: string | undefined | null, isVoice: boolean = false): Promise<void> {
|
||||
// Exit if query is empty
|
||||
if (!query || query === "") return;
|
||||
|
||||
@@ -835,7 +966,7 @@ export class KhojChatView extends KhojPaneView {
|
||||
}
|
||||
} else {
|
||||
// Stream and render chat response
|
||||
await this.readChatStream(response, responseElement);
|
||||
await this.readChatStream(response, responseElement, isVoice);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(`Khoj chat response failed with\n${err}`);
|
||||
@@ -883,7 +1014,7 @@ export class KhojChatView extends KhojPaneView {
|
||||
|
||||
sendMessageTimeout: NodeJS.Timeout | undefined;
|
||||
mediaRecorder: MediaRecorder | undefined;
|
||||
async speechToText(event: MouseEvent | TouchEvent) {
|
||||
async speechToText(event: MouseEvent | TouchEvent | KeyboardEvent) {
|
||||
event.preventDefault();
|
||||
const transcribeButton = <HTMLButtonElement>this.contentEl.getElementsByClassName("khoj-transcribe")[0];
|
||||
const chatInput = <HTMLTextAreaElement>this.contentEl.getElementsByClassName("khoj-chat-input")[0];
|
||||
@@ -916,9 +1047,19 @@ export class KhojChatView extends KhojPaneView {
|
||||
});
|
||||
|
||||
// Parse response from Khoj backend
|
||||
let noSpeechText: string[] = [
|
||||
"Thanks for watching!",
|
||||
"Thanks for watching.",
|
||||
"Thank you for watching!",
|
||||
"Thank you for watching.",
|
||||
"You",
|
||||
"Bye."
|
||||
];
|
||||
let noSpeech: boolean = false;
|
||||
if (response.status === 200) {
|
||||
console.log(response);
|
||||
chatInput.value += response.json.text.trimStart();
|
||||
noSpeech = noSpeechText.includes(response.json.text.trimStart());
|
||||
if (!noSpeech) chatInput.value += response.json.text.trimStart();
|
||||
this.autoResize();
|
||||
} else if (response.status === 501) {
|
||||
throw new Error("⛔️ Configure speech-to-text model on server.");
|
||||
@@ -928,8 +1069,8 @@ export class KhojChatView extends KhojPaneView {
|
||||
throw new Error("⛔️ Failed to transcribe audio.");
|
||||
}
|
||||
|
||||
// Don't auto-send empty messages
|
||||
if (chatInput.value.length === 0) return;
|
||||
// Don't auto-send empty messages or when no speech is detected
|
||||
if (chatInput.value.length === 0 || noSpeech) return;
|
||||
|
||||
// Show stop auto-send button. It stops auto-send when clicked
|
||||
setIcon(sendButton, "stop-circle");
|
||||
@@ -938,6 +1079,7 @@ export class KhojChatView extends KhojPaneView {
|
||||
|
||||
// Start the countdown timer UI
|
||||
stopSendButtonImg.getElementsByTagName("circle")[0].style.animation = "countdown 3s linear 1 forwards";
|
||||
stopSendButtonImg.getElementsByTagName("circle")[0].style.color = "var(--icon-color-active)";
|
||||
|
||||
// Auto send message after 3 seconds
|
||||
this.sendMessageTimeout = setTimeout(() => {
|
||||
@@ -947,7 +1089,7 @@ export class KhojChatView extends KhojPaneView {
|
||||
sendImg.addEventListener('click', async (_) => { await this.chat() });
|
||||
|
||||
// Send message
|
||||
this.chat();
|
||||
this.chat(true);
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
@@ -966,21 +1108,23 @@ export class KhojChatView extends KhojPaneView {
|
||||
});
|
||||
|
||||
this.mediaRecorder.start();
|
||||
setIcon(transcribeButton, "mic-off");
|
||||
// setIcon(transcribeButton, "mic-off");
|
||||
transcribeButton.classList.add("loading-encircle")
|
||||
};
|
||||
|
||||
// Toggle recording
|
||||
if (!this.mediaRecorder || this.mediaRecorder.state === 'inactive' || event.type === 'touchstart') {
|
||||
if (!this.mediaRecorder || this.mediaRecorder.state === 'inactive' || event.type === 'touchstart' || event.type === 'mousedown' || event.type === 'keydown') {
|
||||
navigator.mediaDevices
|
||||
.getUserMedia({ audio: true })
|
||||
?.then(handleRecording)
|
||||
.catch((e) => {
|
||||
this.flashStatusInChatInput("⛔️ Failed to access microphone");
|
||||
});
|
||||
} else if (this.mediaRecorder.state === 'recording' || event.type === 'touchend' || event.type === 'touchcancel') {
|
||||
} else if (this.mediaRecorder?.state === 'recording' || event.type === 'touchend' || event.type === 'touchcancel' || event.type === 'mouseup' || event.type === 'keyup') {
|
||||
this.mediaRecorder.stop();
|
||||
this.mediaRecorder.stream.getTracks().forEach(track => track.stop());
|
||||
this.mediaRecorder = undefined;
|
||||
transcribeButton.classList.remove("loading-encircle");
|
||||
setIcon(transcribeButton, "mic");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@ import { Plugin, WorkspaceLeaf } from 'obsidian';
|
||||
import { KhojSetting, KhojSettingTab, DEFAULT_SETTINGS } from 'src/settings'
|
||||
import { KhojSearchModal } from 'src/search_modal'
|
||||
import { KhojChatView } from 'src/chat_view'
|
||||
import { updateContentIndex, canConnectToBackend, KhojView } from './utils';
|
||||
import { updateContentIndex, canConnectToBackend, KhojView, jumpToPreviousView } from './utils';
|
||||
import { KhojPaneView } from './pane_view';
|
||||
|
||||
|
||||
export default class Khoj extends Plugin {
|
||||
@@ -79,16 +80,30 @@ export default class Khoj extends Plugin {
|
||||
const leaves = workspace.getLeavesOfType(viewType);
|
||||
|
||||
if (leaves.length > 0) {
|
||||
// A leaf with our view already exists, use that
|
||||
leaf = leaves[0];
|
||||
// A leaf with our view already exists, use that
|
||||
leaf = leaves[0];
|
||||
} else {
|
||||
// Our view could not be found in the workspace, create a new leaf
|
||||
// in the right sidebar for it
|
||||
leaf = workspace.getRightLeaf(false);
|
||||
await leaf?.setViewState({ type: viewType, active: true });
|
||||
// Our view could not be found in the workspace, create a new leaf
|
||||
// in the right sidebar for it
|
||||
leaf = workspace.getRightLeaf(false);
|
||||
await leaf?.setViewState({ type: viewType, active: true });
|
||||
}
|
||||
|
||||
// "Reveal" the leaf in case it is in a collapsed sidebar
|
||||
if (leaf) workspace.revealLeaf(leaf);
|
||||
}
|
||||
if (leaf) {
|
||||
const activeKhojLeaf = workspace.getActiveViewOfType(KhojPaneView)?.leaf;
|
||||
// Jump to the previous view if the current view is Khoj Side Pane
|
||||
if (activeKhojLeaf === leaf) jumpToPreviousView();
|
||||
// Else Reveal the leaf in case it is in a collapsed sidebar
|
||||
else {
|
||||
workspace.revealLeaf(leaf);
|
||||
|
||||
if (viewType === KhojView.CHAT) {
|
||||
// focus on the chat input when the chat view is opened
|
||||
let chatView = leaf.view as KhojChatView;
|
||||
let chatInput = <HTMLTextAreaElement>chatView.contentEl.getElementsByClassName("khoj-chat-input")[0];
|
||||
if (chatInput) chatInput.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,16 +38,24 @@ export abstract class KhojPaneView extends ItemView {
|
||||
const leaves = workspace.getLeavesOfType(viewType);
|
||||
|
||||
if (leaves.length > 0) {
|
||||
// A leaf with our view already exists, use that
|
||||
leaf = leaves[0];
|
||||
// A leaf with our view already exists, use that
|
||||
leaf = leaves[0];
|
||||
} else {
|
||||
// Our view could not be found in the workspace, create a new leaf
|
||||
// in the right sidebar for it
|
||||
leaf = workspace.getRightLeaf(false);
|
||||
await leaf?.setViewState({ type: viewType, active: true });
|
||||
// Our view could not be found in the workspace, create a new leaf
|
||||
// in the right sidebar for it
|
||||
leaf = workspace.getRightLeaf(false);
|
||||
await leaf?.setViewState({ type: viewType, active: true });
|
||||
}
|
||||
|
||||
// "Reveal" the leaf in case it is in a collapsed sidebar
|
||||
if (leaf) workspace.revealLeaf(leaf);
|
||||
}
|
||||
if (leaf) {
|
||||
if (viewType === KhojView.CHAT) {
|
||||
// focus on the chat input when the chat view is opened
|
||||
let chatInput = <HTMLTextAreaElement>this.contentEl.getElementsByClassName("khoj-chat-input")[0];
|
||||
if (chatInput) chatInput.focus();
|
||||
}
|
||||
|
||||
// "Reveal" the leaf in case it is in a collapsed sidebar
|
||||
workspace.revealLeaf(leaf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,6 +333,12 @@ export function createCopyParentText(message: string, originalButton: string = '
|
||||
}
|
||||
}
|
||||
|
||||
export function jumpToPreviousView() {
|
||||
const editor: Editor = this.app.workspace.getActiveFileView()?.editor
|
||||
if (!editor) return;
|
||||
editor.focus();
|
||||
}
|
||||
|
||||
export function pasteTextAtCursor(text: string | undefined) {
|
||||
// Get the current active file's editor
|
||||
const editor: Editor = this.app.workspace.getActiveFileView()?.editor
|
||||
|
||||
@@ -477,7 +477,7 @@ span.khoj-nav-item-text {
|
||||
}
|
||||
|
||||
/* Copy button */
|
||||
button.copy-button {
|
||||
button.chat-action-button {
|
||||
display: block;
|
||||
border-radius: 4px;
|
||||
color: var(--text-muted);
|
||||
@@ -491,20 +491,54 @@ button.copy-button {
|
||||
margin-top: 8px;
|
||||
float: right;
|
||||
}
|
||||
button.copy-button span {
|
||||
button.chat-action-button span {
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
transition: 0.5s;
|
||||
}
|
||||
button.chat-action-button:hover {
|
||||
background-color: var(--background-modifier-active-hover);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
img.copy-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
button.copy-button:hover {
|
||||
background-color: var(--background-modifier-active-hover);
|
||||
color: var(--text-normal);
|
||||
/* Circular Loading Spinner */
|
||||
.loader {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 3px solid #FFF;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
animation: rotation 1s linear infinite;
|
||||
}
|
||||
.loader::after {
|
||||
content: '';
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
border: 3px solid transparent;
|
||||
border-bottom-color: var(--flower);
|
||||
}
|
||||
|
||||
@keyframes rotation {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Loading Spinner */
|
||||
@@ -564,6 +598,44 @@ button.copy-button:hover {
|
||||
}
|
||||
}
|
||||
|
||||
/* Loading Encircle */
|
||||
.loading-encircle {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.loading-encircle::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-top: -16px;
|
||||
margin-left: -16px;
|
||||
border: 4px solid transparent;
|
||||
border-color: var(--icon-color-active);
|
||||
border-radius: 50%;
|
||||
animation: pulse 3s ease-in-out infinite;
|
||||
}
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.2);
|
||||
opacity: 0.2;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
@media only screen and (max-width: 600px) {
|
||||
div.khoj-header {
|
||||
display: grid;
|
||||
|
||||
@@ -52,5 +52,6 @@
|
||||
"1.12.1": "0.15.0",
|
||||
"1.13.0": "0.15.0",
|
||||
"1.14.0": "0.15.0",
|
||||
"1.15.0": "0.15.0"
|
||||
"1.15.0": "0.15.0",
|
||||
"1.16.0": "0.15.0"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user