Complete rework for the Gemini update

This commit is contained in:
Your Name 2024-08-27 15:04:04 +02:00
parent a8b2768167
commit aac3d468be
15 changed files with 745 additions and 826 deletions

View file

@ -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<string> {
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;
}
}

View file

@ -1,6 +0,0 @@
export class BardResponse {
text: string;
conversationID:string;
responseID:string;
choiceID:string;
}

View file

@ -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
}
}

View file

@ -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<string> {
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;
}
}

View file

@ -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;
}
}

177
main.ts
View file

@ -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();
})
)
}
}

View file

@ -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

25
package-lock.json generated
View file

@ -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"

View file

@ -20,5 +20,8 @@
"obsidian": "latest",
"tslib": "2.4.0",
"typescript": "4.7.4"
},
"dependencies": {
"@google/generative-ai": "^0.12.0"
}
}

View file

@ -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();
}
}

View file

@ -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 = '<span class="clipboard-icon">📋</span>';
/*
copyButton.style.position = "relative";
copyButton.style.zIndex = "10";
*/
copyButton.addEventListener("click", () => {
navigator.clipboard.writeText(this.#text).then(() => {
copyButton.innerHTML = '<span class="checkmark-icon">✔️</span>';
setTimeout(() => {
copyButton.innerHTML = '<span class="clipboard-icon">📋</span>';
}, 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";
});
}
}

86
src/GeminiChat.ts Normal file
View file

@ -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;
}
}

View file

@ -0,0 +1,17 @@
import { App, FuzzySuggestModal, SuggestModal, TFile } from "obsidian";
export class OpenChatModal extends FuzzySuggestModal<TFile> {
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);
}
}

112
src/Views/GeminiChatView.ts Normal file
View file

@ -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<void> {
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";
}
}

View file

@ -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%;
}