mirror of
https://github.com/khoaliber/khoj.git
synced 2026-03-04 21:29:12 +00:00
Create Chat Modal for Obsidian Plugin
Merge pull request #196 from debanjum/create-chat-modal-for-obsidian - Set your OpenAI API key in the Khoj Obsidian Settings - Use Modal in Obsidian for Chat - Style Chat Modal combining the Khoj Web interface and Obsidian theme style
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
- [Setup Plugin](#2-Setup-Plugin)
|
||||
- [Use](#Use)
|
||||
- [Search](#search)
|
||||
- [Chat](#chat)
|
||||
- [Find Similar Notes](#find-similar-notes)
|
||||
- [Upgrade](#Upgrade)
|
||||
- [Upgrade Backend](#1-Upgrade-Backend)
|
||||
@@ -21,9 +22,14 @@
|
||||
- [Implementation](#Implementation)
|
||||
|
||||
## Features
|
||||
- **Natural**: Advanced natural language understanding using Transformer based ML Models
|
||||
- **Local**: Your personal data stays local. All search, indexing is done on your machine[\*](https://github.com/debanjum/khoj#miscellaneous)
|
||||
- **Incremental**: Incremental search for a fast, search-as-you-type experience
|
||||
- **Search**
|
||||
- **Natural**: Advanced natural language understanding using Transformer based ML Models
|
||||
- **Local**: Your personal data stays local. All search and indexing is done on your machine. *Unlike chat which requires access to GPT.*
|
||||
- **Incremental**: Incremental search for a fast, search-as-you-type experience
|
||||
- **Chat**
|
||||
- **Faster answers**: Find answers faster and with less effort than search
|
||||
- **Iterative discovery**: Iteratively explore and (re-)discover your notes
|
||||
- **Assisted creativity**: Smoothly weave across answers retrieval and content generation
|
||||
|
||||
## Demo
|
||||
https://user-images.githubusercontent.com/6413477/210486007-36ee3407-e6aa-4185-8a26-b0bfc0a4344f.mp4
|
||||
@@ -55,10 +61,21 @@ pip install khoj-assistant && khoj --no-gui
|
||||
### 2. Setup Plugin
|
||||
1. Open [Khoj](https://obsidian.md/plugins?id=khoj) from the *Community plugins* tab in Obsidian settings panel
|
||||
2. Click *Install*, then *Enable* on the Khoj plugin page in Obsidian
|
||||
3. [Optional] To enable Khoj Chat, set your [OpenAI API key](https://platform.openai.com/account/api-keys) in the Khoj plugin settings
|
||||
|
||||
See [official Obsidian plugin docs](https://help.obsidian.md/Extending+Obsidian/Community+plugins) for details
|
||||
|
||||
## Use
|
||||
### Chat
|
||||
Run *Khoj: Chat* from the [Command Palette](https://help.obsidian.md/Plugins/Command+palette) and ask questions in a natural, conversational style.
|
||||
E.g "When did I file my taxes last year?"
|
||||
|
||||
Notes:
|
||||
- *Using Khoj Chat will result in query relevant notes being shared with OpenAI for ChatGPT to respond.*
|
||||
- *To use Khoj Chat, ensure you've set your [OpenAI API key](https://platform.openai.com/account/api-keys) in the Khoj plugin settings.*
|
||||
|
||||
See [[https://github.com/debanjum/khoj/tree/master/#Khoj-Chat][Khoj Chat]] for more details
|
||||
|
||||
### Search
|
||||
Click the *Khoj search* icon 🔎 on the [Ribbon](https://help.obsidian.md/User+interface/Workspace/Ribbon) or run *Khoj: Search* from the [Command Palette](https://help.obsidian.md/Plugins/Command+palette)
|
||||
|
||||
|
||||
130
src/interface/obsidian/src/chat_modal.ts
Normal file
130
src/interface/obsidian/src/chat_modal.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { App, Modal, request, Setting } from 'obsidian';
|
||||
import { KhojSetting } from 'src/settings';
|
||||
|
||||
|
||||
export class KhojChatModal extends Modal {
|
||||
result: string;
|
||||
setting: KhojSetting;
|
||||
|
||||
constructor(app: App, setting: KhojSetting) {
|
||||
super(app);
|
||||
this.setting = setting;
|
||||
|
||||
// Register Modal Keybindings to send user message
|
||||
this.scope.register([], 'Enter', async () => {
|
||||
// Get text in chat input elmenet
|
||||
let input_el = <HTMLInputElement>this.contentEl.getElementsByClassName("khoj-chat-input")[0];
|
||||
|
||||
// Clear text after extracting message to send
|
||||
let user_message = input_el.value;
|
||||
input_el.value = "";
|
||||
|
||||
// Get and render chat response to user message
|
||||
await this.getChatResponse(user_message);
|
||||
});
|
||||
}
|
||||
|
||||
async onOpen() {
|
||||
let { contentEl } = this;
|
||||
contentEl.addClass("khoj-chat");
|
||||
|
||||
// Add title to the Khoj Chat modal
|
||||
contentEl.createEl("h1", ({ attr: { id: "khoj-chat-title" }, text: "Khoj Chat" }));
|
||||
|
||||
// Create area for chat logs
|
||||
contentEl.createDiv({ attr: { id: "khoj-chat-body", class: "khoj-chat-body" } });
|
||||
|
||||
// Get conversation history from Khoj backend
|
||||
let chatUrl = `${this.setting.khojUrl}/api/chat?`;
|
||||
let response = await request(chatUrl);
|
||||
let chatLogs = JSON.parse(response).response;
|
||||
chatLogs.forEach((chatLog: any) => {
|
||||
this.renderMessageWithReferences(chatLog.message, chatLog.by, chatLog.context, new Date(chatLog.created));
|
||||
});
|
||||
|
||||
// Add chat input field
|
||||
contentEl.createEl("input",
|
||||
{
|
||||
attr: {
|
||||
type: "text",
|
||||
id: "khoj-chat-input",
|
||||
autofocus: "autofocus",
|
||||
placeholder: "Chat with Khoj 🦅 [Hit Enter to send message]",
|
||||
class: "khoj-chat-input option"
|
||||
}
|
||||
})
|
||||
.addEventListener('change', (event) => { this.result = (<HTMLInputElement>event.target).value });
|
||||
|
||||
// Scroll to bottom of modal, till the send message input box
|
||||
this.modalEl.scrollTop = this.modalEl.scrollHeight;
|
||||
}
|
||||
|
||||
generateReference(messageEl: any, reference: string, index: number) {
|
||||
// Generate HTML for Chat Reference
|
||||
// `<sup><abbr title="${escaped_ref}" tabindex="0">${index}</abbr></sup>`;
|
||||
let escaped_ref = reference.replace(/"/g, "\\\"")
|
||||
return messageEl.createEl("sup").createEl("abbr", {
|
||||
attr: {
|
||||
title: escaped_ref,
|
||||
tabindex: "0",
|
||||
},
|
||||
text: `[${index}] `,
|
||||
});
|
||||
}
|
||||
|
||||
renderMessageWithReferences(message: string, sender: string, context?: [string], dt?: Date) {
|
||||
let messageEl = this.renderMessage(message, sender, dt);
|
||||
if (context && !!messageEl) {
|
||||
context.map((reference, index) => this.generateReference(messageEl, reference, index+1));
|
||||
}
|
||||
}
|
||||
|
||||
renderMessage(message: string, sender: string, dt?: Date): Element | null {
|
||||
let message_time = this.formatDate(dt ?? new Date());
|
||||
let emojified_sender = sender == "khoj" ? "🦅 Khoj" : "🤔 You";
|
||||
|
||||
// Append message to conversation history HTML element.
|
||||
// The chat logs should display above the message input box to follow standard UI semantics
|
||||
let chat_body_el = this.contentEl.getElementsByClassName("khoj-chat-body")[0];
|
||||
let chat_message_el = chat_body_el.createDiv({
|
||||
attr: {
|
||||
"data-meta": `${emojified_sender} at ${message_time}`,
|
||||
class: `khoj-chat-message ${sender}`
|
||||
},
|
||||
}).createDiv({
|
||||
attr: {
|
||||
class: `khoj-chat-message-text ${sender}`
|
||||
},
|
||||
text: `${message}`
|
||||
})
|
||||
|
||||
// Scroll to bottom after inserting chat messages
|
||||
this.modalEl.scrollTop = this.modalEl.scrollHeight;
|
||||
|
||||
return chat_message_el
|
||||
}
|
||||
|
||||
formatDate(date: Date): string {
|
||||
// Format date in HH:MM, DD MMM YYYY format
|
||||
let time_string = date.toLocaleTimeString('en-IN', { hour: '2-digit', minute: '2-digit', hour12: false });
|
||||
let date_string = date.toLocaleString('en-IN', { year: 'numeric', month: 'short', day: '2-digit' }).replace(/-/g, ' ');
|
||||
return `${time_string}, ${date_string}`;
|
||||
}
|
||||
|
||||
async getChatResponse(query: string | undefined | null): Promise<void> {
|
||||
// Exit if query is empty
|
||||
if (!query || query === "") return;
|
||||
|
||||
// Render user query as chat message
|
||||
this.renderMessage(query, "you");
|
||||
|
||||
// Get chat response from Khoj backend
|
||||
let encodedQuery = encodeURIComponent(query);
|
||||
let chatUrl = `${this.setting.khojUrl}/api/chat?q=${encodedQuery}`;
|
||||
let response = await request(chatUrl);
|
||||
let data = JSON.parse(response);
|
||||
|
||||
// Render Khoj response as chat message
|
||||
this.renderMessage(data.response, "khoj");
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Notice, Plugin } from 'obsidian';
|
||||
import { KhojSetting, KhojSettingTab, DEFAULT_SETTINGS } from 'src/settings'
|
||||
import { KhojModal } from 'src/modal'
|
||||
import { KhojSearchModal } from 'src/search_modal'
|
||||
import { KhojChatModal } from 'src/chat_modal'
|
||||
import { configureKhojBackend } from './utils';
|
||||
|
||||
|
||||
@@ -16,7 +17,7 @@ export default class Khoj extends Plugin {
|
||||
name: 'Search',
|
||||
checkCallback: (checking) => {
|
||||
if (!checking && this.settings.connectedToBackend)
|
||||
new KhojModal(this.app, this.settings).open();
|
||||
new KhojSearchModal(this.app, this.settings).open();
|
||||
return this.settings.connectedToBackend;
|
||||
}
|
||||
});
|
||||
@@ -27,16 +28,27 @@ export default class Khoj extends Plugin {
|
||||
name: 'Find similar notes',
|
||||
editorCheckCallback: (checking) => {
|
||||
if (!checking && this.settings.connectedToBackend)
|
||||
new KhojModal(this.app, this.settings, true).open();
|
||||
new KhojSearchModal(this.app, this.settings, true).open();
|
||||
return this.settings.connectedToBackend;
|
||||
}
|
||||
});
|
||||
|
||||
// Add chat command. It can be triggered from anywhere
|
||||
this.addCommand({
|
||||
id: 'chat',
|
||||
name: 'Chat',
|
||||
checkCallback: (checking) => {
|
||||
if (!checking && this.settings.connectedToBackend && !!this.settings.openaiApiKey)
|
||||
new KhojChatModal(this.app, this.settings).open();
|
||||
return !!this.settings.openaiApiKey;
|
||||
}
|
||||
});
|
||||
|
||||
// Create an icon in the left ribbon.
|
||||
this.addRibbonIcon('search', 'Khoj', (_: MouseEvent) => {
|
||||
// Called when the user clicks the icon.
|
||||
this.settings.connectedToBackend
|
||||
? new KhojModal(this.app, this.settings).open()
|
||||
? new KhojSearchModal(this.app, this.settings).open()
|
||||
: new Notice(`❗️Ensure Khoj backend is running and Khoj URL is pointing to it in the plugin settings`);
|
||||
});
|
||||
|
||||
@@ -59,5 +71,5 @@ export default class Khoj extends Plugin {
|
||||
await configureKhojBackend(this.app.vault, this.settings, false);
|
||||
}
|
||||
this.saveData(this.settings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ export interface SearchResult {
|
||||
file: string;
|
||||
}
|
||||
|
||||
export class KhojModal extends SuggestModal<SearchResult> {
|
||||
export class KhojSearchModal extends SuggestModal<SearchResult> {
|
||||
setting: KhojSetting;
|
||||
rerank: boolean = false;
|
||||
find_similar_notes: boolean;
|
||||
@@ -2,6 +2,7 @@ import { App, Notice, PluginSettingTab, request, Setting } from 'obsidian';
|
||||
import Khoj from 'src/main';
|
||||
|
||||
export interface KhojSetting {
|
||||
openaiApiKey: string;
|
||||
resultsCount: number;
|
||||
khojUrl: string;
|
||||
connectedToBackend: boolean;
|
||||
@@ -13,6 +14,7 @@ export const DEFAULT_SETTINGS: KhojSetting = {
|
||||
khojUrl: 'http://localhost:8000',
|
||||
connectedToBackend: false,
|
||||
autoConfigure: true,
|
||||
openaiApiKey: '',
|
||||
}
|
||||
|
||||
export class KhojSettingTab extends PluginSettingTab {
|
||||
@@ -41,7 +43,16 @@ export class KhojSettingTab extends PluginSettingTab {
|
||||
await this.plugin.saveSettings();
|
||||
containerEl.firstElementChild?.setText(this.getBackendStatusMessage());
|
||||
}));
|
||||
new Setting(containerEl)
|
||||
new Setting(containerEl)
|
||||
.setName('OpenAI API Key')
|
||||
.setDesc('Your OpenAI API Key for Khoj Chat')
|
||||
.addText(text => text
|
||||
.setValue(`${this.plugin.settings.openaiApiKey}`)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.openaiApiKey = value.trim();
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
new Setting(containerEl)
|
||||
.setName('Results Count')
|
||||
.setDesc('The number of search results to show')
|
||||
.addSlider(slider => slider
|
||||
@@ -110,7 +121,7 @@ export class KhojSettingTab extends PluginSettingTab {
|
||||
|
||||
getBackendStatusMessage() {
|
||||
return !this.plugin.settings.connectedToBackend
|
||||
? '❗Disconnected from Khoj backend. Ensure Khoj backend is running and Khoj URL is correctly set below.'
|
||||
: '✅ Connected to Khoj backend.';
|
||||
? '❗Disconnected from Khoj backend. Ensure Khoj backend is running and Khoj URL is correctly set below.'
|
||||
: '✅ Connected to Khoj backend.';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,10 +29,11 @@ export async function configureKhojBackend(vault: Vault, setting: KhojSetting, n
|
||||
|
||||
// Set index name from the path of the current vault
|
||||
let indexName = getVaultAbsolutePath(vault).replace(/\//g, '_').replace(/ /g, '_');
|
||||
// Get default index directory from khoj backend
|
||||
let khojDefaultIndexDirectory = await request(`${khojConfigUrl}/default`)
|
||||
.then(response => JSON.parse(response))
|
||||
.then(data => { return getIndexDirectoryFromBackendConfig(data); });
|
||||
// Get default config fields from khoj backend
|
||||
let defaultConfig = await request(`${khojConfigUrl}/default`).then(response => JSON.parse(response));
|
||||
let khojDefaultIndexDirectory = getIndexDirectoryFromBackendConfig(defaultConfig["content-type"]["markdown"]["embeddings-file"]);
|
||||
let khojDefaultChatDirectory = getIndexDirectoryFromBackendConfig(defaultConfig["processor"]["conversation"]["conversation-logfile"]);
|
||||
let khojDefaultChatModelName = defaultConfig["processor"]["conversation"]["model"];
|
||||
|
||||
// Get current config if khoj backend configured, else get default config from khoj backend
|
||||
await request(khoj_already_configured ? khojConfigUrl : `${khojConfigUrl}/default`)
|
||||
@@ -49,14 +50,7 @@ export async function configureKhojBackend(vault: Vault, setting: KhojSetting, n
|
||||
"compressed-jsonl": `${khojDefaultIndexDirectory}/${indexName}.jsonl.gz`,
|
||||
}
|
||||
}
|
||||
// Disable khoj processors, as not required
|
||||
delete data["processor"];
|
||||
|
||||
// Save new config and refresh index on khoj backend
|
||||
updateKhojBackend(setting.khojUrl, data);
|
||||
console.log(`Khoj: Created khoj backend config:\n${JSON.stringify(data)}`)
|
||||
}
|
||||
|
||||
// Else if khoj config has no markdown content config
|
||||
else if (!data["content-type"]["markdown"]) {
|
||||
// Add markdown config to khoj content-type config
|
||||
@@ -67,28 +61,59 @@ export async function configureKhojBackend(vault: Vault, setting: KhojSetting, n
|
||||
"embeddings-file": `${khojDefaultIndexDirectory}/${indexName}.pt`,
|
||||
"compressed-jsonl": `${khojDefaultIndexDirectory}/${indexName}.jsonl.gz`,
|
||||
}
|
||||
|
||||
// Save updated config and refresh index on khoj backend
|
||||
updateKhojBackend(setting.khojUrl, data);
|
||||
console.log(`Khoj: Added markdown config to khoj backend config:\n${JSON.stringify(data["content-type"])}`)
|
||||
}
|
||||
|
||||
// Else if khoj is not configured to index markdown files in configured obsidian vault
|
||||
else if (data["content-type"]["markdown"]["input-filter"].length != 1 ||
|
||||
data["content-type"]["markdown"]["input-filter"][0] !== mdInVault) {
|
||||
// Update markdown config in khoj content-type config
|
||||
// Set markdown config to only index markdown files in configured obsidian vault
|
||||
let khojIndexDirectory = getIndexDirectoryFromBackendConfig(data);
|
||||
let khojIndexDirectory = getIndexDirectoryFromBackendConfig(data["content-type"]["markdown"]["embeddings-file"]);
|
||||
data["content-type"]["markdown"] = {
|
||||
"input-filter": [mdInVault],
|
||||
"input-files": null,
|
||||
"embeddings-file": `${khojIndexDirectory}/${indexName}.pt`,
|
||||
"compressed-jsonl": `${khojIndexDirectory}/${indexName}.jsonl.gz`,
|
||||
}
|
||||
// Save updated config and refresh index on khoj backend
|
||||
updateKhojBackend(setting.khojUrl, data);
|
||||
console.log(`Khoj: Updated markdown config in khoj backend config:\n${JSON.stringify(data["content-type"]["markdown"])}`)
|
||||
}
|
||||
|
||||
// If OpenAI API key not set in Khoj plugin settings
|
||||
if (!setting.openaiApiKey) {
|
||||
// Disable khoj processors, as not required
|
||||
delete data["processor"];
|
||||
}
|
||||
// Else if khoj backend not configured yet
|
||||
else if (!khoj_already_configured || !data["processor"]) {
|
||||
data["processor"] = {
|
||||
"conversation": {
|
||||
"conversation-logfile": `${khojDefaultChatDirectory}/conversation.json`,
|
||||
"model": khojDefaultChatModelName,
|
||||
"openai-api-key": setting.openaiApiKey,
|
||||
}
|
||||
}
|
||||
}
|
||||
// Else if khoj config has no conversation processor config
|
||||
else if (!data["processor"]["conversation"]) {
|
||||
data["processor"]["conversation"] = {
|
||||
"conversation-logfile": `${khojDefaultChatDirectory}/conversation.json`,
|
||||
"model": khojDefaultChatModelName,
|
||||
"openai-api-key": setting.openaiApiKey,
|
||||
}
|
||||
}
|
||||
// Else if khoj is not configured with OpenAI API key from khoj plugin settings
|
||||
else if (data["processor"]["conversation"]["openai-api-key"] !== setting.openaiApiKey) {
|
||||
data["processor"]["conversation"] = {
|
||||
"conversation-logfile": data["processor"]["conversation"]["conversation-logfile"],
|
||||
"model": data["procesor"]["conversation"]["model"],
|
||||
"openai-api-key": setting.openaiApiKey,
|
||||
}
|
||||
}
|
||||
|
||||
// Save updated config and refresh index on khoj backend
|
||||
updateKhojBackend(setting.khojUrl, data);
|
||||
if (!khoj_already_configured)
|
||||
console.log(`Khoj: Created khoj backend config:\n${JSON.stringify(data)}`)
|
||||
else
|
||||
console.log(`Khoj: Updated khoj backend config:\n${JSON.stringify(data)}`)
|
||||
})
|
||||
.catch(error => {
|
||||
if (notify)
|
||||
@@ -111,6 +136,6 @@ export async function updateKhojBackend(khojUrl: string, khojConfig: Object) {
|
||||
.then(_ => request(`${khojUrl}/api/update?t=markdown`));
|
||||
}
|
||||
|
||||
function getIndexDirectoryFromBackendConfig(khojConfig: any) {
|
||||
return khojConfig["content-type"]["markdown"]["embeddings-file"].split("/").slice(0, -1).join("/");
|
||||
function getIndexDirectoryFromBackendConfig(filepath: string) {
|
||||
return filepath.split("/").slice(0, -1).join("/");
|
||||
}
|
||||
|
||||
@@ -6,3 +6,142 @@ available in the app when your plugin is enabled.
|
||||
If your plugin does not need CSS, delete this file.
|
||||
|
||||
*/
|
||||
|
||||
:root {
|
||||
--khoj-chat-blue: #017eff;
|
||||
--khoj-chat-dark-grey: #475569;
|
||||
}
|
||||
|
||||
.khoj-chat {
|
||||
display: grid;
|
||||
background: var(--background-primary);
|
||||
color: var(--text-normal);
|
||||
text-align: center;
|
||||
font-family: roboto, karma, segoe ui, sans-serif;
|
||||
font-size: var(--font-ui-large);
|
||||
font-weight: 300;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
.khoj-chat > * {
|
||||
padding: 10px;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
#khoj-chat-title {
|
||||
font-weight: 200;
|
||||
color: var(--khoj-chat-blue);
|
||||
}
|
||||
|
||||
#khoj-chat-body {
|
||||
font-size: var(--font-ui-medium);
|
||||
margin: 0px;
|
||||
line-height: 20px;
|
||||
overflow-y: scroll; /* Make chat body scroll to see history */
|
||||
}
|
||||
/* add chat metatdata to bottom of bubble */
|
||||
.khoj-chat-message::after {
|
||||
content: attr(data-meta);
|
||||
display: block;
|
||||
font-size: var(--font-ui-smaller);
|
||||
color: var(--text-muted);
|
||||
margin: -12px 7px 0 -5px;
|
||||
}
|
||||
/* move message by khoj to left */
|
||||
.khoj-chat-message.khoj {
|
||||
margin-left: auto;
|
||||
text-align: left;
|
||||
}
|
||||
/* move message by you to right */
|
||||
.khoj-chat-message.you {
|
||||
margin-right: auto;
|
||||
text-align: right;
|
||||
}
|
||||
/* basic style chat message text */
|
||||
.khoj-chat-message-text {
|
||||
margin: 10px;
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
max-width: 80%;
|
||||
text-align: left;
|
||||
}
|
||||
/* color chat bubble by khoj blue */
|
||||
.khoj-chat-message-text.khoj {
|
||||
color: var(--text-on-accent);
|
||||
background: var(--khoj-chat-blue);
|
||||
margin-left: auto;
|
||||
white-space: pre-line;
|
||||
}
|
||||
/* add left protrusion to khoj chat bubble */
|
||||
.khoj-chat-message-text.khoj:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: -7px;
|
||||
border: 10px solid transparent;
|
||||
border-top-color: var(--khoj-chat-blue);
|
||||
border-bottom: 0;
|
||||
transform: rotate(-60deg);
|
||||
}
|
||||
/* color chat bubble by you dark grey */
|
||||
.khoj-chat-message-text.you {
|
||||
color: var(--text-on-accent);
|
||||
background: var(--khoj-chat-dark-grey);
|
||||
margin-right: auto;
|
||||
}
|
||||
/* add right protrusion to you chat bubble */
|
||||
.khoj-chat-message-text.you:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 91%;
|
||||
right: -2px;
|
||||
border: 10px solid transparent;
|
||||
border-left-color: var(--khoj-chat-dark-grey);
|
||||
border-right: 0;
|
||||
margin-top: -10px;
|
||||
transform: rotate(-60deg)
|
||||
}
|
||||
|
||||
#khoj-chat-footer {
|
||||
padding: 0;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(70px, 100%);
|
||||
grid-column-gap: 10px;
|
||||
grid-row-gap: 10px;
|
||||
}
|
||||
#khoj-chat-footer > * {
|
||||
padding: 15px;
|
||||
background: #f9fafc
|
||||
}
|
||||
#khoj-chat-input.option:hover {
|
||||
box-shadow: 0 0 11px var(--background-modifier-box-shadow);
|
||||
}
|
||||
#khoj-chat-input {
|
||||
font-size: var(--font-ui-medium);
|
||||
padding: 25px 20px;
|
||||
}
|
||||
|
||||
@media (pointer: coarse), (hover: none) {
|
||||
#khoj-chat-body.abbr[title] {
|
||||
position: relative;
|
||||
padding-left: 4px; /* space references out to ease tapping */
|
||||
}
|
||||
#khoj-chat-body.abbr[title]:focus:after {
|
||||
content: attr(title);
|
||||
|
||||
/* position tooltip */
|
||||
position: absolute;
|
||||
left: 16px; /* open tooltip to right of ref link, instead of on top of it */
|
||||
width: auto;
|
||||
z-index: 1; /* show tooltip above chat messages */
|
||||
|
||||
/* style tooltip */
|
||||
background-color: var(--background-secondary);
|
||||
color: var(--text-muted);
|
||||
border-radius: 2px;
|
||||
box-shadow: 1px 1px 4px 0 var(--background-modifier-box-shadow);
|
||||
font-size: var(--font-ui-small);
|
||||
padding: 2px 4px;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user