mirror of
https://github.com/artel250/Obsidian-Gemini-Assistant.git
synced 2026-07-22 08:40:31 +00:00
Complete rework for the Gemini update
This commit is contained in:
parent
a8b2768167
commit
aac3d468be
15 changed files with 745 additions and 826 deletions
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
export class BardResponse {
|
||||
text: string;
|
||||
conversationID:string;
|
||||
responseID:string;
|
||||
choiceID:string;
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
177
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();
|
||||
})
|
||||
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
25
package-lock.json
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -20,5 +20,8 @@
|
|||
"obsidian": "latest",
|
||||
"tslib": "2.4.0",
|
||||
"typescript": "4.7.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google/generative-ai": "^0.12.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
126
src/Components/ChatComponent.ts
Normal file
126
src/Components/ChatComponent.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
|
||||
86
src/Components/ChatMessageComponent.ts
Normal file
86
src/Components/ChatMessageComponent.ts
Normal 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
86
src/GeminiChat.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
17
src/Modals/OpenChatModal.ts
Normal file
17
src/Modals/OpenChatModal.ts
Normal 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
112
src/Views/GeminiChatView.ts
Normal 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";
|
||||
}
|
||||
}
|
||||
181
styles.css
181
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%;
|
||||
}
|
||||
Loading…
Reference in a new issue