prettier formatting

This commit is contained in:
John Mavrick 2023-11-26 12:19:45 -08:00
parent 1a61117aa9
commit 96ac84eb5c
29 changed files with 4449 additions and 4382 deletions

View File

@ -2,9 +2,7 @@
"root": true,
"parser": "@typescript-eslint/parser",
"env": { "node": true },
"plugins": [
"@typescript-eslint"
],
"plugins": ["@typescript-eslint"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",

5
.prettierrc Normal file
View File

@ -0,0 +1,5 @@
{
"printWidth": 80,
"useTabs": false,
"singleQuote": true
}

View File

@ -1,43 +1,43 @@
import esbuild from "esbuild";
import process from "process";
import builtins from "builtin-modules";
import esbuild from 'esbuild';
import process from 'process';
import builtins from 'builtin-modules';
const banner =
`/*
const banner = `/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
if you want to view the source, please visit the github repository of this plugin
*/
`;
const prod = (process.argv[2] === "production");
const prod = process.argv[2] === 'production';
const context = await esbuild.context({
banner: {
js: banner,
},
entryPoints: ["main.ts"],
entryPoints: ['main.ts'],
bundle: true,
external: [
"obsidian",
"electron",
"@codemirror/autocomplete",
"@codemirror/collab",
"@codemirror/commands",
"@codemirror/language",
"@codemirror/lint",
"@codemirror/search",
"@codemirror/state",
"@codemirror/view",
"@lezer/common",
"@lezer/highlight",
"@lezer/lr",
...builtins],
format: "cjs",
target: "es2018",
logLevel: "info",
sourcemap: prod ? false : "inline",
'obsidian',
'electron',
'@codemirror/autocomplete',
'@codemirror/collab',
'@codemirror/commands',
'@codemirror/language',
'@codemirror/lint',
'@codemirror/search',
'@codemirror/state',
'@codemirror/view',
'@lezer/common',
'@lezer/highlight',
'@lezer/lr',
...builtins,
],
format: 'cjs',
target: 'es2018',
logLevel: 'info',
sourcemap: prod ? false : 'inline',
treeShaking: true,
outfile: "main.js",
outfile: 'main.js',
});
if (prod) {

41
main.ts
View File

@ -17,7 +17,7 @@ const DEFAULT_SETTINGS: ObsidianIntelligenceSettings = {
activeThread: undefined,
activeAssistant: undefined,
activeAssistantFiles: undefined,
}
};
export default class ObsidianIntelligence extends Plugin {
settings: ObsidianIntelligenceSettings;
@ -25,14 +25,15 @@ export default class ObsidianIntelligence extends Plugin {
async onload() {
await this.loadSettings();
this.registerView(
VIEW_TYPE,
(leaf) => (new AppView(leaf, this))
);
this.registerView(VIEW_TYPE, (leaf) => new AppView(leaf, this));
const ribbonIconEl = this.addRibbonIcon('bot', 'Open Obsidian Intelligence', (evt: MouseEvent) => {
const ribbonIconEl = this.addRibbonIcon(
'bot',
'Open Obsidian Intelligence',
(evt: MouseEvent) => {
this.activateView();
});
},
);
// Perform additional things with the ribbon
ribbonIconEl.addClass('my-plugin-ribbon-class');
@ -41,24 +42,26 @@ export default class ObsidianIntelligence extends Plugin {
statusBarItemEl.setText('Status Bar Text');
this.addCommand({
id: "obsidian-intelligence-view-open",
name: "Open Obsidian Intelligence",
hotkeys: [{ modifiers: ["Mod", "Shift"], key: "I"}],
id: 'obsidian-intelligence-view-open',
name: 'Open Obsidian Intelligence',
hotkeys: [{ modifiers: ['Mod', 'Shift'], key: 'I' }],
callback: () => {
this.activateView();
}
},
});
// This adds a settings tab so the user can configure various aspects of the plugin
this.addSettingTab(new OISettingTab(this.app, this));
}
onunload() {
}
onunload() {}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
this.settings = Object.assign(
{},
DEFAULT_SETTINGS,
await this.loadData(),
);
}
async saveSettings() {
@ -74,7 +77,7 @@ export default class ObsidianIntelligence extends Plugin {
});
this.app.workspace.revealLeaf(
this.app.workspace.getLeavesOfType(VIEW_TYPE)[0]
this.app.workspace.getLeavesOfType(VIEW_TYPE)[0],
);
}
}
@ -95,12 +98,14 @@ class OISettingTab extends PluginSettingTab {
new Setting(containerEl)
.setName('OpenAI Key')
.setDesc('Can find it https://platform.openai.com/api-keys')
.addText(text => text
.addText((text) =>
text
.setPlaceholder('Enter your API Key')
.setValue(this.plugin.settings.openaiKey)
.onChange(async (value) => {
this.plugin.settings.openaiKey = value;
await this.plugin.saveSettings();
}));
}),
);
}
}

16
package-lock.json generated
View File

@ -28,6 +28,7 @@
"builtin-modules": "3.3.0",
"esbuild": "0.17.3",
"obsidian": "latest",
"prettier": "^3.1.0",
"tslib": "2.4.0",
"typescript": "4.7.4"
}
@ -2524,6 +2525,21 @@
"node": ">= 0.8.0"
}
},
"node_modules/prettier": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz",
"integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==",
"dev": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/property-expr": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz",

View File

@ -6,7 +6,8 @@
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"version": "node version-bump.mjs && git add manifest.json versions.json"
"version": "node version-bump.mjs && git add manifest.json versions.json",
"prettier": "prettier --write ."
},
"keywords": [],
"author": "",
@ -18,6 +19,7 @@
"builtin-modules": "3.3.0",
"esbuild": "0.17.3",
"obsidian": "latest",
"prettier": "^3.1.0",
"tslib": "2.4.0",
"typescript": "4.7.4"
},

View File

@ -1,2 +1,3 @@
# This is a test file
wow

View File

@ -1,18 +1,18 @@
import { ItemView, WorkspaceLeaf } from "obsidian";
import * as React from "react";
import { Root, createRoot } from "react-dom/client";
import PluginView from "./PluginView";
import { App } from "obsidian";
import ObsidianIntelligence from "../../main";
import OpenAI from "openai";
import { ItemView, WorkspaceLeaf } from 'obsidian';
import * as React from 'react';
import { Root, createRoot } from 'react-dom/client';
import PluginView from './PluginView';
import { App } from 'obsidian';
import ObsidianIntelligence from '../../main';
import OpenAI from 'openai';
export const VIEW_TYPE = "example-view";
export const VIEW_TYPE = 'example-view';
export const AppContext = React.createContext<App | undefined>(undefined);
export const PluginContext = React.createContext<ObsidianIntelligence | undefined>(
undefined
);
export const PluginContext = React.createContext<
ObsidianIntelligence | undefined
>(undefined);
export const OpenAIContext = React.createContext<OpenAI | undefined>(undefined);
@ -57,10 +57,10 @@ export class AppView extends ItemView {
}
getDisplayText() {
return "Obsidian Intelligence";
return 'Obsidian Intelligence';
}
getIcon(): string {
return "bot";
return 'bot';
}
async onOpen() {
@ -74,7 +74,7 @@ export class AppView extends ItemView {
{/* </CommandsContext.Provider> */}
</OpenAIContext.Provider>
</PluginContext.Provider>
</AppContext.Provider>
</AppContext.Provider>,
);
}
async onClose() {

View File

@ -1,31 +1,40 @@
import OpenAI from 'openai';
import React, { useEffect, useState, useCallback } from 'react';
import Chatbox from './components/Chatbox';
import { useOpenAI, usePlugin, } from './AppView';
import { useOpenAI, usePlugin } from './AppView';
import MessageInput from './components/MessageInput';
// import FilesUploadUI from './components/FilesUploadUI';
import AssistantManager from './components/AssistantManager';
import { IThread, ThreadAnnotationFile } from './types';
import { createNotice } from '@/utils/Logs';
const listQueryOptions: OpenAI.Beta.Threads.MessageListParams = { order: 'asc'};
const listQueryOptions: OpenAI.Beta.Threads.MessageListParams = {
order: 'asc',
};
const PluginView = () => {
const plugin = usePlugin();
const openaiInstance = useOpenAI();
const [messages, setMessages] = useState<OpenAI.Beta.Threads.ThreadMessage[]>([]);
const [messages, setMessages] = useState<
OpenAI.Beta.Threads.ThreadMessage[]
>([]);
// const [files, setFiles] = useState<string[]>([]);
const [assistants, setAssistants] = useState<OpenAI.Beta.Assistant[]>([]);
const [threads, setThreads] = useState<IThread[]>([]);
const [activeAssistant, setActiveAssistant] = useState<OpenAI.Beta.Assistant | undefined>(undefined);
const [activeThread, setActiveThread] = useState<IThread | undefined>(undefined);
const [activeAssistantFiles, setActiveAssistantFiles] = useState<OpenAI.Files.FileObject[] | undefined>(undefined);
const [activeAssistant, setActiveAssistant] = useState<
OpenAI.Beta.Assistant | undefined
>(undefined);
const [activeThread, setActiveThread] = useState<IThread | undefined>(
undefined,
);
const [activeAssistantFiles, setActiveAssistantFiles] = useState<
OpenAI.Files.FileObject[] | undefined
>(undefined);
const [isResponding, setIsResponding] = useState(false);
useEffect(() => {
fetchThreads();
if (assistants.length < 1) {
fetchAssistants();
fetchActiveConfiguration();
}
@ -48,7 +57,6 @@ const PluginView = () => {
return;
}
await openaiInstance.beta.assistants.list().then((res) => {
// sort by name
const sortedAssistants = res.data.sort((a, b) => {
if (a.name && b.name && a.name < b.name) {
@ -65,7 +73,7 @@ const PluginView = () => {
if (plugin) {
setThreads(plugin.settings.threads);
}
}
};
const fetchActiveConfiguration = () => {
if (plugin) {
@ -78,7 +86,7 @@ const PluginView = () => {
setActiveThread(savedActiveThread);
}
}
}
};
const updateActiveAssistant = async (assistant: OpenAI.Beta.Assistant) => {
if (plugin) {
@ -88,7 +96,7 @@ const PluginView = () => {
plugin.saveSettings();
setActiveAssistant(assistant);
}
}
};
const updateActiveAssistantFiles = (files: OpenAI.Files.FileObject[]) => {
if (plugin) {
@ -96,7 +104,7 @@ const PluginView = () => {
plugin.saveSettings();
setActiveAssistantFiles(files);
}
}
};
const updateThreads = (threads: IThread[]) => {
if (plugin) {
@ -104,30 +112,34 @@ const PluginView = () => {
plugin.saveSettings();
setThreads(threads);
}
}
};
const fetchAssistantFiles = async (): Promise<OpenAI.Files.FileObject[] | []> => {
const fetchAssistantFiles = async (): Promise<
OpenAI.Files.FileObject[] | []
> => {
if (!openaiInstance || !plugin || !activeAssistant) {
return [];
}
try {
const assistantFilesResponse = await openaiInstance.beta.assistants.files.list(activeAssistant.id);
const assistantFilesResponse =
await openaiInstance.beta.assistants.files.list(
activeAssistant.id,
);
const innerFiles: OpenAI.Files.FileObject[] = [];
await Promise.all(
assistantFilesResponse.data.map(async (file) => {
try {
const fileInfoResponse = await openaiInstance.files.retrieve(file.id);
const fileInfoResponse =
await openaiInstance.files.retrieve(file.id);
innerFiles.push(fileInfoResponse);
} catch (error) {
console.error(error);
}
})
}),
);
return innerFiles;
@ -139,24 +151,27 @@ const PluginView = () => {
const updateActiveThread = (thread: IThread) => {
if (plugin) {
plugin.settings.activeThread = thread;
plugin.saveSettings();
setActiveThread(thread);
}
}
};
const fetchMessages = async () => {
if (!openaiInstance || !activeThread) {
return;
}
try {
const messages = await openaiInstance.beta.threads.messages.list(activeThread.id, listQueryOptions);
const messages = await openaiInstance.beta.threads.messages.list(
activeThread.id,
listQueryOptions,
);
setMessages(messages.data);
} catch (e) {
createNotice('Thread expired or not found. Please select a new thread.');
createNotice(
'Thread expired or not found. Please select a new thread.',
);
//remove thread from list
const newThreads = threads.filter((t) => t.id !== activeThread.id);
setThreads(newThreads);
@ -175,26 +190,30 @@ const PluginView = () => {
fetchMessages();
}, [activeThread]);
const onMessageSend = useCallback(async (message: string) => {
const onMessageSend = useCallback(
async (message: string) => {
if (openaiInstance && activeThread && activeAssistant) {
const messageObject = await openaiInstance.beta.threads.messages.create(activeThread.id, {
role: "user",
const messageObject =
await openaiInstance.beta.threads.messages.create(
activeThread.id,
{
role: 'user',
content: message,
});
},
);
setMessages([...messages, messageObject]);
const run = await openaiInstance.beta.threads.runs.create(activeThread.id, {
const run = await openaiInstance.beta.threads.runs.create(
activeThread.id,
{
assistant_id: activeAssistant.id,
});
},
);
let runStatus = await openaiInstance.beta.threads.runs.retrieve(
activeThread.id,
run.id
run.id,
);
// Initialize a counter and max attempts for the polling logic, and how long to wait each try
@ -203,14 +222,24 @@ const PluginView = () => {
const timoutWaitTimeMs = 2000;
setIsResponding(true);
while (runStatus.status !== "completed" && attempts < maxAttempts) {
await new Promise((resolve) => setTimeout(resolve, timoutWaitTimeMs));
runStatus = await openaiInstance.beta.threads.runs.retrieve(activeThread.id, run.id);
while (
runStatus.status !== 'completed' &&
attempts < maxAttempts
) {
await new Promise((resolve) =>
setTimeout(resolve, timoutWaitTimeMs),
);
runStatus = await openaiInstance.beta.threads.runs.retrieve(
activeThread.id,
run.id,
);
attempts++;
}
// Get latest messages
await openaiInstance.beta.threads.messages.list(activeThread.id, listQueryOptions).then((res) => {
await openaiInstance.beta.threads.messages
.list(activeThread.id, listQueryOptions)
.then((res) => {
setMessages(res.data);
const files: ThreadAnnotationFile[] = [];
//for each message in messages, if it has content.type = text, then access content.annotations.file_citation.file_id and get the file name from the active files list
@ -218,20 +247,30 @@ const PluginView = () => {
if (message.content) {
message.content.forEach((content) => {
if (content.type === 'text') {
content.text.annotations.forEach((annotation) => {
content.text.annotations.forEach(
(annotation) => {
// @ts-ignore
if (annotation.file_citation) {
const fileId: string =
// @ts-ignore
const fileId: string = annotation.file_citation.file_id;
const file = activeAssistantFiles?.find((file) => file.id === fileId);
annotation.file_citation
.file_id;
const file =
activeAssistantFiles?.find(
(file) =>
file.id ===
fileId,
);
if (file) {
files.push({
fileId: fileId,
fileName: file.filename,
fileName:
file.filename,
});
}
}
});
},
);
}
});
}
@ -242,9 +281,11 @@ const PluginView = () => {
metadata: {
...activeThread.metadata,
annotationFiles: files,
}
},
};
const newThreads = threads.map((t) => t.id === activeThread.id ? newThread : t);
const newThreads = threads.map((t) =>
t.id === activeThread.id ? newThread : t,
);
updateThreads(newThreads);
updateActiveThread(newThread);
}
@ -252,7 +293,9 @@ const PluginView = () => {
setIsResponding(false);
});
}
}, [openaiInstance, activeThread, activeAssistant, messages]);
},
[openaiInstance, activeThread, activeAssistant, messages],
);
return (
<div className="agent-view-container">
@ -264,7 +307,8 @@ const PluginView = () => {
threads={threads}
updateThreads={updateThreads}
activeThread={activeThread}
updateActiveThread={updateActiveThread}/>
updateActiveThread={updateActiveThread}
/>
<Chatbox
messages={messages}
isResponding={isResponding}

View File

@ -1,21 +1,21 @@
import React, { useEffect, useMemo } from "react";
import OpenAI from "openai";
import { useApp, useOpenAI, usePlugin } from "../AppView";
import { IThread } from "../types";
import DropdownSelect from "./DropdownSelect";
import { MarkdownView } from "obsidian";
import { Bot, MessageSquare, Plus, Pencil, Trash2 } from "lucide-react";
import React, { useEffect, useMemo } from 'react';
import OpenAI from 'openai';
import { useApp, useOpenAI, usePlugin } from '../AppView';
import { IThread } from '../types';
import DropdownSelect from './DropdownSelect';
import { MarkdownView } from 'obsidian';
import { Bot, MessageSquare, Plus, Pencil, Trash2 } from 'lucide-react';
import {
AssistantEditModal,
IAssistantEditModalValues,
} from "./modals/AssistantEditModal";
import {} from "./modals/AssistantEditModal";
} from './modals/AssistantEditModal';
import {} from './modals/AssistantEditModal';
import {
ThreadEditModal,
IThreadEditModalValues,
} from "./modals/ThreadEditModal";
import { createNotice } from "@/utils/Logs";
import { defaultAssistantInstructions } from "@/utils/templates";
} from './modals/ThreadEditModal';
import { createNotice } from '@/utils/Logs';
import { defaultAssistantInstructions } from '@/utils/templates';
interface AssistantManagerProps {
assistants: OpenAI.Beta.Assistant[];
@ -48,8 +48,8 @@ const AssistantManager = ({
}
plugin.addCommand({
id: "create-assistant-from-active-note",
name: "Create Assistant from Active Note",
id: 'create-assistant-from-active-note',
name: 'Create Assistant from Active Note',
checkCallback: (checking: boolean) => {
// Conditions to check
const markdownView =
@ -62,16 +62,17 @@ const AssistantManager = ({
// (link) => addFileType(truncateLink(link.link))
// ) || [];
const links = Object.keys(
app.metadataCache.resolvedLinks[openFile.path]
app.metadataCache.resolvedLinks[openFile.path],
);
//@ts-ignore
const backlinks = Object.keys(
app.metadataCache.getBacklinksForFile(openFile).data
//@ts-ignore
app.metadataCache.getBacklinksForFile(openFile)
.data,
).map((file) => file);
console.log(
"file metadata cache",
app.metadataCache.getCache(openFile.path)?.links
'file metadata cache',
app.metadataCache.getCache(openFile.path)?.links,
);
const currentFile = openFile.path;
const filesToUpload = new Set([
@ -84,7 +85,7 @@ const AssistantManager = ({
name: `${openFile.path} Assistant`,
instructions: defaultAssistantInstructions,
files: Array.from(filesToUpload)
.filter((file) => file.endsWith(".md"))
.filter((file) => file.endsWith('.md'))
.map((file) => ({
filename: file,
})),
@ -104,7 +105,7 @@ const AssistantManager = ({
return;
}
const newThreadName = "New Thread";
const newThreadName = 'New Thread';
await openaiInstance.beta.threads
.create({
@ -150,7 +151,7 @@ const AssistantManager = ({
return newThread;
}
return thread;
})
}),
);
updateActiveThread(newThread);
});
@ -164,14 +165,14 @@ const AssistantManager = ({
// Get the previous values of the thread
const previousValues = {
metadata: {
name: activeThread.metadata.name || "",
name: activeThread.metadata.name || '',
},
};
new ThreadEditModal({
app,
title: "Edit Thread",
submitButtonText: "Edit",
title: 'Edit Thread',
submitButtonText: 'Edit',
previousValues,
onSubmit: editThread,
}).open();
@ -186,7 +187,7 @@ const AssistantManager = ({
.del(activeThread.id)
.then((res) => {
const newThreadsList = threads.filter(
(thread) => thread.id !== activeThread.id
(thread) => thread.id !== activeThread.id,
);
updateThreads(newThreadsList);
updateActiveThread(newThreadsList?.[0]);
@ -208,25 +209,23 @@ const AssistantManager = ({
values.files.map(async (file) => {
if (file.filename) {
const uploadedFile = await uploadFileToOpenAI(
file?.filename
file?.filename,
);
if (uploadedFile) {
uploadedFiles.push(uploadedFile.id);
}
}
})
}),
);
await openaiInstance.beta.assistants
.create({
name: values.name,
description: values.description,
instructions: values.instructions,
tools: [{ type: "code_interpreter" }, { type: "retrieval" }],
model: "gpt-4-1106-preview",
tools: [{ type: 'code_interpreter' }, { type: 'retrieval' }],
model: 'gpt-4-1106-preview',
file_ids: uploadedFiles,
})
.then((res) => {
@ -237,7 +236,7 @@ const AssistantManager = ({
};
const handleCreateAssistant = async (
assistant?: IAssistantEditModalValues
assistant?: IAssistantEditModalValues,
) => {
if (!openaiInstance || !app) {
return;
@ -253,8 +252,8 @@ const AssistantManager = ({
// setActiveAssistant(assistant);
new AssistantEditModal({
app,
title: "Create New Assistant",
submitButtonText: "Create",
title: 'Create New Assistant',
submitButtonText: 'Create',
previousValues: assistant,
onSubmit: createAssistant,
}).open();
@ -270,7 +269,6 @@ const AssistantManager = ({
const assistantFiles: string[] = [];
values.files.forEach((file) => {
if (file.id) {
assistantFiles.push(file.id);
} else if (file.filename) {
@ -285,7 +283,7 @@ const AssistantManager = ({
if (uploadedFile) {
assistantFiles.push(uploadedFile.id);
}
})
}),
);
await openaiInstance.beta.assistants
@ -303,7 +301,7 @@ const AssistantManager = ({
return res;
}
return assistant;
})
}),
);
});
};
@ -316,16 +314,16 @@ const AssistantManager = ({
const files = await getAssistantFiles();
const previousValues = {
name: activeAssistant.name || "",
description: activeAssistant.description || "",
instructions: activeAssistant.instructions || "",
name: activeAssistant.name || '',
description: activeAssistant.description || '',
instructions: activeAssistant.instructions || '',
files: files || [],
};
new AssistantEditModal({
app,
title: "Edit Assistant",
submitButtonText: "Edit",
title: 'Edit Assistant',
submitButtonText: 'Edit',
previousValues,
onSubmit: editAssistant,
}).open();
@ -340,7 +338,7 @@ const AssistantManager = ({
.del(activeAssistant.id)
.then((res) => {
const newAssistantsList = assistants.filter(
(assistant) => assistant.id !== activeAssistant.id
(assistant) => assistant.id !== activeAssistant.id,
);
updateAssistants(newAssistantsList);
@ -349,18 +347,18 @@ const AssistantManager = ({
};
const uploadFileToOpenAI = async (
fileName: string
fileName: string,
): Promise<OpenAI.Files.FileObject | undefined> => {
if (!openaiInstance || !plugin || !app) {
return undefined;
}
const file = await app.vault.adapter.read(fileName);
const blob = new File([file], fileName, { type: "text/markdown" });
const blob = new File([file], fileName, { type: 'text/markdown' });
const returnedFile = await openaiInstance.files
.create({
purpose: "assistants",
purpose: 'assistants',
file: blob,
})
.then((res) => {
@ -404,7 +402,7 @@ const AssistantManager = ({
try {
const assistantFilesResponse =
await openaiInstance.beta.assistants.files.list(
activeAssistant?.id
activeAssistant?.id,
);
const innerFiles: OpenAI.Files.FileObject[] = [];
@ -419,7 +417,7 @@ const AssistantManager = ({
} catch (error) {
console.error(error);
}
})
}),
);
return innerFiles;
@ -433,7 +431,7 @@ const AssistantManager = ({
const assistantOptions = useMemo(() => {
return assistants.map((assistant) => {
return {
label: assistant.name || "",
label: assistant.name || '',
value: assistant.id,
};
});
@ -454,7 +452,7 @@ const AssistantManager = ({
}
const assistant = assistants.find(
(assistant) => assistant.id === assistantId
(assistant) => assistant.id === assistantId,
);
updateActiveAssistant(assistant ?? assistants?.[0]);
@ -465,13 +463,11 @@ const AssistantManager = ({
return;
}
const newActiveThread = threads.find(
(thread) => thread.id === threadId
(thread) => thread.id === threadId,
);
updateActiveThread(newActiveThread ?? activeThread);
};
return (
<div className="chat-top-section-container">
<div className="dropdowns-container">
@ -480,7 +476,7 @@ const AssistantManager = ({
<DropdownSelect
items={assistantOptions}
onChange={onUpdateActiveAssistant}
activeItem={activeAssistant?.id || ""}
activeItem={activeAssistant?.id || ''}
/>
<div className="dropdown-buttons-container">
<button className="create" onClick={deleteAssistant}>
@ -505,7 +501,7 @@ const AssistantManager = ({
<DropdownSelect
items={threadOptions}
onChange={onUpdateActiveThread}
activeItem={activeThread?.id || ""}
activeItem={activeThread?.id || ''}
/>
<div className="dropdown-buttons-container">
<button className="create" onClick={deleteThread}>

View File

@ -1,18 +1,18 @@
import React, { CSSProperties, useEffect, useRef, useState } from "react";
import OpenAI from "openai";
import { ChevronDown, ClipboardCopy } from "lucide-react";
import BeatLoader from "react-spinners/BeatLoader";
import { Tooltip } from "react-tooltip";
import Markdown from "react-markdown";
import { useApp } from "../AppView";
import { createNotice } from "@/utils/Logs";
import { TFile } from "obsidian";
import { ThreadAnnotationFile } from "../types";
import React, { CSSProperties, useEffect, useRef, useState } from 'react';
import OpenAI from 'openai';
import { ChevronDown, ClipboardCopy } from 'lucide-react';
import BeatLoader from 'react-spinners/BeatLoader';
import { Tooltip } from 'react-tooltip';
import Markdown from 'react-markdown';
import { useApp } from '../AppView';
import { createNotice } from '@/utils/Logs';
import { TFile } from 'obsidian';
import { ThreadAnnotationFile } from '../types';
const override: CSSProperties = {
display: "block",
margin: "0 auto",
borderColor: "white",
display: 'block',
margin: '0 auto',
borderColor: 'white',
};
interface ChatboxProps {
@ -40,16 +40,16 @@ const Chatbox = ({ annotationFiles, isResponding, messages }: ChatboxProps) => {
if (messagesContainerRef.current) {
scrollToBottom();
messagesContainerRef.current.addEventListener(
"scroll",
handleScroll
'scroll',
handleScroll,
);
}
return () => {
if (messagesContainerRef.current) {
messagesContainerRef.current.removeEventListener(
"scroll",
handleScroll
'scroll',
handleScroll,
);
}
};
@ -67,11 +67,11 @@ const Chatbox = ({ annotationFiles, isResponding, messages }: ChatboxProps) => {
messages.map((message, index) => (
<div key={index} className={`chat-message ${message.role}`}>
{message.content.map((content, index) => {
if (content.type === "text") {
if (content.type === 'text') {
const getMessageText = () => {
const annotationsTexts =
content.text.annotations.map(
(annotation: any) => annotation.text
(annotation: any) => annotation.text,
);
let text = content.text.value;
@ -80,13 +80,13 @@ const Chatbox = ({ annotationFiles, isResponding, messages }: ChatboxProps) => {
const annotationIndex = index;
const regex = new RegExp(
annotationText,
"g"
'g',
);
text = text.replace(
regex,
`[^${annotationIndex}]`
`[^${annotationIndex}]`,
);
}
},
);
return text;
@ -94,31 +94,31 @@ const Chatbox = ({ annotationFiles, isResponding, messages }: ChatboxProps) => {
const renderAnnotation = (
annotation: any,
index: number
index: number,
) => {
const { file_citation } = annotation;
const fileId = file_citation?.file_id;
const fileName = annotationFiles?.find(
(file) => file.fileId === fileId
(file) => file.fileId === fileId,
)?.fileName;
let quote = file_citation?.quote;
// Check if quote has list markdown syntax
if (quote && quote.includes("- ")) {
quote = quote.replace(/- /g, "\n- ");
if (quote && quote.includes('- ')) {
quote = quote.replace(/- /g, '\n- ');
}
const handleAnnotationClick = () => {
// open new tab and then navigate to fil
console.log(
"handleAnnotationClick",
'handleAnnotationClick',
app,
fileName
fileName,
);
if (app && fileName) {
const file =
app.vault.getAbstractFileByPath(
fileName
fileName,
);
if (file && file instanceof TFile) {
app.workspace.getLeaf().openFile(file);
@ -150,7 +150,7 @@ const Chatbox = ({ annotationFiles, isResponding, messages }: ChatboxProps) => {
const text = getMessageText();
const copyText = () => {
navigator.clipboard.writeText(text);
createNotice("Copied to clipboard!", 2000);
createNotice('Copied to clipboard!', 2000);
};
return (
<div key={index} className="message-content">
@ -160,11 +160,11 @@ const Chatbox = ({ annotationFiles, isResponding, messages }: ChatboxProps) => {
</Markdown>
}
<div className="message-footer">
{message.role === "assistant" && (
{message.role === 'assistant' && (
<div className="copy-icon-container">
<ClipboardCopy
className="copy-icon"
color={"#ffffff"}
color={'#ffffff'}
size={16}
onClick={copyText}
/>
@ -174,8 +174,8 @@ const Chatbox = ({ annotationFiles, isResponding, messages }: ChatboxProps) => {
(annotation: any, index: number) =>
renderAnnotation(
annotation,
index
)
index,
),
)}
</div>
</div>

View File

@ -1,5 +1,5 @@
import React, { useEffect, useRef } from "react";
import { DropdownComponent } from "obsidian";
import React, { useEffect, useRef } from 'react';
import { DropdownComponent } from 'obsidian';
interface ISelectOption {
label: string;
@ -29,7 +29,7 @@ const DropdownSelect: React.FC<DropdownSelectProps> = ({
ref={selectElementRef}
onChange={(e) => onChange(e.target.value)}
value={activeItem}
className={"dropdown-select"}
className={'dropdown-select'}
>
{items.map((item) => (
<option className="" key={item.value} value={item.value}>

View File

@ -1,16 +1,16 @@
import { SendHorizontal } from "lucide-react";
import React, { useState } from "react";
import { SendHorizontal } from 'lucide-react';
import React, { useState } from 'react';
interface MessageInputProps {
onMessageSend: (message: string) => void;
}
const MessageInput: React.FC<MessageInputProps> = ({ onMessageSend }) => {
const [newMessage, setNewMessage] = useState("");
const [newMessage, setNewMessage] = useState('');
const handleSend = () => {
onMessageSend(newMessage);
setNewMessage("");
setNewMessage('');
};
return (

View File

@ -2,14 +2,14 @@ import { App, ButtonComponent, Modal, Notice, Setting } from 'obsidian';
import OpenAI from 'openai';
import { FileSuggest } from '../suggesters/FileSuggester';
import * as yup from 'yup';
import { defaultAssistantInstructions } from '../../../utils/templates'
import { defaultAssistantInstructions } from '../../../utils/templates';
interface AssistantEditModalProps {
app: App,
title: string,
submitButtonText?: string,
previousValues?: IAssistantEditModalValues,
onSubmit: (values: IAssistantEditModalValues) => void,
app: App;
title: string;
submitButtonText?: string;
previousValues?: IAssistantEditModalValues;
onSubmit: (values: IAssistantEditModalValues) => void;
}
export interface IAssistantEditModalValues {
@ -60,7 +60,7 @@ export class AssistantEditModal extends Modal {
new Setting(contentEl)
.setName('Name (required)')
.setDesc('The name of the assistant')
.addText(text => {
.addText((text) => {
text.setPlaceholder('Enter name...')
.onChange((value) => {
this.values.name = value;
@ -74,7 +74,7 @@ export class AssistantEditModal extends Modal {
.setName('Description')
.setDesc('The description of the assistant')
.setClass('form-setting-textarea')
.addTextArea(text => {
.addTextArea((text) => {
text.setPlaceholder('Enter description...')
.onChange((value) => {
this.values.description = value;
@ -88,7 +88,7 @@ export class AssistantEditModal extends Modal {
.setName('Instructions (required)')
.setDesc('The instructions you want the assistant to follow')
.setClass('form-setting-textarea')
.addTextArea(text => {
.addTextArea((text) => {
text.setPlaceholder('Enter instructions...')
.onChange((value) => {
this.values.instructions = value;
@ -101,10 +101,12 @@ export class AssistantEditModal extends Modal {
// Function to add a file to the list
const addFileToList = (fileName: string) => {
// if filename already is in values, replace it. this prevents duplicates and allows for re-uploading
const fileIndex: number = this.values.files && this.values.files.findIndex(file => file.filename === fileName);
const fileIndex: number =
this.values.files &&
this.values.files.findIndex(
(file) => file.filename === fileName,
);
if (fileIndex !== -1) {
this.values.files[fileIndex] = {
filename: fileName,
@ -120,21 +122,24 @@ export class AssistantEditModal extends Modal {
};
const updateFileCountText = () => {
this.fileCountText.setText(`Files Uploaded (${this.values.files.length}/20)`);
}
this.fileCountText.setText(
`Files Uploaded (${this.values.files.length}/20)`,
);
};
const createFileListElement = (fileName: string) => {
const fileDiv = this.fileListDiv.createDiv({ cls: 'file-item' });
fileDiv.createEl('span', { text: fileName });
new ButtonComponent(fileDiv)
.setIcon('trash-2')
.setClass('remove-button')
.onClick(() => {
fileDiv.remove();
// Remove the file from the values object
const file = this.values.files.find(file => file.filename === fileName);
const file = this.values.files.find(
(file) => file.filename === fileName,
);
if (file) {
this.values.files.remove(file);
updateFileCountText();
@ -144,9 +149,11 @@ export class AssistantEditModal extends Modal {
new Setting(contentEl)
.setName(`Files`)
.setDesc('The files uploaded to the assistant (not real-time synced, so you will need to re-upload). Max 20.')
.addSearch(search => {
search.setPlaceholder('Enter file IDs separated by commas...')
.setDesc(
'The files uploaded to the assistant (not real-time synced, so you will need to re-upload). Max 20.',
)
.addSearch((search) => {
search.setPlaceholder('Enter file IDs separated by commas...');
// .onChange((value) => {
// this.values.file_ids.push(value);
@ -160,10 +167,8 @@ export class AssistantEditModal extends Modal {
updateFileCountText();
this.fileListDiv = contentEl.createDiv({ cls: 'file-list' });
// Add the files that were already selected
this.values.files.forEach(file => {
this.values.files.forEach((file) => {
if (file.filename) {
createFileListElement(file.filename);
}
@ -171,7 +176,6 @@ export class AssistantEditModal extends Modal {
}
addSubmitButton(contentEl: HTMLElement) {
const validationSchema = yup.object().shape({
name: yup.string().required('Name is required'),
instructions: yup.string().required('Instructions are required'),
@ -180,7 +184,9 @@ export class AssistantEditModal extends Modal {
const checkRequiredFields = async (): Promise<string[]> => {
try {
await validationSchema.validate(this.values, { abortEarly: false });
await validationSchema.validate(this.values, {
abortEarly: false,
});
return [];
} catch (error) {
if (error instanceof yup.ValidationError) {
@ -201,7 +207,7 @@ export class AssistantEditModal extends Modal {
this.onClose();
this.close();
}
};
const modalFooterEl = contentEl.createDiv({ cls: 'modal-footer' });
new ButtonComponent(modalFooterEl)
.setButtonText(this.submitButtonText)
@ -211,6 +217,5 @@ export class AssistantEditModal extends Modal {
});
}
onClose() {
}
onClose() {}
}

View File

@ -2,18 +2,18 @@ import { App, ButtonComponent, Modal, Notice, Setting } from 'obsidian';
import * as yup from 'yup';
interface ThreadEditModalProps {
app: App,
title: string,
submitButtonText?: string,
previousValues?: IThreadEditModalValues,
onSubmit: (values: IThreadEditModalValues) => void,
app: App;
title: string;
submitButtonText?: string;
previousValues?: IThreadEditModalValues;
onSubmit: (values: IThreadEditModalValues) => void;
}
export interface IThreadEditModalValues {
metadata: {
name: string;
[key: string]: unknown;
},
};
[key: string]: unknown;
}
@ -46,7 +46,7 @@ export class ThreadEditModal extends Modal {
new Setting(contentEl)
.setName('Name')
.setDesc('The name of the thread')
.addText(text => {
.addText((text) => {
text.setPlaceholder('Enter thread name...')
.onChange((value) => {
this.values.metadata.name = value;
@ -54,7 +54,6 @@ export class ThreadEditModal extends Modal {
.setValue(this.values.metadata.name);
});
const validationSchema = yup.object().shape({
metadata: yup.object().shape({
name: yup.string().required('Name is required'),
@ -63,7 +62,9 @@ export class ThreadEditModal extends Modal {
const checkRequiredFields = async (): Promise<string[]> => {
try {
await validationSchema.validate(this.values, { abortEarly: false });
await validationSchema.validate(this.values, {
abortEarly: false,
});
return [];
} catch (error) {
if (error instanceof yup.ValidationError) {
@ -93,6 +94,5 @@ export class ThreadEditModal extends Modal {
});
}
onClose() {
}
onClose() {}
}

View File

@ -1,8 +1,8 @@
// Credits go to Liam's Periodic Notes Plugin: https://github.com/liamcain/obsidian-periodic-notes
import { App, TAbstractFile, TFile } from "obsidian";
import { TextInputSuggest } from "./suggest";
import { get_tfiles_from_folder } from "@/utils/utils";
import { App, TAbstractFile, TFile } from 'obsidian';
import { TextInputSuggest } from './suggest';
import { get_tfiles_from_folder } from '@/utils/utils';
export class FileSuggest extends TextInputSuggest<TFile> {
private onSelect: (file: string) => void;
@ -10,8 +10,7 @@ export class FileSuggest extends TextInputSuggest<TFile> {
constructor(
app: App,
public inputEl: HTMLInputElement,
onSelect: (file: string) => void
onSelect: (file: string) => void,
) {
super(app, inputEl);
this.onSelect = onSelect;
@ -30,7 +29,7 @@ export class FileSuggest extends TextInputSuggest<TFile> {
all_files.forEach((file: TAbstractFile) => {
if (
file instanceof TFile &&
file.extension === "md" &&
file.extension === 'md' &&
file.path.toLowerCase().contains(lower_input_str)
) {
files.push(file);

View File

@ -1,8 +1,8 @@
// Credits go to Liam's Periodic Notes Plugin: https://github.com/liamcain/obsidian-periodic-notes
import { ISuggestOwner, Scope } from "obsidian";
import { createPopper, Instance as PopperInstance } from "@popperjs/core";
import { App } from "obsidian";
import { ISuggestOwner, Scope } from 'obsidian';
import { createPopper, Instance as PopperInstance } from '@popperjs/core';
import { App } from 'obsidian';
const wrapAround = (value: number, size: number): number => {
return ((value % size) + size) % size;
@ -18,37 +18,37 @@ class Suggest<T> {
constructor(
owner: ISuggestOwner<T>,
containerEl: HTMLElement,
scope: Scope
scope: Scope,
) {
this.owner = owner;
this.containerEl = containerEl;
containerEl.on(
"click",
".suggestion-item",
this.onSuggestionClick.bind(this)
'click',
'.suggestion-item',
this.onSuggestionClick.bind(this),
);
containerEl.on(
"mousemove",
".suggestion-item",
this.onSuggestionMouseover.bind(this)
'mousemove',
'.suggestion-item',
this.onSuggestionMouseover.bind(this),
);
scope.register([], "ArrowUp", (event) => {
scope.register([], 'ArrowUp', (event) => {
if (!event.isComposing) {
this.setSelectedItem(this.selectedItem - 1, true);
return false;
}
});
scope.register([], "ArrowDown", (event) => {
scope.register([], 'ArrowDown', (event) => {
if (!event.isComposing) {
this.setSelectedItem(this.selectedItem + 1, true);
return false;
}
});
scope.register([], "Enter", (event) => {
scope.register([], 'Enter', (event) => {
if (!event.isComposing) {
this.useSelectedItem(event);
return false;
@ -74,7 +74,7 @@ class Suggest<T> {
const suggestionEls: HTMLDivElement[] = [];
values.forEach((value) => {
const suggestionEl = this.containerEl.createDiv("suggestion-item");
const suggestionEl = this.containerEl.createDiv('suggestion-item');
this.owner.renderSuggestion(value, suggestionEl);
suggestionEls.push(suggestionEl);
});
@ -94,13 +94,13 @@ class Suggest<T> {
setSelectedItem(selectedIndex: number, scrollIntoView: boolean) {
const normalizedIndex = wrapAround(
selectedIndex,
this.suggestions.length
this.suggestions.length,
);
const prevSelectedSuggestion = this.suggestions[this.selectedItem];
const selectedSuggestion = this.suggestions[normalizedIndex];
prevSelectedSuggestion?.removeClass("is-selected");
selectedSuggestion?.addClass("is-selected");
prevSelectedSuggestion?.removeClass('is-selected');
selectedSuggestion?.addClass('is-selected');
this.selectedItem = normalizedIndex;
@ -124,21 +124,21 @@ export abstract class TextInputSuggest<T> implements ISuggestOwner<T> {
this.inputEl = inputEl;
this.scope = new Scope();
this.suggestEl = createDiv("suggestion-container");
const suggestion = this.suggestEl.createDiv("suggestion");
this.suggestEl = createDiv('suggestion-container');
const suggestion = this.suggestEl.createDiv('suggestion');
this.suggest = new Suggest(this, suggestion, this.scope);
this.scope.register([], "Escape", this.close.bind(this));
this.scope.register([], 'Escape', this.close.bind(this));
this.inputEl.addEventListener("input", this.onInputChanged.bind(this));
this.inputEl.addEventListener("focus", this.onInputChanged.bind(this));
this.inputEl.addEventListener("blur", this.close.bind(this));
this.inputEl.addEventListener('input', this.onInputChanged.bind(this));
this.inputEl.addEventListener('focus', this.onInputChanged.bind(this));
this.inputEl.addEventListener('blur', this.close.bind(this));
this.suggestEl.on(
"mousedown",
".suggestion-container",
'mousedown',
'.suggestion-container',
(event: MouseEvent) => {
event.preventDefault();
}
},
);
}
@ -166,10 +166,10 @@ export abstract class TextInputSuggest<T> implements ISuggestOwner<T> {
container.appendChild(this.suggestEl);
this.popper = createPopper(inputEl, this.suggestEl, {
placement: "bottom-start",
placement: 'bottom-start',
modifiers: [
{
name: "sameWidth",
name: 'sameWidth',
enabled: true,
fn: ({ state, instance }) => {
// Note: positioning needs to be calculated twice -
@ -183,8 +183,8 @@ export abstract class TextInputSuggest<T> implements ISuggestOwner<T> {
state.styles.popper.width = targetWidth;
instance.update();
},
phase: "beforeWrite",
requires: ["computeStyles"],
phase: 'beforeWrite',
requires: ['computeStyles'],
},
],
});

View File

@ -1,9 +1,9 @@
import OpenAI from "openai";
import OpenAI from 'openai';
export interface IThread extends OpenAI.Beta.Thread {
metadata: {
name: string;
annotationFiles?: ThreadAnnotationFile[];
}
};
}
export interface ThreadAnnotationFile {

View File

@ -1,4 +1,4 @@
import { Notice } from "obsidian";
import { Notice } from 'obsidian';
export const createNotice = (message: string, timeout = 5000): void => {
new Notice(`Obsidian Intelligence: ${message}`, timeout);

View File

@ -1,7 +1,10 @@
import { log_error } from "./log";
import { log_error } from './log';
export class TemplaterError extends Error {
constructor(msg: string, public console_msg?: string) {
constructor(
msg: string,
public console_msg?: string,
) {
super(msg);
this.name = this.constructor.name;
Error.captureStackTrace(this, this.constructor);
@ -10,7 +13,7 @@ export class TemplaterError extends Error {
export async function errorWrapper<T>(
fn: () => Promise<T>,
msg: string
msg: string,
): Promise<T> {
try {
return await fn();

View File

@ -1,20 +1,20 @@
import { Notice } from "obsidian";
import { TemplaterError } from "./error";
import { Notice } from 'obsidian';
import { TemplaterError } from './error';
export function log_update(msg: string): void {
const notice = new Notice("", 15000);
const notice = new Notice('', 15000);
// TODO: Find better way for this
// @ts-ignore
notice.noticeEl.innerHTML = `<b>obsidian-agents update</b>:<br/>${msg}`;
}
export function log_error(e: Error | TemplaterError): void {
const notice = new Notice("", 8000);
const notice = new Notice('', 8000);
if (e instanceof TemplaterError && e.console_msg) {
// TODO: Find a better way for this
// @ts-ignore
notice.noticeEl.innerHTML = `<b>obsidian-agents Error</b>:<br/>${e.message}<br/>Check console for more information`;
console.error(`obsidian-agents Error:`, e.message, "\n", e.console_msg);
console.error(`obsidian-agents Error:`, e.message, '\n', e.console_msg);
} else {
// @ts-ignore
notice.noticeEl.innerHTML = `<b>obsidian-agents Error</b>:<br/>${e.message}`;

View File

@ -1,4 +1,4 @@
import { TemplaterError } from "./error";
import { TemplaterError } from './error';
import {
App,
normalizePath,
@ -6,14 +6,14 @@ import {
TFile,
TFolder,
Vault,
} from "obsidian";
} from 'obsidian';
export function delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export function escape_RegExp(str: string): string {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}
export function generate_command_regex(): RegExp {
@ -52,13 +52,19 @@ export function resolve_tfile(file_str: string): TFile {
return file;
}
export function get_tfiles_from_folder(folder_str: string, extension?: string): Array<TFile> {
export function get_tfiles_from_folder(
folder_str: string,
extension?: string,
): Array<TFile> {
const folder = resolve_tfolder(folder_str);
const files: Array<TFile> = [];
Vault.recurseChildren(folder, (file: TAbstractFile) => {
if (file instanceof TFile && (extension && file.extension === extension)) {
if (
file instanceof TFile &&
extension &&
file.extension === extension
) {
files.push(file);
}
});
@ -73,7 +79,7 @@ export function get_tfiles_from_folder(folder_str: string, extension?: string):
export function arraymove<T>(
arr: T[],
fromIndex: number,
toIndex: number
toIndex: number,
): void {
if (toIndex < 0 || toIndex === arr.length) {
return;

View File

@ -35,7 +35,6 @@
justify-content: center;
height: 100%;
width: 100%;
}
.scroll-to-bottom-button {
@ -88,7 +87,6 @@
padding-bottom: 8px;
}
.copy-icon-container {
padding-top: 8px;
}
@ -125,7 +123,6 @@
width: 15%;
}
.collapsible-container {
width: 100%;
max-height: 25%;
@ -160,7 +157,6 @@
flex-grow: 1;
}
.files-list {
display: flex;
flex-direction: column;
@ -181,7 +177,6 @@
gap: 4px;
}
/* AssistantManager.tsx */
.chat-top-section-container {

View File

@ -13,18 +13,10 @@
"isolatedModules": true,
"jsx": "react",
"strictNullChecks": true,
"lib": [
"DOM",
"ES5",
"ES6",
"ES7"
],
"lib": ["DOM", "ES5", "ES6", "ES7"],
"paths": {
"@/*": ["src/*"]
}
},
"include": [
"**/*.ts",
"src/ui/AppView.tsx"
]
"include": ["**/*.ts", "src/ui/AppView.tsx"]
}

View File

@ -1,14 +1,14 @@
import { readFileSync, writeFileSync } from "fs";
import { readFileSync, writeFileSync } from 'fs';
const targetVersion = process.env.npm_package_version;
// read minAppVersion from manifest.json and bump version to target version
let manifest = JSON.parse(readFileSync("manifest.json", "utf8"));
let manifest = JSON.parse(readFileSync('manifest.json', 'utf8'));
const { minAppVersion } = manifest;
manifest.version = targetVersion;
writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t"));
writeFileSync('manifest.json', JSON.stringify(manifest, null, '\t'));
// update versions.json with target version and minAppVersion from manifest.json
let versions = JSON.parse(readFileSync("versions.json", "utf8"));
let versions = JSON.parse(readFileSync('versions.json', 'utf8'));
versions[targetVersion] = minAppVersion;
writeFileSync("versions.json", JSON.stringify(versions, null, "\t"));
writeFileSync('versions.json', JSON.stringify(versions, null, '\t'));