diff --git a/components/BardConnection.ts b/components/BardConnection.ts deleted file mode 100644 index 607c063..0000000 --- a/components/BardConnection.ts +++ /dev/null @@ -1,209 +0,0 @@ -import { url } from "inspector"; -import { Notice, RequestUrlParam, request, requestUrl } from "obsidian"; -import { join } from "path"; -import * as querystring from "querystring"; -import { json } from "stream/consumers"; - - -export class Bard { - #bard_token: string; - #bard_token_2: string; //__Secure-1PSIDCC - #bard_tokne_3: string; //__Secure-1PSIDTS - #reqid: number; - #snim0e: string; - #conversationID: string | null; - #responseID: string | null; - #choiceId: string | null; - #fSid: string; - #bl: string; - - constructor(bard_token: string, bard_token_2: string, bard_token_3: string) { - this.#bard_token = bard_token; - this.#bard_token_2 = bard_token_2; - this.#bard_tokne_3 = bard_token_3; - this.#reqid = Math.round(Math.random() * 9999); - this.#conversationID = ""; - this.#responseID = ""; - this.#choiceId = ""; - } - - async deleteConversation(conversationID: string) { - const input_text_struct = [ - [["GzXR5e", `["${conversationID}", 10]`, null, "generic"]] - ]; - const data = { - "f.req": JSON.stringify(input_text_struct), - "at": this.#snim0e - } - - await this.makeRequest("post", "https://bard.google.com/_/BardChatUi/data/batchexecute?", "GzXR5e", data, "/chat"); - } - - async getConversation(conversationID: string) { - const input_text_struct = [ - [["hNvQHb", `["${conversationID}", 10]`, null, "generic"]] - ]; - - const data = { - "f.req": JSON.stringify(input_text_struct), - "at": this.#snim0e, - }; - - let jsons = await this.makeRequest("post", "https://bard.google.com/_/BardChatUi/data/batchexecute?", "hNvQHb", data, "/chat/" + conversationID) - var result = new Array(); - - - jsons[0].forEach((element: any[][]) => { - let userMessage = element[2][0][0]; - let botResponse = "Bot response" - let responseId = ""; - let choiceId = "" - - for (let index = 0; index < element[3][0].length; index++) { - if (element[3][0][index][0] == element[3][3]) { - botResponse = element[3][0][index][1][0] - choiceId = element[3][3]; - } - } - responseId = element[0][1]; - result.unshift({ "UserMessage": userMessage, "BotResponse": botResponse, "responseID": responseId, "choiceId": choiceId }); - }); - - return result; - } - - async getConversations() { - const input_text_struct = [ - [["MaZiqc", "[13,null,[0]]", null, "generic"]] - ]; - - const data = { - "f.req": JSON.stringify(input_text_struct), - "at": this.#snim0e - } - - let jsons = await this.makeRequest("post", "https://bard.google.com/_/BardChatUi/data/batchexecute?", "MaZiqc", data, "/chat"); - - return jsons[0]; - } - - async getResponse(query: string): Promise { - const input_text_struct = [ - [query, 0, null, [], null, null, 0], - null, - [this.#conversationID, this.#responseID, this.#choiceId] /*, null, null, []], - null, null, null, [1], 0, [], [], 1, 0,*/ - ] - - const data = { - "f.req": JSON.stringify([null, JSON.stringify(input_text_struct)]), - "at": this.#snim0e, - } - - let jsons = await this.makeRequest("post", "https://bard.google.com/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate?", "", data, ""); - - - this.#conversationID = jsons[1][0] - this.#responseID = jsons[1][1] - this.#choiceId = jsons[4][0][0] - - return jsons[4][0][1][0] - } - - async makeRequest(method: string, url: string, rpcids: string, data: any, path: string) { - let params = { - "_reqid": this.#reqid, - "bl": this.#bl, - "f.sid": this.#fSid, - "source-path": path, - "rt": "c", - "rpcids": rpcids - } - const requestParams: RequestUrlParam = { - url: url + querystring.stringify(params), - method: method, - throw: true, - body: new URLSearchParams(data).toString(), - contentType: 'application/x-www-form-urlencoded;charset=UTF-8', - headers: { - "Cookie": this.getCookies() - } - } - - - - let resp = await request(requestParams); - - this.#reqid += 100000; - - let lines = resp.split('\n').filter(line => line.startsWith('[["wrb.fr')); - return lines.map(line => JSON.parse(JSON.parse(line)[0][2]))[0]; - } - - getCookies() { - let cookies = `__Secure-1PSID=${this.#bard_token}` - if (this.#bard_token_2 != undefined && this.#bard_token_2 != "") { cookies += `;__Secure-1PSIDCC=${this.#bard_token_2}` } - if (this.#bard_tokne_3 != undefined && this.#bard_tokne_3 != "") { cookies += `;__Secure-1PSIDTS=${this.#bard_tokne_3}` } - - return cookies; - } - - async getAuthentication() { - if (this.#bard_token == null || this.#bard_token.charAt(this.#bard_token.length - 1) != ".") { - throw new Error("__Secure-Ps1d token is either missing or incomplete"); - } - let resp = await request({ - method: "get", - url: "https://bard.google.com/", - headers: { - "Cookie": this.getCookies() - } - }) - - - // get the at token - var regex = /"SNlM0e":"(.*?)"/; - const sn1m0e = resp.match(regex)?.[1]; - - // get the fsid token - regex = /"FdrFJe":"(.*?)"/; - const fsid = resp.match(regex)?.[1]; - - // get the bl value - regex = /"cfb2h":"(.*?)"/; - const bl = resp.match(regex)?.[1]; - - if (sn1m0e == null) { - throw new Error("Unable to get the snim0e token"); - } else if (fsid == null) { - throw new Error("Unable to get the f.sid token"); - } else if (bl == null) { - throw new Error("Unable to get the bl token"); - } else { - this.#snim0e = sn1m0e; - this.#fSid = fsid; - this.#bl = bl; - } - } - - static async getBard(bard_token: string, bard_token_2: string, bard_token_3: string) { - let bard = new Bard(bard_token, bard_token_2, bard_token_3); - try { - await bard.getAuthentication(); - return bard; - } catch (error) { - console.error(error); - new Notice(error); - } - } - - setConversationId(conversationID: string | null) { - this.#conversationID = conversationID; - } - setResponseId(responseID: string | null) { - this.#responseID = responseID; - } - setChoiceId(choiceID: string | null) { - this.#choiceId = choiceID; - } -} \ No newline at end of file diff --git a/components/BardResponse.ts b/components/BardResponse.ts deleted file mode 100644 index c56a97c..0000000 --- a/components/BardResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -export class BardResponse { - text: string; - conversationID:string; - responseID:string; - choiceID:string; -} \ No newline at end of file diff --git a/components/ChatModal.ts b/components/ChatModal.ts deleted file mode 100644 index e3043b3..0000000 --- a/components/ChatModal.ts +++ /dev/null @@ -1,260 +0,0 @@ -import BardPlugin from "main"; -import { App, Modal, Notice, MarkdownRenderer } from "obsidian"; -import { send } from "process"; -import { Bard } from "./BardConnection"; -import { type } from "os"; -import { DebugBard } from "./DebugBard"; -import { Console } from "console"; -import { createHash } from "crypto"; - -export class ChatModal extends Modal { - #plugin: BardPlugin; - #chatMessages: HTMLElement; - #userInput: HTMLInputElement; - #Bard: Bard; - - #Chat: HTMLElement; - #Conversations: HTMLElement; - - - constructor(app: App, plugin: BardPlugin) { - super(app); - this.#plugin = plugin; - if (!this.#plugin.settings.DeveloperMode) { - Bard.getBard(plugin.settings.Bard_Token, plugin.settings.Bard_Token_2, plugin.settings.Bard_Token_3).then((result) => { - if (result) { - this.#Bard = result; - } - else { - new Notice("Something went wrong when trying to create a Bard connection"); - } - }); - } else { - console.log("BARD running in Dev mode"); - DebugBard.getBard(plugin.settings.Bard_Token, plugin.settings.Bard_Token_2, plugin.settings.Bard_Token_3). - then(value => this.#Bard = value); - } - } - - onOpen() { - let { contentEl } = this; - - contentEl.createEl('h3', { text: 'Chat with Bart AI' }); - let root = contentEl.createDiv("chat-modal-content"); - this.#Chat = root.createEl("div", { cls: "chat-interface panel" }); - - this.#chatMessages = this.#Chat.createDiv('chat-messages'); - - const conversationsButton = this.#Chat.createEl('button', { - text: 'Conversations', - cls: 'conversation-button' - }); - - let chatInput = this.#Chat.createDiv('chat-input'); - this.#userInput = chatInput.createEl('input', { type: 'text', placeholder: 'Type your message...' }); - chatInput.createEl('button', { - text: 'Send' - }).onClickEvent(() => { - this.sendMessage(); - }); - - this.#userInput.addEventListener('keydown', (event) => { - if (event.key === 'Enter') { - this.sendMessage(); - - // Prevents default behavior (newline) when pressing Enter - event.preventDefault(); - } - }); - - this.#Conversations = root.createDiv({ - cls: 'conversation-list panel hidden' // Initially hidden - }); - - - conversationsButton.onClickEvent(() => { - this.showConversations(); - this.loadConversations(); - }); - } - - showConversations() { - this.#Chat.classList.add("fadeout") - this.#Conversations.classList.add("fadein") - - this.#Chat.style.pointerEvents = 'none'; - this.#Conversations.style.pointerEvents = "none" - - this.#Chat.toggleClass("hidden", false); - this.#Conversations.toggleClass("hidden", false); - - this.#Conversations.style.overflowY = "auto"; - - this.#Chat.addEventListener('animationend', () => { - this.#Conversations.style.pointerEvents = "auto"; - this.#Chat.style.pointerEvents = "none" - - this.#Conversations.removeClass("fadein"); - this.#Chat.removeClass("fadeout"); - - this.#Conversations.toggleClass("hidden", false); - this.#Chat.toggleClass("hidden", true); - - }, { once: true }); - } - - showChat() { - this.#Chat.addClass("fadein"); - this.#Conversations.addClass("fadeout"); - - this.#Chat.style.pointerEvents = "none"; - this.#Conversations.style.pointerEvents = "none" - - this.#Chat.toggleClass("hidden", false); - this.#Conversations.toggleClass("hidden", false); - - // disable vertical scroll to prevent jittering when changin - this.#Conversations.style.overflowY = "hidden"; - - this.#Conversations.addEventListener('animationend', () => { - this.#Chat.style.pointerEvents = "auto"; - this.#Conversations.style.pointerEvents = "none"; - - this.#Conversations.removeClass("fadeout"); - this.#Chat.removeClass("fadein"); - - this.#Conversations.toggleClass("hidden", true); - this.#Chat.toggleClass("hidden", false); - }, { once: true }); - - } - - clearChat() { - while (this.#chatMessages.lastChild) { - this.#chatMessages.lastChild.remove(); - } - this.#Bard.setConversationId(null); - this.#Bard.setResponseId(null); - this.#Bard.setChoiceId(null); - } - - switchToConversation(conversationId: string) { - this.clearChat(); - this.#Bard.setConversationId(conversationId); - this.#Bard.getConversation(conversationId).then((result) => { - result.forEach((element) => { - this.displayMessage(element["UserMessage"], true); - this.displayMessage(element["BotResponse"], false) - }) - this.#Bard.setResponseId(result[result.length - 1]["responseID"]) - this.#Bard.setChoiceId(result[result.length - 1]["choiceId"]) - }); - } - - loadConversations() { - if (!this.#Bard) { - new Notice("Bard not ready"); - return; - } - - while (this.#Conversations.lastChild) { - this.#Conversations.lastChild.remove(); - } - - this.#Bard.getConversations().then((result) => { - //add the new chat button - const newChatButton = this.#Conversations.createEl("div", { - cls: "newChatButton", - text: "New Chat" - }) - newChatButton.onClickEvent((event) => { - event.stopPropagation(); - this.showChat(); - this.clearChat(); - }) - - //add all the conversations - result.forEach((conversation: string[]) => { - const listItem = this.#Conversations.createEl('div', { - cls: 'conversation-list-item' - }); - - const label = listItem.createEl('span', { - text: conversation[1], - }); - - // Create the delete button and initially hide it - const deleteButton = listItem.createEl('button', { - text: 'Delete', - cls: 'delete-button' // Add the 'conversation-button' class for styling - }); - deleteButton.style.display = 'none'; // Hide button by default - - deleteButton.onClickEvent((event) => { - event.stopPropagation(); - deleteButton.parentElement?.remove(); - this.#Bard.deleteConversation(conversation[0]); - }) - - listItem.dataset.conversationId = conversation[0]; - - // Toggle delete button visibility on hover - listItem.onmouseenter = () => deleteButton.style.display = 'block'; - listItem.onmouseleave = () => deleteButton.style.display = 'none'; - - // Clicking a conversation switches to it - listItem.onClickEvent(() => { - if (listItem.dataset.conversationId != undefined) { - this.switchToConversation(listItem.dataset.conversationId); - this.showChat(); - } - }) - }); - }); - } - - sendMessage() { - let userMessage = this.#userInput.value; - - if (userMessage == null || userMessage == "") return; - - this.displayMessage(userMessage, true); - - this.#userInput.value = ''; - - const botResponseDiv = this.#chatMessages.createEl('div', { cls: 'message bot-message' }); - botResponseDiv.append(this.typingAnimation()); - - this.#Bard.getResponse(userMessage).then((response) => { - botResponseDiv.remove(); - this.displayMessage(response, false); - }); - } - - displayMessage(text: string, isUser: boolean) { - let cls = "bot-message"; - if (isUser) { - cls = "user-message"; - } - - const messageDiv = this.#chatMessages.createEl("div", { cls: "message " + cls }); - MarkdownRenderer.render(this.app, text, messageDiv, "", this.#plugin); - messageDiv.scrollIntoView({ behavior: "smooth" }); - } - - typingAnimation() { - const typingAnim = createDiv({ cls: "typing-indicator" }) - typingAnim.appendChild(createSpan()) - typingAnim.appendChild(createSpan()) - typingAnim.appendChild(createSpan()) - - return typingAnim; - } - - onClose() { - let { contentEl } = this; - contentEl.empty(); // Clear modal content - } - - -} diff --git a/components/DebugBard.ts b/components/DebugBard.ts deleted file mode 100644 index 02233fd..0000000 --- a/components/DebugBard.ts +++ /dev/null @@ -1,172 +0,0 @@ -import { Console } from "console"; -import { Bard } from "./BardConnection"; -import { url } from "inspector"; -import { Notice, RequestUrlParam, request, requestUrl } from "obsidian"; -import { join } from "path"; -import * as querystring from "querystring"; -import { json } from "stream/consumers"; - - -export class DebugBard extends Bard { - #bard_token: string; - #bard_token_2: string; //__Secure-1PSIDCC - #bard_tokne_3: string; //__Secure-1PSIDTS - #reqid: number; - #snim0e: string; - #conversationID: string; - #responseID: string; - #choiceId: string; - #fSid: string; - #bl: string; - - constructor(bard_token: string, bard_token_2: string, bard_token_3: string) { - super(bard_token, bard_token_2, bard_token_3); - this.#bard_token = bard_token; - this.#bard_token_2 = bard_token_2; - this.#bard_tokne_3 = bard_token_3; - this.#reqid = Math.round(Math.random() * 9999); - this.#conversationID = ""; - this.#responseID = ""; - this.#choiceId = ""; - } - - async deleteConversation(conversationID: string) { - console.log("DEBUG BARD: Deleting conversation " + conversationID) - } - - async getConversation(conversationID: string) { - - let result = [{ - "UserMessage": "Please remember the code word lavender", - "BotResponse": "I will remember the code word \"lavender.\"", - "responseID": "r_7fd58f4c34784641", - "choiceId": "rc_1de3369c0219bdf3" - }, - { - "UserMessage": "what is the code word?", - "BotResponse": "The code word is \"lavender.\"", - "responseID": "r_4f9d38a217087817", - "choiceId": "rc_361bc099016b4525" - }] - - return result; - } - - async getConversations() { - let jsons = [[ - ['c_e079d823ac905a97', 'Forterro | abas: ERP Software for Mid-Sized Manufacturing', false, false, '', [1699453998, 750035000]], - ['c_9fe162a28ea02c64', 'Remembering code word "lavender"', false, false, '', [1699356005, 15547000]], - ['c_43a86bec0da1ee00', 'Code word table request', false, false, '', [1699355935, 93132000]], - ['c_83efa0bb126b621c', 'Remember code word', false, false, '', [1699355894, 787354000]], - ['c_e466dfc9cf1ffe23', 'Remembering code words', false, false, '', [1699355562, 86529000]], - ['c_7bd5c81834a618e5', 'Introducing the code word "char"', false, false, '', [1699355388, 906106000]], - ['c_c4be49b893dff725', 'Assistant Is Working', false, false, '', [1699354436, 878028000]], - ['c_0bb40a99b570e07d', 'Remember code word', false, false, '', [1699354419, 578969000]], - ['c_7815b5d52918ecff', 'Remembering the word "rose"', false, false, '', [1699354295, 549268000]], - ['c_dee3dbc77a070d31', 'Assistant ready to assist', false, false, '', [1699351414, 504864000]], - ['c_29a9a2d67fef675e', 'Remembering the code word "tulip"', false, false, '', [1699346862, 940578000]], - ['c_466fd4891b989e4e', 'Greeting', false, false, '', [1699306111, 95254000]] - ], 'tCqkBAblK0n83Blerc2Z2zIPwT+RH5VXXEUJ8c5Is+5l+Bw0/m…dErKkyliAliaco7s7VgO7nDARfvmGg943JyQfHzycfKWVJQ=='] - - return jsons[0]; - } - - async getResponse(query: string): Promise { - - console.log("DEBUG BARD: Responding to \"" + query + "\""); - - return "DEBUG RESPONSE"; - } - - async makeRequest(method: string, url: string, rpcids: string, data: any, path: string) { - let params = { - "_reqid": this.#reqid, - "bl": this.#bl, - "f.sid": this.#fSid, - "source-path": path, - "rt": "c", - "rpcids": rpcids - } - const requestParams: RequestUrlParam = { - url: url + querystring.stringify(params), - method: method, - throw: true, - body: new URLSearchParams(data).toString(), - contentType: 'application/x-www-form-urlencoded;charset=UTF-8', - headers: { - "Cookie": this.getCookies() - } - } - - - - let resp = await request(requestParams); - - this.#reqid += 100000; - - let lines = resp.split('\n').filter(line => line.startsWith('[["wrb.fr')); - console.log(lines.map(line => JSON.parse(JSON.parse(line)[0][2]))[0]); - return lines.map(line => JSON.parse(JSON.parse(line)[0][2]))[0]; - } - - getCookies() { - let cookies = `__Secure-1PSID=${this.#bard_token}` - if (this.#bard_token_2 != undefined && this.#bard_token_2 != "") { cookies += `;__Secure-1PSIDCC=${this.#bard_token_2}` } - if (this.#bard_tokne_3 != undefined && this.#bard_tokne_3 != "") { cookies += `;__Secure-1PSIDTS=${this.#bard_tokne_3}` } - - return cookies; - } - - async getAuthentication() { - if (this.#bard_token == null || this.#bard_token.charAt(this.#bard_token.length - 1) != ".") { - throw new Error("__Secure-Ps1d token is either missing or incomplete"); - } - let resp = await request({ - method: "get", - url: "https://bard.google.com/", - headers: { - "Cookie": this.getCookies() - } - }) - - - // get the at token - var regex = /"SNlM0e":"(.*?)"/; - const sn1m0e = resp.match(regex)?.[1]; - - // get the fsid token - regex = /"FdrFJe":"(.*?)"/; - const fsid = resp.match(regex)?.[1]; - - // get the bl value - regex = /"cfb2h":"(.*?)"/; - const bl = resp.match(regex)?.[1]; - - if (sn1m0e == null) { - throw new Error("Unable to get the snim0e token"); - } else if (fsid == null) { - throw new Error("Unable to get the f.sid token"); - } else if (bl == null) { - throw new Error("Unable to get the bl token"); - } else { - this.#snim0e = sn1m0e; - this.#fSid = fsid; - this.#bl = bl; - } - } - - static async getBard(bard_token: string, bard_token_2: string, bard_token_3: string) { - let bard = new DebugBard(bard_token, bard_token_2, bard_token_3); - return bard; - } - - setConversationId(conversationID: string) { - this.#conversationID = conversationID; - } - setResponseId(responseID: string) { - this.#responseID = responseID; - } - setChoiceId(choiceID: string) { - this.#choiceId = choiceID; - } -} \ No newline at end of file diff --git a/components/InsertTextModal.ts b/components/InsertTextModal.ts deleted file mode 100644 index ce81aa5..0000000 --- a/components/InsertTextModal.ts +++ /dev/null @@ -1,107 +0,0 @@ -import BardPlugin from "main"; -import { App, Modal, Notice, MarkdownRenderer, Plugin, Editor, EditorPosition } from "obsidian"; -import { send } from "process"; -import { Bard } from "./BardConnection"; -import { type } from "os"; -import { DebugBard } from "./DebugBard"; -import { Console } from "console"; -import { createHash } from "crypto"; -import { CursorPos } from "readline"; -import { text } from "stream/consumers"; - -export class InsertTextModal extends Modal { - #plugin: BardPlugin; - #editor: Editor; - #Bard: Bard; - #replacingSelection: boolean; - #cursor: EditorPosition; - #queryPanel: HTMLElement; - - constructor(app: App, plugin: BardPlugin, editor: Editor) { - super(app); - this.#plugin = plugin; - this.#editor = editor; - if (!this.#plugin.settings.DeveloperMode) { - Bard.getBard(plugin.settings.Bard_Token, plugin.settings.Bard_Token_2, plugin.settings.Bard_Token_3).then((result) => { - if (result) { - this.#Bard = result; - } - else { - new Notice("Something went wrong when trying to create a Bard connection"); - } - }); - } else { - console.log("BARD running in Dev mode"); - DebugBard.getBard(plugin.settings.Bard_Token, plugin.settings.Bard_Token_2, plugin.settings.Bard_Token_3). - then(value => this.#Bard = value); - } - - if (editor.getSelection() == null) { - this.#replacingSelection = false; - this.#cursor = editor.getCursor(); - } else { - this.#replacingSelection = true; - } - } - - onOpen(): void { - let { contentEl } = this; - - this.titleEl.textContent = "Insert text with Bard"; - - this.#queryPanel = contentEl.createEl("div", { cls: "insertTextModalContent" }); - let QueryInput = this.#queryPanel.createEl("textarea", { type: "text", placeholder: "Type your input...", cls: "insertTextModalInput" }); - let QueryButton = this.#queryPanel.createEl("button", { text: "Generate Text" }) - - QueryButton.onClickEvent((event) => { - this.GenerateResponse(QueryInput.value) - }) - - this.#queryPanel.addEventListener("keydown", (event) => { - if (event.key == "Enter") { - this.GenerateResponse(QueryInput.value); - } - }) - } - - GenerateResponse(query: string) { - if (query == "" || query == undefined) { - return; - } - this.#queryPanel.style.display = "none"; - - //create response elements - let { contentEl } = this; - let responseContainer = contentEl.createEl("div", { cls: "insertTextModalResponse" }); - - const botResponseDiv = responseContainer.createEl('div', { cls: 'message bot-message' }); - botResponseDiv.append(this.typingAnimation()); - - this.#Bard.getResponse(query).then((response) => { - botResponseDiv.remove(); - responseContainer.createEl("div", { text: response }) - }) - let keepButton = responseContainer.createEl("button", { text: "confirm" }) - keepButton.onClickEvent(() => { this.insert(responseContainer.getText()) }) - - } - - insert(text: string) { - if (this.#replacingSelection) { - this.#editor.replaceSelection(text); - } else { - this.#editor.replaceRange(text, this.#cursor); - } - this.close(); - } - - typingAnimation() { - const typingAnim = createDiv({ cls: "typing-indicator" }) - typingAnim.appendChild(createSpan()) - typingAnim.appendChild(createSpan()) - typingAnim.appendChild(createSpan()) - - return typingAnim; - } - -} \ No newline at end of file diff --git a/main.ts b/main.ts index 30d4fe2..6910fda 100644 --- a/main.ts +++ b/main.ts @@ -1,66 +1,123 @@ -import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian'; -import { ChatModal } from "components/ChatModal"; -import { Bard } from 'components/BardConnection'; -import { InsertTextModal } from 'components/InsertTextModal'; +import { App, Editor, FileView, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting, TFile, Vault, View, ViewState, WorkspaceLeaf, addIcon, loadMermaid } from 'obsidian'; +import { OpenChatModal } from 'src/Modals/OpenChatModal'; +import { GeminiChatView, VIEW_TYPE_GEMINI_CHAT } from 'src/Views/GeminiChatView'; -interface TalkToBardSettings { - Bard_Token: string; - Bard_Token_2: string; - Bard_Token_3: string; +interface GeminiPluginSettings { + Gemini_Api_Key: string; DeveloperMode: boolean; + DefaultSavePath: string; } -const DEFAULT_SETTINGS: TalkToBardSettings = { - Bard_Token: '', - Bard_Token_2: "", - Bard_Token_3: "", - DeveloperMode: false +const DEFAULT_SETTINGS: GeminiPluginSettings = { + Gemini_Api_Key: "", + DeveloperMode: false, + DefaultSavePath: "Gemini Chats" } -export default class BardPlugin extends Plugin { - settings: TalkToBardSettings; +export default class GeminiPlugin extends Plugin { + settings: GeminiPluginSettings; async onload() { await this.loadSettings(); - this.addSettingTab(new SettingsTab(this.app, this)); - this.addCommand({ - id: "open-chat", - name: "Chat with Bard", - callback: () => { - new ChatModal(this.app, this).open(); + this.registerView( + VIEW_TYPE_GEMINI_CHAT, + (leaf) => { + return new GeminiChatView(leaf, this.app, this); } + ); + this.registerExtensions(["gemini"], VIEW_TYPE_GEMINI_CHAT); + this.registerEvent(this.app.workspace.on("file-open", (file) => { + if (file?.extension == "gemini") { + this.activateChatView(file); + } + })) + this.addRibbonIcon("sparkles", "New Gemini Chat", () => { this.newChatView() }); + this.addCommand({ + id: 'gemini-new-chat', + name: 'New Gemini Chat', + callback: () => { this.newChatView() }, }) this.addCommand({ - id: "bard-insert", - name: "Bard Insert", - editorCallback: (editor: Editor) => { - new InsertTextModal(this.app, this, editor).open(); + id: 'gemini-open-chat', + name: 'Open Gemini Chat', + callback: () => { + new OpenChatModal(this.app, (file: TFile) => { + this.app.workspace.getLeaf(false).openFile(file); + }).open() } }) - this.addCommand({ - id: "debug", - name: "Testtttt", - editorCallback: (editor: Editor) => { - console.log(editor.getSelection()); - } - }) + } + + async onunload() { + this.app.workspace.detachLeavesOfType(VIEW_TYPE_GEMINI_CHAT); } async loadSettings() { this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); } + async saveSettings() { await this.saveData(this.settings); } -} -class SettingsTab extends PluginSettingTab { - plugin: BardPlugin; - constructor(app: App, plugin: BardPlugin) { + async activateChatView(file: TFile) { + const { workspace } = this.app; + + let leaf: WorkspaceLeaf | null = null; + const leaves = workspace.getLeavesOfType(VIEW_TYPE_GEMINI_CHAT).filter((leaf) => { + if (leaf.view instanceof FileView) { + const view = leaf.view as FileView; + + if (view.file == file) { + return true; + } + } + return false; + }); + + if (leaves.length > 0) { + leaf = leaves[0]; + } else { + leaf = workspace.getLeaf(false); + await leaf.setViewState({ type: VIEW_TYPE_GEMINI_CHAT, active: true }); + } + + if (leaf) { + workspace.setActiveLeaf(leaf); + } + } + + async newChatView() { + let path = `${this.settings.DefaultSavePath}/`; + if(this.settings.DefaultSavePath == "") path = ""; + + let name = "New Chat.gemini" + + let index = 0; + while (this.app.vault.getAbstractFileByPath(path + name) != null) { + index += 1; + name = `New Chat ${index}.gemini`; + + if(index >= 100){ + // Exit condition to avoid infinite loop + new Notice("Failed to create a new chat"); + return; + } + } + + let file = await this.app.vault.create(path + name, ""); + await this.app.workspace.getLeaf(false).openFile(file); + } +} + +class SettingsTab extends PluginSettingTab { + plugin: GeminiPlugin; + + constructor(app: App, plugin: GeminiPlugin) { super(app, plugin); this.plugin = plugin; } @@ -71,43 +128,37 @@ class SettingsTab extends PluginSettingTab { containerEl.empty(); new Setting(containerEl) - .setName('__Secure-1PSID') - .setDesc('Enter your __Secure-1PSID token here') + .setName("Gemini API Key") .addText(text => text - .setPlaceholder('Your token here') - .setValue(this.plugin.settings.Bard_Token) + .setPlaceholder("Your API key here") + .setValue(this.plugin.settings.Gemini_Api_Key) .onChange(async (value) => { - this.plugin.settings.Bard_Token = value; + this.plugin.settings.Gemini_Api_Key = value; await this.plugin.saveSettings(); - })); + })) - let optionalTokensHeader = containerEl.createEl("p", { text: "Enter these cookie values if the __Secure-1PSID token does not work" }); - new Setting(optionalTokensHeader) - .setName("__Secure-1PSIDCC") - .addText(text => text - .setPlaceholder("Your token here") - .setValue(this.plugin.settings.Bard_Token_2) - .onChange(async (value) => { - this.plugin.settings.Bard_Token_2 = value; - await this.plugin.saveSettings(); - })) - new Setting(optionalTokensHeader) - .setName("__Secure-1PSIDTS") - .addText(text => text - .setPlaceholder("Your token here") - .setValue(this.plugin.settings.Bard_Token_3) - .onChange(async (value) => { - this.plugin.settings.Bard_Token_3 = value; - await this.plugin.saveSettings(); - })) new Setting(containerEl) .setName("Developer mode") - .setDesc("Fakes the sending of requests to avoid error 429") + .setDesc("Fakes the sending of requests, leave this off unless you want fake answers for some reason...") .addToggle(value => value .setValue(this.plugin.settings.DeveloperMode) .onChange(async (value) => { this.plugin.settings.DeveloperMode = value; await this.plugin.saveSettings(); - })); + })) + + new Setting(containerEl) + .setName("Default File Path") + .setDesc("The default folder for saved Gemini Chats. Leave empty to have new chats appear at vault root.") + .addText(text => text + .setPlaceholder("Folder path") + .setValue(this.plugin.settings.DefaultSavePath) + .onChange(async (value) => { + this.plugin.settings.DefaultSavePath = value; + await this.plugin.saveSettings(); + }) + + ) + } } diff --git a/manifest.json b/manifest.json index 3a56e3d..8dd8a4b 100644 --- a/manifest.json +++ b/manifest.json @@ -1,9 +1,9 @@ { "id": "chat-with-bard", - "name": "Chat with Bard", + "name": "Gemini AI Assistant", "version": "0.2.0", "minAppVersion": "0.15.0", - "description": "Chat with the Google Bard Assistant directly from Obsidian", + "description": "Use Google Gemini directly in obsidian for free.", "author": "Artel250", "authorUrl": "https://github.com/Artel250", "isDesktopOnly": true diff --git a/package-lock.json b/package-lock.json index 273bef8..94c5383 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "obsidian-sample-plugin", "version": "1.0.0", "license": "MIT", + "dependencies": { + "@google/generative-ai": "^0.12.0" + }, "devDependencies": { "@types/node": "^16.11.6", "@typescript-eslint/eslint-plugin": "5.29.0", @@ -460,6 +463,14 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@google/generative-ai": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.12.0.tgz", + "integrity": "sha512-krWjurjEUHSFhCX4lGHMOhbnpBfYZGU31mpHpPBQwcfWm0T+/+wxC4UCAJfkxxc3/HvGJVG8r4AqrffaeDHDlA==", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.11", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", @@ -852,12 +863,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -1330,9 +1341,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" diff --git a/package.json b/package.json index 6a00766..acc0b25 100644 --- a/package.json +++ b/package.json @@ -20,5 +20,8 @@ "obsidian": "latest", "tslib": "2.4.0", "typescript": "4.7.4" + }, + "dependencies": { + "@google/generative-ai": "^0.12.0" } } diff --git a/src/Components/ChatComponent.ts b/src/Components/ChatComponent.ts new file mode 100644 index 0000000..8aa15cf --- /dev/null +++ b/src/Components/ChatComponent.ts @@ -0,0 +1,126 @@ +import { App, Plugin } from "obsidian"; +import { ChatMessageComponent } from "./ChatMessageComponent" +import { GeminiChat } from "../GeminiChat"; +import GeminiPlugin from "main"; + +type MessageListener = (message: string) => void; + +export class ChatComponent { + private chatInput: HTMLInputElement; + private chatDisplay: HTMLElement; + private parentElement: HTMLElement; + private messagesContainer: HTMLElement; + private sendButton: HTMLButtonElement; + + GeminiChat: GeminiChat; + private App: App; + private Plugin: GeminiPlugin; + newMessageListeners: MessageListener[] = []; + + constructor(parentElement: HTMLElement, App: App, Plugin: GeminiPlugin) { + this.parentElement = parentElement; + this.GeminiChat = new GeminiChat(Plugin, App); + this.App = App; + this.Plugin = Plugin; + this.initializeChatUI(); + } + + private initializeChatUI() { + + // Chat display within the container, taking up all available space + this.chatDisplay = this.parentElement.createDiv({ cls: 'gemini-chat-display' }); + + // Create a container for the chat messages + this.messagesContainer = this.chatDisplay.createDiv({ cls: 'messages-container' }); + + const inputContainer = this.chatDisplay.createDiv({ cls: 'input-container' }); + + this.chatInput = inputContainer.createEl('input', { + type: 'text', + cls: 'gemini-chat-input' + }); + this.chatInput.placeholder = 'Ask Gemini...'; + + // Send button + this.sendButton = inputContainer.createEl('button', { + text: 'Send', + cls: 'send-button' + }); + this.sendButton.addEventListener('click', () => this.handleChatInput()); + + this.chatInput.placeholder = 'Ask Gemini...'; + this.chatInput.addEventListener('keypress', (e: KeyboardEvent) => { + if (e.key === 'Enter') { + this.handleChatInput(); + } + }); + } + + public loadChat(GeminiChat: GeminiChat) { + this.GeminiChat = GeminiChat; + + //Load previous messages + GeminiChat.messages.forEach(element => { + this.displayMessage(element.text, element.user) + }); + } + + private async handleChatInput() { + const userInput = this.chatInput.value; + if (userInput == "") return; + this.displayMessage(userInput, true); + this.chatInput.value = ''; + this.showTypingAnimation(); + + const aiResponse = await this.GeminiChat.sendUserMessage(userInput); + + this.displayMessage(aiResponse.text, false); + this.hideTypingAnimation(); + } + + private showTypingAnimation() { + const typingIndicator = document.createElement('div'); + typingIndicator.className = "typing-indicator"; + + this.messagesContainer.prepend(typingIndicator) + + typingIndicator.createSpan({}); + typingIndicator.createSpan({}); + typingIndicator.createSpan({}); + } + + private hideTypingAnimation() { + const typingIndicator = this.chatDisplay.querySelector('.typing-indicator'); + if (typingIndicator) { + typingIndicator.remove(); + } + } + + private displayMessage(text: string, user: boolean) { + new ChatMessageComponent(text, user, this.messagesContainer, this.App, this.Plugin); + this.notifyMessageListeners(); + } + + private scrollToBottom() { + // Ensure the latest messages are visible + this.messagesContainer.scrollTop = this.messagesContainer.scrollHeight; + } + + public focusInput(){ + this.scrollToBottom(); + this.chatInput.focus(); + } + + addListener(listener: MessageListener) { + this.newMessageListeners.push(listener); + } + + private notifyMessageListeners() { + this.newMessageListeners.forEach((listener) => listener(this.GeminiChat.compileHistory())) + } + + reset(){ + this.messagesContainer.empty(); + } +} + diff --git a/src/Components/ChatMessageComponent.ts b/src/Components/ChatMessageComponent.ts new file mode 100644 index 0000000..236ae3e --- /dev/null +++ b/src/Components/ChatMessageComponent.ts @@ -0,0 +1,86 @@ +import { App, MarkdownRenderer, Plugin} from "obsidian"; + +export class ChatMessageComponent { + #text: string; + #UserMessage: boolean; + #ParentElement: HTMLElement; + #App: App; + #Plugin: Plugin; + + constructor(text: string, user: boolean, parent: HTMLElement, app:App, plugin:Plugin) { + this.#text = text; + this.#UserMessage = user; + this.#ParentElement = parent; + this.#App = app + this.#Plugin = plugin; + + // Create the message + const message = document.createElement("div"); + + // Create the button panel + const buttonPanel = document.createElement("div"); + buttonPanel.addClass("message-button-panel") + buttonPanel.style.opacity = "0"; + /* + buttonPanel.style.position = "absolute"; + buttonPanel.style.top = "0"; + buttonPanel.style.right = "0"; + buttonPanel.style.padding = "5px"; + buttonPanel.style.display = "flex"; + buttonPanel.style.gap = "5px"; + buttonPanel.style.opacity = "0"; + buttonPanel.style.transition = "opacity 0.3s ease-in-out"; + */ + + + // Create the copy button + const copyButton = document.createElement("button"); + copyButton.addClass("message-button") + copyButton.innerHTML = '📋'; + /* + copyButton.style.position = "relative"; + copyButton.style.zIndex = "10"; + */ + copyButton.addEventListener("click", () => { + navigator.clipboard.writeText(this.#text).then(() => { + copyButton.innerHTML = '✔️'; + setTimeout(() => { + copyButton.innerHTML = '📋'; + }, 3000); + }).catch(err => { + console.error("Failed to copy: ", err); + }); + }); + + buttonPanel.append(copyButton); + + // Create the content element + const content = document.createElement("div"); + MarkdownRenderer.render(this.#App, text, content, "", this.#Plugin); + + message.classList.add("chat-message"); + if (user) + message.classList.add("user-message"); + else + message.classList.add("ai-message"); + + // Style the message container + message.style.position = "relative"; + message.style.padding = "10px"; // Add padding to avoid overlap with the button + + // Append the button panel and content to the message + message.appendChild(buttonPanel); + message.appendChild(content); + + // Prepend the message to the parent element + parent.prepend(message); + + // Show the button panel on hover + message.addEventListener("mouseenter", () => { + buttonPanel.style.opacity = "1"; + }); + message.addEventListener("mouseleave", () => { + buttonPanel.style.opacity = "0"; + }); + } +} \ No newline at end of file diff --git a/src/GeminiChat.ts b/src/GeminiChat.ts new file mode 100644 index 0000000..4178a84 --- /dev/null +++ b/src/GeminiChat.ts @@ -0,0 +1,86 @@ +import { ChatSession, GenerativeModel, GoogleGenerativeAI } from '@google/generative-ai'; +import GeminiPlugin from 'main'; +import { App, TFile, Vault } from 'obsidian'; + +export class GeminiChat { + #API_KEY: string; + messages: ChatMessage[]; + #model: GenerativeModel; + app: App; + chatName: string + #session: ChatSession; + + constructor(plugin: GeminiPlugin, app: App) { + this.#API_KEY = plugin.settings.Gemini_Api_Key; + this.#model = new GoogleGenerativeAI(this.#API_KEY).getGenerativeModel({ model: "gemini-1.5-flash" }); + this.#session = this.#model.startChat() + this.app = app; + this.messages = []; + this.chatName = "Untitled Gemini Chat" + } + + async sendUserMessage(text: string) { + const userMessage = new ChatMessage(text, true); + this.messages.push(userMessage); + + // Send the message to the model and handle the response + const result = await this.#session.sendMessage(userMessage.text); + const modelMessage = new ChatMessage(result.response.text(), false); + this.messages.push(modelMessage); + return modelMessage; + } + + compileHistory() { + let history = this.messages.map(message => { + return { + role: message.user ? "user" : "model", + parts: message.text + }; + }); + + return JSON.stringify(this.messages); + } + + async saveHistory(chatName: String) { + const history = JSON.stringify(this.messages); + const filePath = `ChatHistory/${chatName}.gemini`; + await this.app.vault.adapter.write(filePath, history); + } + + async loadHistoryFromFile(file:TFile) { + try { + console.log(`Creating chat from: ${file}`) + let f = file; + if (f instanceof TFile) { + this.chatName = f.basename; + } else { + throw new Error(`Path Error for: ${file.path}`); + } + + let history = await this.app.vault.adapter.read(file.path); + + this.messages = JSON.parse(history).map((msg: { text: string; user: boolean }) => new ChatMessage(msg.text, msg.user)); + + let h = this.messages.map(message => { + return { + role: message.user ? "user" : "model", + parts: [{ text: message.text }] + }; + }); + this.#session = await this.#model.startChat({ history: h }); + } catch (error) { + console.log("No previous chat history found, starting fresh."); + this.messages = []; + } + } +} + +class ChatMessage { + text: string; + user: boolean; + + constructor(text: string, user: boolean) { + this.text = text; + this.user = user; + } +} \ No newline at end of file diff --git a/src/Modals/OpenChatModal.ts b/src/Modals/OpenChatModal.ts new file mode 100644 index 0000000..bb881c5 --- /dev/null +++ b/src/Modals/OpenChatModal.ts @@ -0,0 +1,17 @@ +import { App, FuzzySuggestModal, SuggestModal, TFile } from "obsidian"; + +export class OpenChatModal extends FuzzySuggestModal { + + constructor(app: App, protected onSelected: (file: TFile) => void) { + super(app); + } + getItems(): TFile[] { + return app.vault.getFiles().filter(file => file.extension == "gemini") + } + getItemText(item: TFile): string { + return item.path.replace(".gemini", ""); + } + onChooseItem(item: TFile, evt: MouseEvent | KeyboardEvent): void { + this.onSelected(item); + } +} \ No newline at end of file diff --git a/src/Views/GeminiChatView.ts b/src/Views/GeminiChatView.ts new file mode 100644 index 0000000..addfc09 --- /dev/null +++ b/src/Views/GeminiChatView.ts @@ -0,0 +1,112 @@ +import GeminiPlugin from 'main'; +import { TextFileView, WorkspaceLeaf, TFile, App, FileView, ItemView, MarkdownView, loadMermaid } from 'obsidian'; +import { ChatComponent } from '../Components/ChatComponent'; +import { GeminiChat } from '../GeminiChat'; +import { error } from 'console'; + +export const VIEW_TYPE_GEMINI_CHAT = 'gemini-chat-view'; + +export class GeminiChatView extends FileView { + + private chatComponent: ChatComponent; + private plugin: GeminiPlugin; + file: TFile | null; + private titleElement: HTMLTextAreaElement; + + + constructor(leaf: WorkspaceLeaf, app: App, plugin: GeminiPlugin) { + super(leaf); + this.app = app; + this.plugin = plugin; + this.file = null; + + this.contentEl.empty(); + + this.contentEl.style.display = "flex"; + this.contentEl.style.flexDirection = "column"; + + this.chatComponent = new ChatComponent(this.contentEl, app, plugin); + this.chatComponent.addListener(this.updateHistory.bind(this)) + + } + + private initialiseUI() { + if (!this.file) return; + + this.contentEl.empty(); + + this.contentEl.style.display = "flex"; + this.contentEl.style.flexDirection = "column"; + + // Initialise top bar + this.titleElement = this.contentEl.createEl("textarea", { + text: "New Chat", + cls: "chat-title-text" + }) + + this.titleElement.rows = 1; + this.titleElement.maxLength = 200; + this.titleElement.setText(this.file.basename); + + + this.titleElement.addEventListener("input", (event) => { + // Adjust the height of the textarea dynamically + this.titleElement.style.height = "auto"; + this.titleElement.style.height = (this.titleElement.scrollHeight) + "px"; + }); + this.titleElement.addEventListener("keydown", (ev) => { + if (ev.key == "Enter") { + this.titleElement.blur(); + + if (this.file && this.titleElement.textContent) { + let filepath = this.file?.path.replace(this.file.basename, this.titleElement.value); + console.log("renaming " + filepath); + this.app.vault.rename(this.file, filepath); + } + + } + }) + let topBar = this.contentEl.createDiv({ cls: "chat-top-bar" }); + + this.chatComponent = new ChatComponent(this.contentEl, app, this.plugin); + this.chatComponent.addListener(this.updateHistory.bind(this)) + } + + + updateHistory(history: string) { + if (this.file) { + this.app.vault.adapter.write(this.file.path, history); + } else { + throw error("Can't sync history, file is null"); + } + } + + async onLoadFile(file: TFile): Promise { + + this.chatComponent.reset(); + + this.file = file; + this.initialiseUI(); + + let chat = new GeminiChat(this.plugin, this.app); + await chat.loadHistoryFromFile(file) + + this.chatComponent.loadChat(chat); + + if (this.chatComponent.GeminiChat.messages.length == 0) { + + this.titleElement.focus() + this.titleElement.select() + }else{ + this.chatComponent.focusInput(); + } + } + + getViewType(): string { + return VIEW_TYPE_GEMINI_CHAT; + } + + canAcceptExtension(extension: string): boolean { + return extension == "gemini"; + } +} diff --git a/styles.css b/styles.css index c9e6386..ca86b6a 100644 --- a/styles.css +++ b/styles.css @@ -261,4 +261,185 @@ flex-wrap: wrap; justify-content: center; align-items: center; +} + +/*.modal-content { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; +} +*/ + +.parent-element { + display: flex; + flex-direction: column; + height: 100%; + /* This should be the height of the container in which the chat is placed */ +} + +.messages-container { + /* Container for chat messages */ + flex-grow: 1; + /* Takes up all available space */ + overflow-y: auto; + /* Only this part is scrollable */ + flex-direction: column-reverse; + display: flex; + padding: 20px; +} + +.gemini-chat-display { + flex-grow: 1; + display: flex; + flex-direction: column; + overflow-y: hidden; + padding-bottom: 10px; + /* Space at the bottom inside the chat display */ + height: 100%; +} + +.input-container { + display: flex; + justify-content: space-between; + padding: 5px; + box-sizing: border-box; +} + +.gemini-chat-input { + flex-grow: 1; + margin-right: 5px; + /* Spacing between input and button */ +} + +.send-button { + padding: 5px 10px; + cursor: pointer; +} + +/* The rest remains unchanged */ + + +.user-message { + /* Align user messages to the right */ + background-color: var(--background-secondary-alt); + align-self: flex-end; + justify-self: flex-start; +} + +.ai-message { + /* Align AI messages to the left */ + background-color: var(--background-secondary-alt); + align-self: flex-start; + justify-self: flex-start; +} + +.message-button-panel { + background: none; + border: none; + cursor: pointer; + padding: 0; + margin: 0; + outline: none; + display: inline-flex; + align-items: center; + justify-content: flex-end; + flex-direction: row; + display: flex; + width: auto; + height: auto; +} + +.message-button { + margin: 0; + height: fit-content; + padding: 0; + display: flex; + font-size: 10px; + border: none; + background: none; + outline: none; + background-color: transparent !important; + box-shadow: none !important; +} + +.message-button:hover { + background: none; + border: none; + outline: none; + background-color: transparent; + box-shadow: none; +} + +.chat-root { + overflow: hidden; + display: flex; + flex-direction: column; +} + +.chat-top-bar { + padding: 8px; + border-bottom: solid; + display: flex; + justify-content: space-between; + flex-direction: row; +} + +.chat-title-text { + font-size: xx-large; + width: 100%; + height: fit-content; + overflow: hidden; + background: none; + border: none; + outline: none; + border: none; + resize: none; +} +.chat-title-text:focus{ + box-shadow: none; +} + +.chat-message { + margin: 5px; + padding: 10px; + border-radius: 5px; + max-width: 70%; + word-wrap: break-word; + align-items: end; +} + +.typing-indicator { + align-self: flex-start; + display: flex; + justify-content: center; + margin: 5px; +} + +.typing-indicator span { + margin: 0 2px; + animation: blink 1s step-start infinite; +} + +@keyframes blink { + 50% { + opacity: 0; + } +} + +.newChatBase{ + flex-direction: column; + display: flex; + flex-wrap: nowrap; + height: 100%; + justify-content: flex-start; +} +.newChatButton{ + margin: 5%; + border: 0; +} +.newChatHeader{ + font-size: xx-large; + text-align: center; + margin: 10%; } \ No newline at end of file