Compare commits

..

11 commits
0.0.1 ... main

Author SHA1 Message Date
vorotamoroz
b7f8e8a61a bump 2023-06-20 17:37:08 +09:00
vorotamoroz
8c57ebbe19 Fixed:
- Now the layout could not be broken even if there are multiple conversations.
Improved:
- We can interrupt the response from the assistant.
- We can remove the existing statements.
2023-06-20 17:36:04 +09:00
vorotamoroz
7ed2a836f9 Improved:
- No longer comments will be shown on a callout.
2023-05-09 17:41:43 +09:00
vorotamoroz
d6d79cf9f3 Improved:
- Response is now streaming
- Conversation is now customisable (i.e. temperature).

Fixed:
- Now conversation will be separated with an empty line to render markdown correctly.

Removed:
- No longer consuming tokens are counted.
2023-05-09 12:47:06 +09:00
vorotamoroz
4fcf0bea63 Fixed:
- Remove usage of global `App` and now `App` on the property of the plugin has been used.
2023-03-22 00:46:17 +09:00
vorotamoroz
4b7990a5aa Improved:
- Consulting dialogue is now shown as callouts
- Default template from `new dialogue` is modified for rendering code blocks.
- Minor format issue has been fixed.
2023-03-20 10:42:50 +09:00
vorotamoroz
4c34ac4d99 bump 2023-03-16 13:37:30 +09:00
vorotamoroz
164f8d8c28 remove prefix 2023-03-16 13:36:58 +09:00
vorotamoroz
45087f6753 Fix the directory structure 2023-03-16 13:34:22 +09:00
vorotamoroz
bab429cb61 Add license 2023-03-16 12:01:22 +09:00
vorotamoroz
a8b333af07 - Fix styles
- Add showing consumed tokens
- Model can be configurable.
2023-03-16 11:44:44 +09:00
9 changed files with 474 additions and 444 deletions

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 vorotamoroz
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -21,5 +21,9 @@ Note: We need the [API key of ChatGPT](https://platform.openai.com/account/api-k
2. That is all. Now, it is time to chat!
## Memo
- The dialogue is saved as a code block. We can continue the conversation anytime we want.
- The dialogue is saved as a code block. We can resume the conversation anytime we want.
- Also, they are searchable.
- Long consultation consumes many tokens. If the dialogue gets longer, starting a brand-new one is recommended.
## License
- MIT License

View file

@ -15,7 +15,7 @@ const context = await esbuild.context({
banner: {
js: banner,
},
entryPoints: ["main.ts"],
entryPoints: ["src/main.ts"],
bundle: true,
external: [
"obsidian",

216
main.ts
View file

@ -1,216 +0,0 @@
import { App, Editor, MarkdownRenderer, MarkdownView, Notice, Plugin, PluginSettingTab, Setting, TFile } from 'obsidian';
import { Configuration, OpenAIApi } from "openai"
import axios from "axios";
interface RingASecretarySettings {
token: string;
defaultSystem: string;
}
const DEFAULT_SETTINGS: RingASecretarySettings = {
token: "",
defaultSystem: "",
}
const roleSystem = "**SYSTEM**";
const roleUser = "**USER**";
const roleAssistant = "**ASSISTANT**";
type DIALOGUE_ROLE = "**SYSTEM**" | "**USER**" | "**ASSISTANT**";
type API_ROLE = "system" | "user" | "assistant";
const MarkToRole = {
"**SYSTEM**": "system",
"**USER**": "user",
"**ASSISTANT**": "assistant",
} as Record<DIALOGUE_ROLE, API_ROLE>;
const WAIT_MARK = " WAITING FOR RESPONSE...";
export default class RingASecretaryPlugin extends Plugin {
settings: RingASecretarySettings;
configuration: Configuration;
processingFile?: TFile;
async askToAI(dialogue: string) {
const messages = [] as { role: "system" | "user" | "assistant", content: string }[];
let currentRole = "user" as "user" | "system" | "assistant";
let buffer = "";
const dialogLines = dialogue.split("\n");
for (const line of dialogLines) {
let lineBuf = line.trim();
if (lineBuf.startsWith("#")) continue;
let newRole = "" as "" | API_ROLE;
for (const [dialogueRole, APIRole] of Object.entries(MarkToRole)) {
if (lineBuf.startsWith(dialogueRole)) {
newRole = APIRole;
lineBuf = lineBuf.substring(dialogueRole.length + 1);
}
}
if (newRole != "" && buffer.trim() != "") {
messages.push({ role: currentRole, content: buffer.trim() });
currentRole = newRole;
buffer = "";
}
buffer += lineBuf + "\n";
}
if (buffer != "") {
messages.push({ role: currentRole, content: buffer });
}
const openai = new OpenAIApi(this.configuration);
const response = await openai.createChatCompletion({
model: "gpt-3.5-turbo-0301",
messages
// temperature: 0,
// max_tokens: 7,
});
const responseContent = response.data.choices[0].message?.content;
// const responseRole = response.data.choices[0].message?.role;
this.writeResponseToFile(responseContent ?? "");
}
async writeResponseToFile(response: string) {
if (!this.processingFile) return;
const file = this.processingFile;
await app.vault.process(file, (data) => {
return data.replace(WAIT_MARK, response);
});
app.vault.trigger("modify", file);
this.processingFile = undefined;
}
async onload() {
await this.loadSettings();
axios.defaults.adapter = "http";
this.configuration = new Configuration({ apiKey: this.settings.token });
this.registerMarkdownCodeBlockProcessor("aichat", (source, el, ctx) => {
const sourcePath = ctx.sourcePath;
const fx = el.createDiv({ text: "", cls: ["obsidian-fx"] });
MarkdownRenderer.renderMarkdown(source, fx, sourcePath, this)
const ops = el.createDiv({ text: "", cls: ["obsidian-fx-buttons"] });
const span = ops.createSpan({ text: "USER:" });
const input = ops.createEl("textarea");
const submit = ops.createEl("button", { text: "🤵" });
const secInfo = ctx.getSectionInfo(el);
ops.appendChild(span);
ops.appendChild(input)
ops.appendChild(submit);
fx.appendChild(ops)
el.replaceWith(fx);
// ctx.
const c = new AbortController();
const submitFunc = async () => {
if (source.contains(WAIT_MARK)) {
new Notice("Some question is already in progress...", 5000)//TODO:MESSAGE
return
}
const f = app.vault.getAbstractFileByPath(sourcePath);
if (!f) {
new Notice("Could not edit the file", 3000);
return;
}
if (!(f instanceof TFile)) {
new Notice("Could not edit the file", 3000);
return;
}
const dataMain = source;
const text = input.value;
//TODO:ESCAPE MARKDOWN?
const newBody = `${dataMain}\n${roleUser}:${text} \n${roleAssistant}: ${WAIT_MARK}`;
await app.vault.process(f, (data) => {
const dataList = data.split("\n");
const dataBefore = dataList.slice(0, (secInfo?.lineStart ?? 0) + 1);
const dataAfter = dataList.slice(secInfo?.lineEnd)
return dataBefore.join("\n") + `\n${newBody}\n` + dataAfter.join("\n");
});
this.processingFile = f;
setTimeout(() => {
this.askToAI(newBody).catch(err => {
this.writeResponseToFile("Something has been occurred (PLUGIN)")
});
}, 20);
app.vault.trigger("modify", f);
}
submit.addEventListener("click", submitFunc, { signal: c.signal });
input.addEventListener("keydown", e => {
console.dir(e);
if (e.key == "Enter" && (e.shiftKey) && !e.isComposing) {
e.preventDefault();
submitFunc();
input.value = "";
}
}, { signal: c.signal, capture: true })
});
this.addCommand({
id: 'obs-chat-new-dialogue',
name: 'New dialogue',
editorCallback: async (editor: Editor, view: MarkdownView) => {
editor.replaceSelection(
`${"```aichat"}
${this.settings.defaultSystem ? `${roleSystem}:${this.settings.defaultSystem}` : `#${roleSystem}:`}
${"```"}
`
)
}
});
this.addSettingTab(new RingASecretarySettingTab(this.app, this));
}
onunload() {
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings() {
await this.saveData(this.settings);
}
}
class RingASecretarySettingTab extends PluginSettingTab {
plugin: RingASecretaryPlugin;
constructor(app: App, plugin: RingASecretaryPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
containerEl.createEl('h2', { text: 'Settings for Ring a secretary' });
new Setting(containerEl)
.setName('Token')
.setDesc('The token of ChatGPT')
.addText(text => text
.setPlaceholder('sk-TrbCVkcuvcshu7b....')
.setValue(this.plugin.settings.token)
.onChange(async (value) => {
this.plugin.settings.token = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('Initial prompt')
.setDesc('Initial prompt; i.e., instructions and prerequisites presented to the AI.')
.addText(text => text
.setPlaceholder('Behave as a British English speaker and answer my questions')
.setValue(this.plugin.settings.defaultSystem)
.onChange(async (value) => {
this.plugin.settings.defaultSystem = value;
await this.plugin.saveSettings();
}));
}
}

View file

@ -1,9 +1,9 @@
{
"id": "ring-a-secretary",
"name": "Ring a secretary",
"version": "0.0.1",
"version": "0.0.8",
"minAppVersion": "0.15.0",
"description": "ChatGPT powered secretary",
"description": "Yet another ChatGPT-powered digital secretary",
"author": "vorotamoroz",
"authorUrl": "https://github.com/vrtmrz",
"isDesktopOnly": false

224
package-lock.json generated
View file

@ -1,22 +1,21 @@
{
"name": "ring-a-secretary",
"version": "0.0.1",
"version": "0.0.8",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "ring-a-secretary",
"version": "0.0.1",
"version": "0.0.8",
"license": "MIT",
"devDependencies": {
"@types/node": "^16.11.6",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"axios": "^1.3.4",
"builtin-modules": "3.3.0",
"esbuild": "0.17.3",
"obsidian": "latest",
"openai": "^3.2.1",
"openai-ext": "^1.2.6",
"tslib": "2.4.0",
"typescript": "4.7.4"
}
@ -825,23 +824,6 @@
"node": ">=8"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"dev": true
},
"node_modules/axios": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.4.tgz",
"integrity": "sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==",
"dev": true,
"dependencies": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@ -931,18 +913,6 @@
"dev": true,
"peer": true
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -989,15 +959,6 @@
"dev": true,
"peer": true
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"dev": true,
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/dir-glob": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@ -1406,40 +1367,6 @@
"dev": true,
"peer": true
},
"node_modules/follow-redirects": {
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dev": true,
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@ -1750,27 +1677,6 @@
"node": ">=8.6"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"dev": true,
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dev": true,
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@ -1830,24 +1736,11 @@
"wrappy": "1"
}
},
"node_modules/openai": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/openai/-/openai-3.2.1.tgz",
"integrity": "sha512-762C9BNlJPbjjlWZi4WYK9iM2tAVAv0uUp1UmI34vb0CN5T2mjB/qM6RYBmNKMh/dN9fC+bxqPwWJZUTWW052A==",
"dev": true,
"dependencies": {
"axios": "^0.26.0",
"form-data": "^4.0.0"
}
},
"node_modules/openai/node_modules/axios": {
"version": "0.26.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
"integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
"dev": true,
"dependencies": {
"follow-redirects": "^1.14.8"
}
"node_modules/openai-ext": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/openai-ext/-/openai-ext-1.2.6.tgz",
"integrity": "sha512-bva4Ql5G1lwBGeIBu0vI3mI9FeTyW/l2b7cq5kdEYasujzSAQxxqN9rPi4lSvjNuKofp3C0ravUMdTZkUtKlxw==",
"dev": true
},
"node_modules/optionator": {
"version": "0.9.1",
@ -1973,12 +1866,6 @@
"node": ">= 0.8.0"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"dev": true
},
"node_modules/punycode": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
@ -2785,23 +2672,6 @@
"integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
"dev": true
},
"asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"dev": true
},
"axios": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.4.tgz",
"integrity": "sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==",
"dev": true,
"requires": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@ -2870,15 +2740,6 @@
"dev": true,
"peer": true
},
"combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"requires": {
"delayed-stream": "~1.0.0"
}
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -2914,12 +2775,6 @@
"dev": true,
"peer": true
},
"delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"dev": true
},
"dir-glob": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@ -3241,23 +3096,6 @@
"dev": true,
"peer": true
},
"follow-redirects": {
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
"dev": true
},
"form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dev": true,
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
}
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@ -3495,21 +3333,6 @@
"picomatch": "^2.3.1"
}
},
"mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"dev": true
},
"mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dev": true,
"requires": {
"mime-db": "1.52.0"
}
},
"minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@ -3559,26 +3382,11 @@
"wrappy": "1"
}
},
"openai": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/openai/-/openai-3.2.1.tgz",
"integrity": "sha512-762C9BNlJPbjjlWZi4WYK9iM2tAVAv0uUp1UmI34vb0CN5T2mjB/qM6RYBmNKMh/dN9fC+bxqPwWJZUTWW052A==",
"dev": true,
"requires": {
"axios": "^0.26.0",
"form-data": "^4.0.0"
},
"dependencies": {
"axios": {
"version": "0.26.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
"integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
"dev": true,
"requires": {
"follow-redirects": "^1.14.8"
}
}
}
"openai-ext": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/openai-ext/-/openai-ext-1.2.6.tgz",
"integrity": "sha512-bva4Ql5G1lwBGeIBu0vI3mI9FeTyW/l2b7cq5kdEYasujzSAQxxqN9rPi4lSvjNuKofp3C0ravUMdTZkUtKlxw==",
"dev": true
},
"optionator": {
"version": "0.9.1",
@ -3665,12 +3473,6 @@
"dev": true,
"peer": true
},
"proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"dev": true
},
"punycode": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",

View file

@ -1,7 +1,7 @@
{
"name": "ring-a-secretary",
"version": "0.0.1",
"description": "This is a sample plugin for Obsidian (https://obsidian.md)",
"version": "0.0.8",
"description": "Yet another ChatGPT-powered digital secretary",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
@ -15,11 +15,10 @@
"@types/node": "^16.11.6",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"axios": "^1.3.4",
"builtin-modules": "3.3.0",
"esbuild": "0.17.3",
"obsidian": "latest",
"openai": "^3.2.1",
"openai-ext": "^1.2.6",
"tslib": "2.4.0",
"typescript": "4.7.4"
},

353
src/main.ts Normal file
View file

@ -0,0 +1,353 @@
import { App, Editor, MarkdownRenderer, MarkdownView, Notice, Plugin, PluginSettingTab, Setting, TFile } from 'obsidian';
// import { Configuration, OpenAIApi } from "openai"
import { ClientStreamChatCompletionConfig, OpenAIExt } from "openai-ext";
interface RingASecretarySettings {
token: string;
defaultSystem: string;
model: string;
showConsumedTokens: boolean;
template: string;
}
const DEFAULT_SETTINGS: RingASecretarySettings = {
token: "",
defaultSystem: "",
model: 'gpt-3.5-turbo-0301',
showConsumedTokens: false,
template: "##temperature\n##top_p\n##max_tokens\n##presence_penalty"
}
const roleSystem = "**SYSTEM**";
const roleUser = "**USER**";
const roleAssistant = "**ASSISTANT**";
type DIALOGUE_ROLE = "**SYSTEM**" | "**USER**" | "**ASSISTANT**";
type API_ROLE = "system" | "user" | "assistant";
const MarkToRole = {
"**SYSTEM**": "system",
"**USER**": "user",
"**ASSISTANT**": "assistant",
} as Record<DIALOGUE_ROLE, API_ROLE>;
const RESPONSE_START_MARK = "<span class='ofx-response-start'></span>";
const RESPONSE_END_MARK = "<span class='ofx-thinking'></span>";
const WAIT_MARK_INITIAL = `${RESPONSE_START_MARK}Please, bear with me.`;
function replaceStringBetweenMarks(source: string, newPiece: string, fromMark: string, toMark: string) {
const start = source.indexOf(fromMark);
const end = source.indexOf(toMark);
if (start == -1 || end == -1) return source;
return source.substring(0, start) + newPiece + source.substring(end + toMark.length);
}
// const WAIT_MARK = "<span class='ofx-response-start'></span><span class='ofx-thinking'></span>Please, bear with me.";
export default class RingASecretaryPlugin extends Plugin {
settings: RingASecretarySettings;
configuration: ClientStreamChatCompletionConfig;
request?: XMLHttpRequest;
async askToAI(dialogue: string, targetFile: TFile, targetKey: string) {
if (this.request) {
new Notice("Some question is already in progress...", 5000);
return
}
const messages = [] as { role: "system" | "user" | "assistant", content: string }[];
let currentRole = "" as "" | "user" | "system" | "assistant";
let buffer = "";
// let temperature: number | undefine = 0; /* 0 -> 1[def] -> 2 */
// let top_p: number | undefine = undefined; /* 1 def */
// let max_tokens: number | undefined = undefined; /* inf def */
// let presence_penalty: number | undefined = undefined; /* -2 -> 2[def] -> 2*/
const options = {} as {
temperature?: number,
top_p?: number,
max_tokens?: number,
presence_penalty?: number
}
const dialogLines = dialogue.split("\n");
const params = ["temperature", "top_p", "max_tokens", "presence_penalty"] as ("temperature" | "top_p" | "max_tokens" | "presence_penalty")[];
for (const line of dialogLines) {
let lineBuf = line.trim();
if (lineBuf.startsWith("#")) {
for (const param of params) {
const paramHead = `##${param}`;
if (lineBuf.startsWith(paramHead)) {
const val = lineBuf.substring(paramHead.length).trim();
if (val != "") options[param] = Number.parseFloat(val);
}
continue;
}
continue;
}
let newRole = "" as "" | API_ROLE;
for (const [dialogueRole, APIRole] of Object.entries(MarkToRole)) {
if (lineBuf.startsWith(dialogueRole)) {
newRole = APIRole;
lineBuf = lineBuf.substring(dialogueRole.length + 1);
}
}
if (newRole != "") {
if (buffer.trim() != "") {
messages.push({ role: currentRole == "" ? "user" : currentRole, content: buffer.trim() });
buffer = "";
}
currentRole = newRole;
}
buffer += lineBuf + "\n";
}
if (buffer != "" && !buffer.contains(RESPONSE_START_MARK)) {
messages.push({ role: currentRole == "" ? "user" : currentRole, content: buffer });
}
// eslint-disable-next-line @typescript-eslint/no-this-alias
const _this = this;
this.request = OpenAIExt.streamClientChatCompletion({
...options,
model: this.settings.model,
messages,
}, {
apiKey: this.settings.token,
allEnvsAllowed: true,
handler: {
// Content contains the string draft, which may be partial. When isFinal is true, the completion is done.
onContent(content, isFinal, xhr) {
_this.applyResponse(content, isFinal, targetKey, targetFile, dialogue);
},
onDone(xhr) {
//TODO
_this.request = undefined;
},
onError(error, status, xhr) {
_this.applyResponse(error.message, true, targetKey, targetFile, dialogue);
console.error(error);
_this.request = undefined;
},
},
});
}
/** Apply the response to the note while streaming, and write response to the file when it has been done. */
async applyResponse(response: string, isDone: boolean, targetKey: string, targetFile: TFile, basedDialogue: string) {
if (!isDone) {
const data = basedDialogue;
const out = replaceStringBetweenMarks(data, `${RESPONSE_START_MARK}${response}<span id='${targetKey}'></span>`, RESPONSE_START_MARK, RESPONSE_END_MARK);
const els = document.querySelectorAll(`span#${targetKey}`);
const el = els.length > 0 ? els[0] as HTMLSpanElement : undefined;
if (el) {
const p = el.matchParent("div.obsidian-fx") as HTMLDivElement;
this.renderMarkdownToEl(p, out, targetFile.path)
}
return;
}
await this.app.vault.process(targetFile, (data) => {
const out = replaceStringBetweenMarks(data, `${response}\n`, RESPONSE_START_MARK, RESPONSE_END_MARK);
return out;
});
this.app.vault.trigger("modify", targetFile);
}
renderMarkdownToEl(fx: HTMLDivElement, source: string, sourcePath: string) {
// All pragmatic comments will be hidden, but just simple comments are shown as with strikethrough.
const renderSource1 = `${source.replace(/^(##.*)$/mg, "<!-- $1 -->").replace(/^#(.*?)\s*$/mg, "~~$1~~")}`;
// All lines can be deleted or cancelled
const renderSource2 = renderSource1.split("\n").map((e, i) =>
e.contains(RESPONSE_START_MARK) ? e.split(roleAssistant).join(`<a href="#" class="fx-toggle-comment" data-line="${i}" data-cancel="1">✋</a>${roleAssistant}`) : e.split(roleAssistant).join(`<a href="#" class="fx-toggle-comment" data-line="${i}">🗑️</a>${roleAssistant}`).split(roleUser).join(`<a href="#" class="fx-toggle-comment" data-line="${i}">🗑️</a>${roleUser}`))
.join("\n")
fx.replaceChildren("");
const renderSource = `> [!consult]+\n${renderSource2.replace(/^/mg, "> ")}`;
MarkdownRenderer.renderMarkdown(renderSource, fx, sourcePath, this).then(() => {
document.querySelectorAll(".fx-toggle-comment").forEach((e) => {
e.addEventListener("click", (evt) => {
evt.preventDefault();
const el = e.matchParent("div.obsidian-fx") as HTMLDivElement;
if (!el) return;
const filename = el.getAttribute("data-source-path");
const line = Number.parseInt(el.getAttribute("data-context-line") as string);
const commentLine = Number.parseInt(e.getAttribute("data-line") as string);
if (!filename) return;
const f = this.app.vault.getAbstractFileByPath(filename);
if (!(f instanceof TFile)) {
new Notice("Could not find the file", 3000);
return;
}
const cancel = e.getAttribute("data-cancel");
if (cancel == "1") {
if (this.request) {
this.request.abort();
this.request = undefined;
new Notice("Cancelled!", 5000);
}
}
this.app.vault.process(f, (data) => {
const f = data.split("\n");
// Toggle commenting out, or remove it if it has been cancelled.
f[line + commentLine + 1] = cancel == "1" ? "" : f[line + commentLine + 1].startsWith("#") ? f[line + commentLine + 1].substring(1) : `#${f[line + commentLine + 1]}`;
return f.join("\n");
});
})
});
})
}
async onload() {
await this.loadSettings();
this.registerMarkdownCodeBlockProcessor("aichat", (source, el, ctx) => {
const uniq = `obsidian-fx-${Date.now()}-${~~(Math.random() * 10000)}`;
const fx = el.createDiv({ text: "", cls: ["obsidian-fx", uniq] });
// Store the path and the line to modify later.
const sourcePath = ctx.sourcePath;
const secInfo = ctx.getSectionInfo(el);
fx.setAttribute("data-source-path", sourcePath);
fx.setAttribute("data-context-line", `${secInfo?.lineStart}`);
this.renderMarkdownToEl(fx, source, sourcePath);
const ops = el.createDiv({ text: "", cls: ["obsidian-fx-buttons"] });
const span = ops.createEl("span", { text: "USER:", cls: "label" });
const input = ops.createEl("textarea", { cls: "ai-dialogue-input" });
const submit = ops.createEl("button", { text: "🤵" });
ops.appendChild(span);
ops.appendChild(input)
ops.appendChild(submit);
el.appendChild(fx);
el.appendChild(ops)
const c = new AbortController();
const submitFunc = async () => {
if (source.contains(RESPONSE_START_MARK)) {
new Notice("Some question is already in progress... If not, please modify the code block directly.", 5000);
return
}
const f = this.app.vault.getAbstractFileByPath(sourcePath);
if (!f) {
new Notice("Could not find the file", 3000);
return;
}
if (!(f instanceof TFile)) {
new Notice("Could not find the file", 3000);
return;
}
const dataMain = source;
const text = input.value;
if (text.trim() == "") {
new Notice("Request is empty");
return;
}
//Note: ESCAPE MARKDOWN?
const newBody = `${dataMain}\n${roleUser}: ${text}\n \n${roleAssistant}: ${WAIT_MARK_INITIAL}<span id='${uniq}'></span>${RESPONSE_END_MARK}`;
await this.app.vault.process(f, (data) => {
const dataList = data.split("\n");
const dataBefore = dataList.slice(0, (secInfo?.lineStart ?? 0) + 1);
const dataAfter = dataList.slice(secInfo?.lineEnd)
return dataBefore.join("\n") + `\n${newBody}\n` + dataAfter.join("\n");
});
setTimeout(() => {
this.askToAI(newBody, f, uniq).catch(err => {
this.applyResponse(`Something has been occurred: ${err?.message}`, true, uniq, f, newBody);
new Notice(`Something has been occurred: ${err?.message}`);
});
}, 20);
this.app.vault.trigger("modify", f);
}
submit.addEventListener("click", submitFunc, { signal: c.signal });
input.addEventListener("keydown", e => {
setTimeout(() => {
if (input.clientHeight < input.scrollHeight) {
input.style.height = `${input.scrollHeight + 4}px`;
}
}, 10);
if (e.key == "Enter" && (e.shiftKey) && !e.isComposing) {
e.preventDefault();
submitFunc();
input.value = "";
}
}, { signal: c.signal, capture: true })
});
this.addCommand({
id: 'new-dialogue',
name: 'New dialogue',
editorCallback: async (editor: Editor, view: MarkdownView) => {
editor.replaceSelection(
`${"````aichat"}
${this.settings.template ? `${this.settings.template}\n` : ""}${this.settings.defaultSystem ? `${roleSystem}: ${this.settings.defaultSystem}` : `#${roleSystem}: `}
${"````"}
`
)
}
});
this.addSettingTab(new RingASecretarySettingTab(this.app, this));
}
onunload() {
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings() {
await this.saveData(this.settings);
}
}
class RingASecretarySettingTab extends PluginSettingTab {
plugin: RingASecretaryPlugin;
constructor(app: App, plugin: RingASecretaryPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
containerEl.createEl('h2', { text: 'Settings for Ring a secretary' });
new Setting(containerEl)
.setName('Token')
.setDesc('The token of ChatGPT')
.addText(text => text
.setPlaceholder('sk-TrbCVkcuvcshu7b....')
.setValue(this.plugin.settings.token)
.onChange(async (value) => {
this.plugin.settings.token = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('Model')
.setDesc('Model')
.addText(text => text
.setPlaceholder('gpt-3.5-turbo-0301')
.setValue(this.plugin.settings.model)
.onChange(async (value) => {
this.plugin.settings.model = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('Initial prompt')
.setDesc('Initial prompt; i.e., instructions and prerequisites presented to the AI.')
.addText(text => text
.setPlaceholder('Behave as a British English speaker and answer my questions')
.setValue(this.plugin.settings.defaultSystem)
.onChange(async (value) => {
this.plugin.settings.defaultSystem = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('Conversation customising template')
.setDesc('')
.addTextArea(text => text
.setPlaceholder("")
.setValue(this.plugin.settings.template)
.onChange(async (value) => {
this.plugin.settings.template = value;
await this.plugin.saveSettings();
}));
}
}

View file

@ -6,8 +6,7 @@ available in the app when your plugin is enabled.
If your plugin does not need CSS, delete this file.
*/
.markdown-reading-view .obsidian-fx input,
.markdown-reading-view .obsidian-fx button {
.markdown-reading-view .obsidian-fx .obsidian-fx-buttons {
display: none;
}
@ -16,18 +15,86 @@ If your plugin does not need CSS, delete this file.
justify-content: center;
align-items: center;
margin: auto;
padding: 4px;
}
.obsidian-fx * {
margin: auto 2px;
.obsidian-fx-buttons button {
flex-grow: 0;
padding: 4px 8px 4px 8px;
margin: 4px 0px 4px 4px;
width: auto;
}
.obsidian-fx textarea {
.obsidian-fx-buttons .label {
margin-right: 4px;
}
.is-phone .obsidian-fx-buttons .label {
display: none;
}
.obsidian-fx-buttons textarea {
flex-grow: 1;
min-height: 3ex;
resize: vertical;
-webkit-appearance: none;
outline: none;
outline-style: none;
box-shadow: none;
border: 0px;
border-bottom: 1px solid var(--divider-color);
padding: 2px 4px;
font-size: var(--font-adaptive-normal);
font-weight: var(--normal-weight);
line-height: var(--line-height);
font-family: var(--font-editor);
}
/*
.obsidian-fx textarea .obsidian-fx input,
.obsidian-fx button {} */
.obsidian-fx-buttons textarea:focus {
-webkit-appearance: none;
outline: none;
outline-style: none;
}
.obsidian-fx:has(.callout.is-collapsed)+.obsidian-fx-buttons {
display: none;
}
.ofx-thinking {
animation: ofx-thinking-animation 1s infinite;
}
.ofx-thinking::before {
content: " ......";
animation: ofx-thinking-animation 1s infinite;
}
@keyframes ofx-thinking-animation {
0% {
content: ". .....";
}
20% {
content: ".. ....";
}
40% {
content: "... ...";
}
60% {
content: ".... ..";
}
80% {
content: "..... .";
}
100% {
content: " ...... ";
}
}
.callout[data-callout="consult"] {
--callout-icon: concierge-bell;
}