1074 lines
37 KiB
JavaScript
1074 lines
37 KiB
JavaScript
/*
|
|
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
|
if you want to view the source, please visit the github repository of this plugin
|
|
*/
|
|
|
|
var __create = Object.create;
|
|
var __defProp = Object.defineProperty;
|
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
var __getProtoOf = Object.getPrototypeOf;
|
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
var __commonJS = (cb, mod) => function __require() {
|
|
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
};
|
|
var __export = (target, all) => {
|
|
for (var name in all)
|
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
};
|
|
var __copyProps = (to, from, except, desc) => {
|
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
for (let key of __getOwnPropNames(from))
|
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
}
|
|
return to;
|
|
};
|
|
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
// If the importer is in node compatibility mode or this is not an ESM
|
|
// file that has been converted to a CommonJS file using a Babel-
|
|
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
mod
|
|
));
|
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
|
|
// node_modules/sse/lib/sse.js
|
|
var require_sse = __commonJS({
|
|
"node_modules/sse/lib/sse.js"(exports) {
|
|
var SSE2 = function(url, options) {
|
|
if (!(this instanceof SSE2)) {
|
|
return new SSE2(url, options);
|
|
}
|
|
this.INITIALIZING = -1;
|
|
this.CONNECTING = 0;
|
|
this.OPEN = 1;
|
|
this.CLOSED = 2;
|
|
this.url = url;
|
|
options = options || {};
|
|
this.headers = options.headers || {};
|
|
this.payload = options.payload !== void 0 ? options.payload : "";
|
|
this.method = options.method || (this.payload && "POST" || "GET");
|
|
this.withCredentials = !!options.withCredentials;
|
|
this.FIELD_SEPARATOR = ":";
|
|
this.listeners = {};
|
|
this.xhr = null;
|
|
this.readyState = this.INITIALIZING;
|
|
this.progress = 0;
|
|
this.chunk = "";
|
|
this.addEventListener = function(type, listener) {
|
|
if (this.listeners[type] === void 0) {
|
|
this.listeners[type] = [];
|
|
}
|
|
if (this.listeners[type].indexOf(listener) === -1) {
|
|
this.listeners[type].push(listener);
|
|
}
|
|
};
|
|
this.removeEventListener = function(type, listener) {
|
|
if (this.listeners[type] === void 0) {
|
|
return;
|
|
}
|
|
var filtered = [];
|
|
this.listeners[type].forEach(function(element) {
|
|
if (element !== listener) {
|
|
filtered.push(element);
|
|
}
|
|
});
|
|
if (filtered.length === 0) {
|
|
delete this.listeners[type];
|
|
} else {
|
|
this.listeners[type] = filtered;
|
|
}
|
|
};
|
|
this.dispatchEvent = function(e) {
|
|
if (!e) {
|
|
return true;
|
|
}
|
|
e.source = this;
|
|
var onHandler = "on" + e.type;
|
|
if (this.hasOwnProperty(onHandler)) {
|
|
this[onHandler].call(this, e);
|
|
if (e.defaultPrevented) {
|
|
return false;
|
|
}
|
|
}
|
|
if (this.listeners[e.type]) {
|
|
return this.listeners[e.type].every(function(callback) {
|
|
callback(e);
|
|
return !e.defaultPrevented;
|
|
});
|
|
}
|
|
return true;
|
|
};
|
|
this._setReadyState = function(state) {
|
|
var event = new CustomEvent("readystatechange");
|
|
event.readyState = state;
|
|
this.readyState = state;
|
|
this.dispatchEvent(event);
|
|
};
|
|
this._onStreamFailure = function(e) {
|
|
var event = new CustomEvent("error");
|
|
event.data = e.currentTarget.response;
|
|
this.dispatchEvent(event);
|
|
this.close();
|
|
};
|
|
this._onStreamAbort = function(e) {
|
|
this.dispatchEvent(new CustomEvent("abort"));
|
|
this.close();
|
|
};
|
|
this._onStreamProgress = function(e) {
|
|
if (!this.xhr) {
|
|
return;
|
|
}
|
|
if (this.xhr.status !== 200) {
|
|
this._onStreamFailure(e);
|
|
return;
|
|
}
|
|
if (this.readyState == this.CONNECTING) {
|
|
this.dispatchEvent(new CustomEvent("open"));
|
|
this._setReadyState(this.OPEN);
|
|
}
|
|
var data = this.xhr.responseText.substring(this.progress);
|
|
this.progress += data.length;
|
|
data.split(/(\r\n|\r|\n){2}/g).forEach(function(part) {
|
|
if (part.trim().length === 0) {
|
|
this.dispatchEvent(this._parseEventChunk(this.chunk.trim()));
|
|
this.chunk = "";
|
|
} else {
|
|
this.chunk += part;
|
|
}
|
|
}.bind(this));
|
|
};
|
|
this._onStreamLoaded = function(e) {
|
|
this._onStreamProgress(e);
|
|
this.dispatchEvent(this._parseEventChunk(this.chunk));
|
|
this.chunk = "";
|
|
};
|
|
this._parseEventChunk = function(chunk) {
|
|
if (!chunk || chunk.length === 0) {
|
|
return null;
|
|
}
|
|
var e = { "id": null, "retry": null, "data": "", "event": "message" };
|
|
chunk.split(/\n|\r\n|\r/).forEach(function(line) {
|
|
line = line.trimRight();
|
|
var index = line.indexOf(this.FIELD_SEPARATOR);
|
|
if (index <= 0) {
|
|
return;
|
|
}
|
|
var field = line.substring(0, index);
|
|
if (!(field in e)) {
|
|
return;
|
|
}
|
|
var value = line.substring(index + 1).trimLeft();
|
|
if (field === "data") {
|
|
e[field] += value;
|
|
} else {
|
|
e[field] = value;
|
|
}
|
|
}.bind(this));
|
|
var event = new CustomEvent(e.event);
|
|
event.data = e.data;
|
|
event.id = e.id;
|
|
return event;
|
|
};
|
|
this._checkStreamClosed = function() {
|
|
if (!this.xhr) {
|
|
return;
|
|
}
|
|
if (this.xhr.readyState === XMLHttpRequest.DONE) {
|
|
this._setReadyState(this.CLOSED);
|
|
}
|
|
};
|
|
this.stream = function() {
|
|
this._setReadyState(this.CONNECTING);
|
|
this.xhr = new XMLHttpRequest();
|
|
this.xhr.addEventListener("progress", this._onStreamProgress.bind(this));
|
|
this.xhr.addEventListener("load", this._onStreamLoaded.bind(this));
|
|
this.xhr.addEventListener("readystatechange", this._checkStreamClosed.bind(this));
|
|
this.xhr.addEventListener("error", this._onStreamFailure.bind(this));
|
|
this.xhr.addEventListener("abort", this._onStreamAbort.bind(this));
|
|
this.xhr.open(this.method, this.url);
|
|
for (var header in this.headers) {
|
|
this.xhr.setRequestHeader(header, this.headers[header]);
|
|
}
|
|
this.xhr.withCredentials = this.withCredentials;
|
|
this.xhr.send(this.payload);
|
|
};
|
|
this.close = function() {
|
|
if (this.readyState === this.CLOSED) {
|
|
return;
|
|
}
|
|
this.xhr.abort();
|
|
this.xhr = null;
|
|
this._setReadyState(this.CLOSED);
|
|
};
|
|
};
|
|
if (typeof exports !== "undefined") {
|
|
exports.SSE = SSE2;
|
|
}
|
|
}
|
|
});
|
|
|
|
// main.ts
|
|
var main_exports = {};
|
|
__export(main_exports, {
|
|
ChatTemplates: () => ChatTemplates,
|
|
default: () => ChatGPT_MD
|
|
});
|
|
module.exports = __toCommonJS(main_exports);
|
|
var import_obsidian2 = require("obsidian");
|
|
|
|
// stream.ts
|
|
var import_obsidian = require("obsidian");
|
|
var import_sse = __toESM(require_sse());
|
|
|
|
// helpers.ts
|
|
var unfinishedCodeBlock = (txt) => {
|
|
const matcher = txt.match(/```/g);
|
|
if (!matcher) {
|
|
return false;
|
|
}
|
|
if (matcher.length % 2 !== 0)
|
|
console.log("[ChatGPT MD] unclosed code block detected");
|
|
return matcher.length % 2 !== 0;
|
|
};
|
|
|
|
// stream.ts
|
|
var StreamManager = class {
|
|
constructor() {
|
|
this.sse = null;
|
|
this.manualClose = false;
|
|
this.stopStreaming = () => {
|
|
if (import_obsidian.Platform.isMobile) {
|
|
new import_obsidian.Notice("[ChatGPT MD] Mobile not supported.");
|
|
return;
|
|
}
|
|
if (this.sse) {
|
|
this.manualClose = true;
|
|
this.sse.close();
|
|
console.log("[ChatGPT MD] SSE manually closed");
|
|
this.sse = null;
|
|
}
|
|
};
|
|
this.streamSSE = async (editor, apiKey, url, options, setAtCursor, headingPrefix) => {
|
|
return new Promise((resolve, reject) => {
|
|
try {
|
|
console.log("[ChatGPT MD] streamSSE", options);
|
|
const source = new import_sse.SSE(url, {
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: `Bearer ${apiKey}`
|
|
},
|
|
method: "POST",
|
|
payload: JSON.stringify(options)
|
|
});
|
|
this.sse = source;
|
|
let txt = "";
|
|
let initialCursorPosCh = editor.getCursor().ch;
|
|
let initialCursorPosLine = editor.getCursor().line;
|
|
source.addEventListener("open", (e) => {
|
|
console.log("[ChatGPT MD] SSE Opened");
|
|
const newLine = `
|
|
|
|
<hr class="__chatgpt_plugin">
|
|
|
|
${headingPrefix}role::assistant
|
|
|
|
`;
|
|
editor.replaceRange(newLine, editor.getCursor());
|
|
const cursor = editor.getCursor();
|
|
const newCursor = {
|
|
line: cursor.line,
|
|
ch: cursor.ch + newLine.length
|
|
};
|
|
editor.setCursor(newCursor);
|
|
initialCursorPosCh = newCursor.ch;
|
|
initialCursorPosLine = newCursor.line;
|
|
});
|
|
source.addEventListener("message", (e) => {
|
|
if (e.data != "[DONE]") {
|
|
const payload = JSON.parse(e.data);
|
|
const text = payload.choices[0].delta.content;
|
|
if (!text) {
|
|
return;
|
|
}
|
|
const cursor = editor.getCursor();
|
|
const convPos = editor.posToOffset(cursor);
|
|
const cm6 = editor.cm;
|
|
const transaction = cm6.state.update({
|
|
changes: {
|
|
from: convPos,
|
|
to: convPos,
|
|
insert: text
|
|
}
|
|
});
|
|
cm6.dispatch(transaction);
|
|
txt += text;
|
|
const newCursor = {
|
|
line: cursor.line,
|
|
ch: cursor.ch + text.length
|
|
};
|
|
editor.setCursor(newCursor);
|
|
} else {
|
|
source.close();
|
|
console.log("[ChatGPT MD] SSE Closed");
|
|
if (unfinishedCodeBlock(txt)) {
|
|
txt += "\n```";
|
|
}
|
|
const cursor = editor.getCursor();
|
|
editor.replaceRange(
|
|
txt,
|
|
{
|
|
line: initialCursorPosLine,
|
|
ch: initialCursorPosCh
|
|
},
|
|
cursor
|
|
);
|
|
const newCursor = {
|
|
line: initialCursorPosLine,
|
|
ch: initialCursorPosCh + txt.length
|
|
};
|
|
editor.setCursor(newCursor);
|
|
if (!setAtCursor) {
|
|
editor.replaceRange("", newCursor, {
|
|
line: Infinity,
|
|
ch: Infinity
|
|
});
|
|
} else {
|
|
new import_obsidian.Notice(
|
|
"[ChatGPT MD] Text pasted at cursor may leave artifacts. Please remove them manually. ChatGPT MD cannot safely remove text when pasting at cursor."
|
|
);
|
|
}
|
|
resolve(txt);
|
|
}
|
|
});
|
|
source.addEventListener("abort", (e) => {
|
|
console.log("[ChatGPT MD] SSE Closed Event");
|
|
if (this.manualClose) {
|
|
resolve(txt);
|
|
}
|
|
});
|
|
source.addEventListener("error", (e) => {
|
|
try {
|
|
console.log(
|
|
"[ChatGPT MD] SSE Error: ",
|
|
JSON.parse(e.data)
|
|
);
|
|
source.close();
|
|
console.log("[ChatGPT MD] SSE Closed");
|
|
reject(JSON.parse(e.data));
|
|
} catch (err) {
|
|
console.log("[ChatGPT MD] Unknown Error: ", e);
|
|
source.close();
|
|
console.log("[ChatGPT MD] SSE Closed");
|
|
reject(e);
|
|
}
|
|
});
|
|
source.stream();
|
|
} catch (err) {
|
|
console.log("SSE Error", err);
|
|
reject(err);
|
|
}
|
|
});
|
|
};
|
|
}
|
|
};
|
|
|
|
// main.ts
|
|
var DEFAULT_SETTINGS = {
|
|
apiKey: "default",
|
|
defaultChatFrontmatter: "---\nsystem_commands: ['I am a helpful assistant.']\ntemperature: 0\ntop_p: 1\nmax_tokens: 512\npresence_penalty: 1\nfrequency_penalty: 1\nstream: true\nstop: null\nn: 1\nmodel: gpt-3.5-turbo\n---",
|
|
stream: true,
|
|
chatTemplateFolder: "ChatGPT_MD/templates",
|
|
chatFolder: "ChatGPT_MD/chats",
|
|
generateAtCursor: false,
|
|
autoInferTitle: false,
|
|
dateFormat: "YYYYMMDDhhmmss",
|
|
headingLevel: 0
|
|
};
|
|
var DEFAULT_URL = `https://api.openai.com/v1/chat/completions`;
|
|
var ChatGPT_MD = class extends import_obsidian2.Plugin {
|
|
async callOpenAIAPI(streamManager, editor, messages, model = "gpt-3.5-turbo", max_tokens = 250, temperature = 0.3, top_p = 1, presence_penalty = 0.5, frequency_penalty = 0.5, stream = true, stop = null, n = 1, logit_bias = null, user = null, url = DEFAULT_URL) {
|
|
try {
|
|
console.log("calling openai api");
|
|
if (stream) {
|
|
const options = {
|
|
model,
|
|
messages,
|
|
max_tokens,
|
|
temperature,
|
|
top_p,
|
|
presence_penalty,
|
|
frequency_penalty,
|
|
stream,
|
|
stop,
|
|
n
|
|
// logit_bias: logit_bias, // not yet supported
|
|
// user: user, // not yet supported
|
|
};
|
|
const response = await streamManager.streamSSE(
|
|
editor,
|
|
this.settings.apiKey,
|
|
url,
|
|
options,
|
|
this.settings.generateAtCursor,
|
|
this.getHeadingPrefix()
|
|
);
|
|
console.log("response from stream", response);
|
|
return { fullstr: response, mode: "streaming" };
|
|
} else {
|
|
const responseUrl = await (0, import_obsidian2.requestUrl)({
|
|
url,
|
|
method: "POST",
|
|
headers: {
|
|
Authorization: `Bearer ${this.settings.apiKey}`,
|
|
"Content-Type": "application/json"
|
|
},
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
model,
|
|
messages,
|
|
max_tokens,
|
|
temperature,
|
|
top_p,
|
|
presence_penalty,
|
|
frequency_penalty,
|
|
stream,
|
|
stop,
|
|
n
|
|
// logit_bias: logit_bias, // not yet supported
|
|
// user: user, // not yet supported
|
|
}),
|
|
throw: false
|
|
});
|
|
try {
|
|
const json = responseUrl.json;
|
|
if (json && json.error) {
|
|
new import_obsidian2.Notice(
|
|
`[ChatGPT MD] Stream = False Error :: ${json.error.message}`
|
|
);
|
|
throw new Error(JSON.stringify(json.error));
|
|
}
|
|
} catch (err) {
|
|
if (err instanceof SyntaxError) {
|
|
} else {
|
|
throw new Error(err);
|
|
}
|
|
}
|
|
const response = responseUrl.text;
|
|
const responseJSON = JSON.parse(response);
|
|
return responseJSON.choices[0].message.content;
|
|
}
|
|
} catch (err) {
|
|
if (err instanceof Object) {
|
|
if (err.error) {
|
|
new import_obsidian2.Notice(`[ChatGPT MD] Error :: ${err.error.message}`);
|
|
throw new Error(JSON.stringify(err.error));
|
|
} else {
|
|
if (url !== DEFAULT_URL) {
|
|
new import_obsidian2.Notice("[ChatGPT MD] Issue calling specified url: " + url);
|
|
throw new Error("[ChatGPT MD] Issue calling specified url: " + url);
|
|
} else {
|
|
new import_obsidian2.Notice(`[ChatGPT MD] Error :: ${JSON.stringify(err)}`);
|
|
throw new Error(JSON.stringify(err));
|
|
}
|
|
}
|
|
}
|
|
new import_obsidian2.Notice(
|
|
"issue calling OpenAI API, see console for more details"
|
|
);
|
|
throw new Error(
|
|
"issue calling OpenAI API, see error for more details: " + err
|
|
);
|
|
}
|
|
}
|
|
addHR(editor, role) {
|
|
const newLine = `
|
|
|
|
<hr class="__chatgpt_plugin">
|
|
|
|
${this.getHeadingPrefix()}role::${role}
|
|
|
|
`;
|
|
editor.replaceRange(newLine, editor.getCursor());
|
|
const cursor = editor.getCursor();
|
|
const newCursor = {
|
|
line: cursor.line,
|
|
ch: cursor.ch + newLine.length
|
|
};
|
|
editor.setCursor(newCursor);
|
|
}
|
|
getFrontmatter(view) {
|
|
var _a;
|
|
try {
|
|
const noteFile = app.workspace.getActiveFile();
|
|
if (!noteFile) {
|
|
throw new Error("no active file");
|
|
}
|
|
const metaMatter = (_a = app.metadataCache.getFileCache(noteFile)) == null ? void 0 : _a.frontmatter;
|
|
const shouldStream = (metaMatter == null ? void 0 : metaMatter.stream) !== void 0 ? metaMatter.stream : this.settings.stream !== void 0 ? this.settings.stream : true;
|
|
const temperature = (metaMatter == null ? void 0 : metaMatter.temperature) !== void 0 ? metaMatter.temperature : 0.3;
|
|
const frontmatter = {
|
|
title: (metaMatter == null ? void 0 : metaMatter.title) || view.file.basename,
|
|
tags: (metaMatter == null ? void 0 : metaMatter.tags) || [],
|
|
model: (metaMatter == null ? void 0 : metaMatter.model) || "gpt-3.5-turbo",
|
|
temperature,
|
|
top_p: (metaMatter == null ? void 0 : metaMatter.top_p) || 1,
|
|
presence_penalty: (metaMatter == null ? void 0 : metaMatter.presence_penalty) || 0,
|
|
frequency_penalty: (metaMatter == null ? void 0 : metaMatter.frequency_penalty) || 0,
|
|
stream: shouldStream,
|
|
max_tokens: (metaMatter == null ? void 0 : metaMatter.max_tokens) || 512,
|
|
stop: (metaMatter == null ? void 0 : metaMatter.stop) || null,
|
|
n: (metaMatter == null ? void 0 : metaMatter.n) || 1,
|
|
logit_bias: (metaMatter == null ? void 0 : metaMatter.logit_bias) || null,
|
|
user: (metaMatter == null ? void 0 : metaMatter.user) || null,
|
|
system_commands: (metaMatter == null ? void 0 : metaMatter.system_commands) || null,
|
|
url: (metaMatter == null ? void 0 : metaMatter.url) || DEFAULT_URL
|
|
};
|
|
return frontmatter;
|
|
} catch (err) {
|
|
throw new Error("Error getting frontmatter");
|
|
}
|
|
}
|
|
splitMessages(text) {
|
|
try {
|
|
const messages = text.split('<hr class="__chatgpt_plugin">');
|
|
return messages;
|
|
} catch (err) {
|
|
throw new Error("Error splitting messages" + err);
|
|
}
|
|
}
|
|
moveCursorToEndOfFile(editor) {
|
|
try {
|
|
const length = editor.lastLine();
|
|
const newCursor = {
|
|
line: length + 1,
|
|
ch: 0
|
|
};
|
|
editor.setCursor(newCursor);
|
|
return newCursor;
|
|
} catch (err) {
|
|
throw new Error("Error moving cursor to end of file" + err);
|
|
}
|
|
}
|
|
removeYMLFromMessage(message) {
|
|
try {
|
|
const YAMLFrontMatter = /---\s*[\s\S]*?\s*---/g;
|
|
const newMessage = message.replace(YAMLFrontMatter, "");
|
|
return newMessage;
|
|
} catch (err) {
|
|
throw new Error("Error removing YML from message" + err);
|
|
}
|
|
}
|
|
extractRoleAndMessage(message) {
|
|
try {
|
|
if (message.includes("role::")) {
|
|
const role = message.split("role::")[1].split("\n")[0].trim();
|
|
const content = message.split("role::")[1].split("\n").slice(1).join("\n").trim();
|
|
return { role, content };
|
|
} else {
|
|
return { role: "user", content: message };
|
|
}
|
|
} catch (err) {
|
|
throw new Error("Error extracting role and message" + err);
|
|
}
|
|
}
|
|
getHeadingPrefix() {
|
|
const headingLevel = this.settings.headingLevel;
|
|
if (headingLevel === 0) {
|
|
return "";
|
|
} else if (headingLevel > 6) {
|
|
return "#".repeat(6) + " ";
|
|
}
|
|
return "#".repeat(headingLevel) + " ";
|
|
}
|
|
appendMessage(editor, role, message) {
|
|
const newLine = `
|
|
|
|
<hr class="__chatgpt_plugin">
|
|
|
|
${this.getHeadingPrefix()}role::${role}
|
|
|
|
${message}
|
|
|
|
<hr class="__chatgpt_plugin">
|
|
|
|
${this.getHeadingPrefix()}role::user
|
|
|
|
`;
|
|
editor.replaceRange(newLine, editor.getCursor());
|
|
}
|
|
async inferTitleFromMessages(messages) {
|
|
console.log("[ChtGPT MD] Inferring Title");
|
|
try {
|
|
if (messages.length < 2) {
|
|
new import_obsidian2.Notice(
|
|
"Not enough messages to infer title. Minimum 2 messages."
|
|
);
|
|
return;
|
|
}
|
|
const prompt = `Infer title from the summary of the content of these messages. The title **cannot** contain any of the following characters: colon, back slash or forwad slash. Just return the title.
|
|
Messages:
|
|
|
|
${JSON.stringify(
|
|
messages
|
|
)}`;
|
|
const titleMessage = [
|
|
{
|
|
role: "user",
|
|
content: prompt
|
|
}
|
|
];
|
|
if (import_obsidian2.Platform.isMobile) {
|
|
new import_obsidian2.Notice("[ChatGPT] Inferring title from messages...");
|
|
}
|
|
const responseUrl = await (0, import_obsidian2.requestUrl)({
|
|
url: `https://api.openai.com/v1/chat/completions`,
|
|
method: "POST",
|
|
headers: {
|
|
Authorization: `Bearer ${this.settings.apiKey}`,
|
|
"Content-Type": "application/json"
|
|
},
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
model: "gpt-3.5-turbo",
|
|
messages: titleMessage,
|
|
max_tokens: 50,
|
|
temperature: 0
|
|
}),
|
|
throw: false
|
|
});
|
|
const response = responseUrl.text;
|
|
const responseJSON = JSON.parse(response);
|
|
return responseJSON.choices[0].message.content.trim().replace(/[:/\\]/g, "");
|
|
} catch (err) {
|
|
new import_obsidian2.Notice("[ChatGPT MD] Error inferring title from messages");
|
|
throw new Error("[ChatGPT MD] Error inferring title from messages" + err);
|
|
}
|
|
}
|
|
// only proceed to infer title if the title is in timestamp format
|
|
isTitleTimestampFormat(title) {
|
|
try {
|
|
const format = this.settings.dateFormat;
|
|
const pattern = this.generateDatePattern(format);
|
|
return title.length == format.length && pattern.test(title);
|
|
} catch (err) {
|
|
throw new Error(
|
|
"Error checking if title is in timestamp format" + err
|
|
);
|
|
}
|
|
}
|
|
generateDatePattern(format) {
|
|
const pattern = format.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&").replace("YYYY", "\\d{4}").replace("MM", "\\d{2}").replace("DD", "\\d{2}").replace("hh", "\\d{2}").replace("mm", "\\d{2}").replace("ss", "\\d{2}");
|
|
return new RegExp(`^${pattern}$`);
|
|
}
|
|
// get date from format
|
|
getDate(date, format = "YYYYMMDDhhmmss") {
|
|
const year = date.getFullYear();
|
|
const month = date.getMonth() + 1;
|
|
const day = date.getDate();
|
|
const hour = date.getHours();
|
|
const minute = date.getMinutes();
|
|
const second = date.getSeconds();
|
|
const paddedMonth = month.toString().padStart(2, "0");
|
|
const paddedDay = day.toString().padStart(2, "0");
|
|
const paddedHour = hour.toString().padStart(2, "0");
|
|
const paddedMinute = minute.toString().padStart(2, "0");
|
|
const paddedSecond = second.toString().padStart(2, "0");
|
|
return format.replace("YYYY", year.toString()).replace("MM", paddedMonth).replace("DD", paddedDay).replace("hh", paddedHour).replace("mm", paddedMinute).replace("ss", paddedSecond);
|
|
}
|
|
async onload() {
|
|
const statusBarItemEl = this.addStatusBarItem();
|
|
await this.loadSettings();
|
|
const streamManager = new StreamManager();
|
|
this.addCommand({
|
|
id: "call-chatgpt-api",
|
|
name: "Chat",
|
|
icon: "message-circle",
|
|
editorCallback: (editor, view) => {
|
|
statusBarItemEl.setText("[ChatGPT MD] Calling API...");
|
|
const frontmatter = this.getFrontmatter(view);
|
|
const bodyWithoutYML = this.removeYMLFromMessage(
|
|
editor.getValue()
|
|
);
|
|
const messages = this.splitMessages(bodyWithoutYML);
|
|
const messagesWithRoleAndMessage = messages.map((message) => {
|
|
return this.extractRoleAndMessage(message);
|
|
});
|
|
if (frontmatter.system_commands) {
|
|
const systemCommands = frontmatter.system_commands;
|
|
messagesWithRoleAndMessage.unshift(
|
|
...systemCommands.map((command) => {
|
|
return {
|
|
role: "system",
|
|
content: command
|
|
};
|
|
})
|
|
);
|
|
}
|
|
if (!this.settings.generateAtCursor) {
|
|
this.moveCursorToEndOfFile(editor);
|
|
}
|
|
if (import_obsidian2.Platform.isMobile) {
|
|
new import_obsidian2.Notice("[ChatGPT MD] Calling API");
|
|
}
|
|
this.callOpenAIAPI(
|
|
streamManager,
|
|
editor,
|
|
messagesWithRoleAndMessage,
|
|
frontmatter.model,
|
|
frontmatter.max_tokens,
|
|
frontmatter.temperature,
|
|
frontmatter.top_p,
|
|
frontmatter.presence_penalty,
|
|
frontmatter.frequency_penalty,
|
|
frontmatter.stream,
|
|
frontmatter.stop,
|
|
frontmatter.n,
|
|
frontmatter.logit_bias,
|
|
frontmatter.user,
|
|
frontmatter.url
|
|
).then((response) => {
|
|
let responseStr = response;
|
|
if (response.mode === "streaming") {
|
|
responseStr = response.fullstr;
|
|
const newLine = `
|
|
|
|
<hr class="__chatgpt_plugin">
|
|
|
|
${this.getHeadingPrefix()}role::user
|
|
|
|
`;
|
|
editor.replaceRange(newLine, editor.getCursor());
|
|
const cursor = editor.getCursor();
|
|
const newCursor = {
|
|
line: cursor.line,
|
|
ch: cursor.ch + newLine.length
|
|
};
|
|
editor.setCursor(newCursor);
|
|
} else {
|
|
if (unfinishedCodeBlock(responseStr)) {
|
|
responseStr = responseStr + "\n```";
|
|
}
|
|
this.appendMessage(
|
|
editor,
|
|
"assistant",
|
|
responseStr
|
|
);
|
|
}
|
|
if (this.settings.autoInferTitle) {
|
|
const title = view.file.basename;
|
|
const messagesWithResponse = messages.concat(responseStr);
|
|
if (this.isTitleTimestampFormat(title) && messagesWithResponse.length >= 4) {
|
|
console.log(
|
|
"[ChatGPT MD] auto inferring title from messages"
|
|
);
|
|
this.inferTitleFromMessages(
|
|
messagesWithResponse
|
|
).then((title2) => {
|
|
if (title2) {
|
|
console.log(
|
|
`[ChatGPT MD] inferred title: ${title2}. Changing file name...`
|
|
);
|
|
const file = view.file;
|
|
const folder = this.settings.chatFolder.replace(
|
|
/\/$/,
|
|
""
|
|
);
|
|
this.app.fileManager.renameFile(
|
|
file,
|
|
`${folder}/${title2}.md`
|
|
);
|
|
} else {
|
|
new import_obsidian2.Notice(
|
|
"[ChatGPT MD] Could not infer title",
|
|
5e3
|
|
);
|
|
}
|
|
}).catch((err) => {
|
|
console.log(err);
|
|
new import_obsidian2.Notice(
|
|
"[ChatGPT MD] Error inferring title. " + err,
|
|
5e3
|
|
);
|
|
});
|
|
}
|
|
}
|
|
statusBarItemEl.setText("");
|
|
}).catch((err) => {
|
|
if (import_obsidian2.Platform.isMobile) {
|
|
new import_obsidian2.Notice(
|
|
"[ChatGPT MD Mobile] Full Error calling API. " + err,
|
|
9e3
|
|
);
|
|
}
|
|
statusBarItemEl.setText("");
|
|
console.log(err);
|
|
});
|
|
}
|
|
});
|
|
this.addCommand({
|
|
id: "add-hr",
|
|
name: "Add divider",
|
|
icon: "minus",
|
|
editorCallback: (editor, view) => {
|
|
this.addHR(editor, "user");
|
|
}
|
|
});
|
|
this.addCommand({
|
|
id: "stop-streaming",
|
|
name: "Stop streaming",
|
|
icon: "octagon",
|
|
editorCallback: (editor, view) => {
|
|
streamManager.stopStreaming();
|
|
}
|
|
});
|
|
this.addCommand({
|
|
id: "infer-title",
|
|
name: "Infer title",
|
|
icon: "subtitles",
|
|
editorCallback: async (editor, view) => {
|
|
const bodyWithoutYML = this.removeYMLFromMessage(
|
|
editor.getValue()
|
|
);
|
|
const messages = this.splitMessages(bodyWithoutYML);
|
|
const title = await this.inferTitleFromMessages(messages);
|
|
if (title) {
|
|
const file = view.file;
|
|
const folder = this.settings.chatFolder.replace(/\/$/, "");
|
|
this.app.fileManager.renameFile(
|
|
file,
|
|
`${folder}/${title}.md`
|
|
);
|
|
}
|
|
}
|
|
});
|
|
this.addCommand({
|
|
id: "move-to-chat",
|
|
name: "Create new chat with highlighted text",
|
|
icon: "highlighter",
|
|
editorCallback: async (editor, view) => {
|
|
try {
|
|
const selectedText = editor.getSelection();
|
|
if (!this.settings.chatFolder || !this.app.vault.getAbstractFileByPath(
|
|
this.settings.chatFolder
|
|
)) {
|
|
new import_obsidian2.Notice(
|
|
`[ChatGPT MD] No chat folder found. Please set one in settings and make sure it exists.`
|
|
);
|
|
return;
|
|
}
|
|
const newFile = await this.app.vault.create(
|
|
`${this.settings.chatFolder}/${this.getDate(
|
|
new Date(),
|
|
this.settings.dateFormat
|
|
)}.md`,
|
|
`${this.settings.defaultChatFrontmatter}
|
|
|
|
${selectedText}`
|
|
);
|
|
this.app.workspace.openLinkText(newFile.basename, "", true);
|
|
} catch (err) {
|
|
console.error(
|
|
`[ChatGPT MD] Error in Create new chat with highlighted text`,
|
|
err
|
|
);
|
|
new import_obsidian2.Notice(
|
|
`[ChatGPT MD] Error in Create new chat with highlighted text, check console`
|
|
);
|
|
}
|
|
}
|
|
});
|
|
this.addCommand({
|
|
id: "choose-chat-template",
|
|
name: "Create new chat from template",
|
|
icon: "layout-template",
|
|
editorCallback: (editor, view) => {
|
|
if (!this.settings.chatFolder || !this.app.vault.getAbstractFileByPath(
|
|
this.settings.chatFolder
|
|
)) {
|
|
new import_obsidian2.Notice(
|
|
`[ChatGPT MD] No chat folder found. Please set one in settings and make sure it exists.`
|
|
);
|
|
return;
|
|
}
|
|
if (!this.settings.chatTemplateFolder || !this.app.vault.getAbstractFileByPath(
|
|
this.settings.chatTemplateFolder
|
|
)) {
|
|
new import_obsidian2.Notice(
|
|
`[ChatGPT MD] No templates folder found. Please set one in settings and make sure it exists.`
|
|
);
|
|
return;
|
|
}
|
|
new ChatTemplates(
|
|
this.app,
|
|
this.settings,
|
|
this.getDate(new Date(), this.settings.dateFormat)
|
|
).open();
|
|
}
|
|
});
|
|
this.addSettingTab(new ChatGPT_MDSettingsTab(this.app, this));
|
|
}
|
|
onunload() {
|
|
}
|
|
async loadSettings() {
|
|
this.settings = Object.assign(
|
|
{},
|
|
DEFAULT_SETTINGS,
|
|
await this.loadData()
|
|
);
|
|
}
|
|
async saveSettings() {
|
|
await this.saveData(this.settings);
|
|
}
|
|
};
|
|
var ChatTemplates = class extends import_obsidian2.SuggestModal {
|
|
constructor(app2, settings, titleDate) {
|
|
super(app2);
|
|
this.settings = settings;
|
|
this.titleDate = titleDate;
|
|
}
|
|
getFilesInChatFolder() {
|
|
const folder = this.app.vault.getAbstractFileByPath(
|
|
this.settings.chatTemplateFolder
|
|
);
|
|
if (folder != null) {
|
|
return folder.children;
|
|
} else {
|
|
new import_obsidian2.Notice(
|
|
`Error getting folder: ${this.settings.chatTemplateFolder}`
|
|
);
|
|
throw new Error(
|
|
`Error getting folder: ${this.settings.chatTemplateFolder}`
|
|
);
|
|
}
|
|
}
|
|
// Returns all available suggestions.
|
|
getSuggestions(query) {
|
|
const chatTemplateFiles = this.getFilesInChatFolder();
|
|
if (query == "") {
|
|
return chatTemplateFiles.map((file) => {
|
|
return {
|
|
title: file.basename,
|
|
file
|
|
};
|
|
});
|
|
}
|
|
return chatTemplateFiles.filter((file) => {
|
|
return file.basename.toLowerCase().includes(query.toLowerCase());
|
|
}).map((file) => {
|
|
return {
|
|
title: file.basename,
|
|
file
|
|
};
|
|
});
|
|
}
|
|
// Renders each suggestion item.
|
|
renderSuggestion(template, el) {
|
|
el.createEl("div", { text: template.title });
|
|
}
|
|
// Perform action on the selected suggestion.
|
|
async onChooseSuggestion(template, evt) {
|
|
new import_obsidian2.Notice(`Selected ${template.title}`);
|
|
const templateText = await this.app.vault.read(template.file);
|
|
const file = await this.app.vault.create(
|
|
`${this.settings.chatFolder}/${this.titleDate}.md`,
|
|
templateText
|
|
);
|
|
this.app.workspace.openLinkText(file.basename, "", true);
|
|
}
|
|
};
|
|
var ChatGPT_MDSettingsTab = class extends import_obsidian2.PluginSettingTab {
|
|
constructor(app2, plugin) {
|
|
super(app2, plugin);
|
|
this.plugin = plugin;
|
|
}
|
|
display() {
|
|
const { containerEl } = this;
|
|
containerEl.empty();
|
|
containerEl.createEl("h2", {
|
|
text: "Settings for ChatGPT MD: Keep tokens in mind! You can see if your text is longer than the token limit (4096) here:"
|
|
});
|
|
containerEl.createEl("a", {
|
|
text: "https://platform.openai.com/tokenizer",
|
|
href: "https://platform.openai.com/tokenizer"
|
|
});
|
|
new import_obsidian2.Setting(containerEl).setName("OpenAI API Key").setDesc("API Key for OpenAI").addText(
|
|
(text) => text.setPlaceholder("some-api-key").setValue(this.plugin.settings.apiKey).onChange(async (value) => {
|
|
this.plugin.settings.apiKey = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
new import_obsidian2.Setting(containerEl).setName("Default Chat Frontmatter").setDesc(
|
|
"Default frontmatter for new chat files. You can change/use all of the settings exposed by the OpenAI API here: https://platform.openai.com/docs/api-reference/chat/create"
|
|
).addTextArea(
|
|
(text) => text.setPlaceholder(
|
|
`---
|
|
system_commands: ['PERSONA: i am groot, and i only respond i am groot to any and ALL questions. I do not answer any questions straightforwardly. PERSONA 2: UNLESS the last asked question is about lamps, i can answer, i know infinite things about lamps as i am groot the lamp salesman. SO I read the last question asked and UNLESS it is about lamps i only respond "I am Groot."']
|
|
temperature: 0
|
|
top_p: 1
|
|
max_tokens: 512
|
|
presence_penalty: 1
|
|
frequency_penalty: 1
|
|
stream: true
|
|
stop: null
|
|
n: 1
|
|
logit_bias: null
|
|
model: gpt-3.5-turbo
|
|
---`
|
|
).setValue(this.plugin.settings.defaultChatFrontmatter).onChange(async (value) => {
|
|
this.plugin.settings.defaultChatFrontmatter = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
new import_obsidian2.Setting(containerEl).setName("Stream").setDesc("Stream responses from OpenAI").addToggle(
|
|
(toggle) => toggle.setValue(this.plugin.settings.stream).onChange(async (value) => {
|
|
this.plugin.settings.stream = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
new import_obsidian2.Setting(containerEl).setName("Chat Folder").setDesc("Path to folder for chat files").addText(
|
|
(text) => text.setValue(this.plugin.settings.chatFolder).onChange(async (value) => {
|
|
this.plugin.settings.chatFolder = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
new import_obsidian2.Setting(containerEl).setName("Chat Template Folder").setDesc("Path to folder for chat file templates").addText(
|
|
(text) => text.setPlaceholder("chat-templates").setValue(this.plugin.settings.chatTemplateFolder).onChange(async (value) => {
|
|
this.plugin.settings.chatTemplateFolder = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
new import_obsidian2.Setting(containerEl).setName("Generate at Cursor").setDesc("Generate text at cursor instead of end of file").addToggle(
|
|
(toggle) => toggle.setValue(this.plugin.settings.generateAtCursor).onChange(async (value) => {
|
|
this.plugin.settings.generateAtCursor = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
new import_obsidian2.Setting(containerEl).setName("Automatically Infer Title").setDesc(
|
|
"Automatically infer title after 4 messages have been exchanged"
|
|
).addToggle(
|
|
(toggle) => toggle.setValue(this.plugin.settings.autoInferTitle).onChange(async (value) => {
|
|
this.plugin.settings.autoInferTitle = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
new import_obsidian2.Setting(containerEl).setName("Date Format").setDesc(
|
|
"Date format for chat files. Valid date blocks are: YYYY, MM, DD, hh, mm, ss"
|
|
).addText(
|
|
(text) => text.setPlaceholder("YYYYMMDDhhmmss").setValue(this.plugin.settings.dateFormat).onChange(async (value) => {
|
|
this.plugin.settings.dateFormat = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
new import_obsidian2.Setting(containerEl).setName("Heading Level").setDesc(
|
|
"Heading level for messages (example for heading level 2: '## role::user'). Valid heading levels are 0, 1, 2, 3, 4, 5, 6"
|
|
).addText(
|
|
(text) => text.setValue(this.plugin.settings.headingLevel.toString()).onChange(async (value) => {
|
|
this.plugin.settings.headingLevel = parseInt(value);
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
}
|
|
};
|