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,22 +2,20 @@
"root": true, "root": true,
"parser": "@typescript-eslint/parser", "parser": "@typescript-eslint/parser",
"env": { "node": true }, "env": { "node": true },
"plugins": [ "plugins": ["@typescript-eslint"],
"@typescript-eslint"
],
"extends": [ "extends": [
"eslint:recommended", "eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended" "plugin:@typescript-eslint/recommended"
], ],
"parserOptions": { "parserOptions": {
"sourceType": "module" "sourceType": "module"
}, },
"rules": { "rules": {
"no-unused-vars": "off", "no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
"@typescript-eslint/ban-ts-comment": "off", "@typescript-eslint/ban-ts-comment": "off",
"no-prototype-builtins": "off", "no-prototype-builtins": "off",
"@typescript-eslint/no-empty-function": "off" "@typescript-eslint/no-empty-function": "off"
} }
} }

5
.prettierrc Normal file
View File

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

View File

@ -1,3 +1,3 @@
# Obsidian Intelligence # Obsidian Intelligence
AI-powered assistants trained on your notes AI-powered assistants trained on your notes

View File

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

145
main.ts
View File

@ -4,103 +4,108 @@ import OpenAI from 'openai';
import { IThread } from './src/ui/types'; import { IThread } from './src/ui/types';
interface ObsidianIntelligenceSettings { interface ObsidianIntelligenceSettings {
openaiKey: string; openaiKey: string;
threads: IThread[]; threads: IThread[];
activeThread: IThread | undefined; activeThread: IThread | undefined;
activeAssistant: OpenAI.Beta.Assistant | undefined; activeAssistant: OpenAI.Beta.Assistant | undefined;
activeAssistantFiles: OpenAI.Files.FileObject[] | undefined; activeAssistantFiles: OpenAI.Files.FileObject[] | undefined;
} }
const DEFAULT_SETTINGS: ObsidianIntelligenceSettings = { const DEFAULT_SETTINGS: ObsidianIntelligenceSettings = {
openaiKey: '', openaiKey: '',
threads: [], threads: [],
activeThread: undefined, activeThread: undefined,
activeAssistant: undefined, activeAssistant: undefined,
activeAssistantFiles: undefined, activeAssistantFiles: undefined,
} };
export default class ObsidianIntelligence extends Plugin { export default class ObsidianIntelligence extends Plugin {
settings: ObsidianIntelligenceSettings; settings: ObsidianIntelligenceSettings;
view: AppView; view: AppView;
async onload() { async onload() {
await this.loadSettings(); await this.loadSettings();
this.registerView( this.registerView(VIEW_TYPE, (leaf) => new AppView(leaf, this));
VIEW_TYPE,
(leaf) => (new AppView(leaf, this)) const ribbonIconEl = this.addRibbonIcon(
'bot',
'Open Obsidian Intelligence',
(evt: MouseEvent) => {
this.activateView();
},
); );
// Perform additional things with the ribbon
ribbonIconEl.addClass('my-plugin-ribbon-class');
const ribbonIconEl = this.addRibbonIcon('bot', 'Open Obsidian Intelligence', (evt: MouseEvent) => { // This adds a status bar item to the bottom of the app. Does not work on mobile apps.
this.activateView(); const statusBarItemEl = this.addStatusBarItem();
}); statusBarItemEl.setText('Status Bar Text');
// Perform additional things with the ribbon
ribbonIconEl.addClass('my-plugin-ribbon-class');
// This adds a status bar item to the bottom of the app. Does not work on mobile apps. this.addCommand({
const statusBarItemEl = this.addStatusBarItem(); id: 'obsidian-intelligence-view-open',
statusBarItemEl.setText('Status Bar Text'); name: 'Open Obsidian Intelligence',
hotkeys: [{ modifiers: ['Mod', 'Shift'], key: 'I' }],
this.addCommand({
id: "obsidian-intelligence-view-open",
name: "Open Obsidian Intelligence",
hotkeys: [{ modifiers: ["Mod", "Shift"], key: "I"}],
callback: () => { callback: () => {
this.activateView(); this.activateView();
} },
}); });
// This adds a settings tab so the user can configure various aspects of the plugin // This adds a settings tab so the user can configure various aspects of the plugin
this.addSettingTab(new OISettingTab(this.app, this)); this.addSettingTab(new OISettingTab(this.app, this));
} }
onunload() { onunload() {}
} async loadSettings() {
this.settings = Object.assign(
{},
DEFAULT_SETTINGS,
await this.loadData(),
);
}
async loadSettings() { async saveSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); await this.saveData(this.settings);
} }
async saveSettings() { async activateView() {
await this.saveData(this.settings);
}
async activateView() {
this.app.workspace.detachLeavesOfType(VIEW_TYPE); this.app.workspace.detachLeavesOfType(VIEW_TYPE);
await this.app.workspace.getRightLeaf(false).setViewState({ await this.app.workspace.getRightLeaf(false).setViewState({
type: VIEW_TYPE, type: VIEW_TYPE,
active: true, active: true,
}); });
this.app.workspace.revealLeaf( this.app.workspace.revealLeaf(
this.app.workspace.getLeavesOfType(VIEW_TYPE)[0] this.app.workspace.getLeavesOfType(VIEW_TYPE)[0],
); );
} }
} }
class OISettingTab extends PluginSettingTab { class OISettingTab extends PluginSettingTab {
plugin: ObsidianIntelligence; plugin: ObsidianIntelligence;
constructor(app: App, plugin: ObsidianIntelligence) { constructor(app: App, plugin: ObsidianIntelligence) {
super(app, plugin); super(app, plugin);
this.plugin = plugin; this.plugin = plugin;
} }
display(): void { display(): void {
const {containerEl} = this; const { containerEl } = this;
containerEl.empty(); containerEl.empty();
new Setting(containerEl) new Setting(containerEl)
.setName('OpenAI Key') .setName('OpenAI Key')
.setDesc('Can find it https://platform.openai.com/api-keys') .setDesc('Can find it https://platform.openai.com/api-keys')
.addText(text => text .addText((text) =>
.setPlaceholder('Enter your API Key') text
.setValue(this.plugin.settings.openaiKey) .setPlaceholder('Enter your API Key')
.onChange(async (value) => { .setValue(this.plugin.settings.openaiKey)
this.plugin.settings.openaiKey = value; .onChange(async (value) => {
await this.plugin.saveSettings(); this.plugin.settings.openaiKey = value;
})); await this.plugin.saveSettings();
} }),
} );
}
}

View File

@ -1,11 +1,11 @@
{ {
"id": "obsidian-intelligence", "id": "obsidian-intelligence",
"name": "Obsidian Intelligence", "name": "Obsidian Intelligence",
"version": "1.0.0", "version": "1.0.0",
"minAppVersion": "0.15.0", "minAppVersion": "0.15.0",
"description": "AI-powered assistants inside Obsidian", "description": "AI-powered assistants inside Obsidian",
"author": "John Mavrick", "author": "John Mavrick",
"authorUrl": "https://beacons.ai/johnmavrick", "authorUrl": "https://beacons.ai/johnmavrick",
"fundingUrl": "https://patreon.com/johnmavrick", "fundingUrl": "https://patreon.com/johnmavrick",
"isDesktopOnly": false "isDesktopOnly": false
} }

6404
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,37 +1,39 @@
{ {
"name": "obsidian-intelligence", "name": "obsidian-intelligence",
"version": "1.0.0", "version": "1.0.0",
"description": "AI-powered assistants inside Obsidian", "description": "AI-powered assistants inside Obsidian",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {
"dev": "node esbuild.config.mjs", "dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", "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": "", "keywords": [],
"license": "MIT", "author": "",
"devDependencies": { "license": "MIT",
"@types/node": "^16.11.6", "devDependencies": {
"@typescript-eslint/eslint-plugin": "5.29.0", "@types/node": "^16.11.6",
"@typescript-eslint/parser": "5.29.0", "@typescript-eslint/eslint-plugin": "5.29.0",
"builtin-modules": "3.3.0", "@typescript-eslint/parser": "5.29.0",
"esbuild": "0.17.3", "builtin-modules": "3.3.0",
"obsidian": "latest", "esbuild": "0.17.3",
"tslib": "2.4.0", "obsidian": "latest",
"typescript": "4.7.4" "prettier": "^3.1.0",
}, "tslib": "2.4.0",
"dependencies": { "typescript": "4.7.4"
"@popperjs/core": "^2.11.8", },
"@types/react": "^18.2.37", "dependencies": {
"@types/react-dom": "^18.2.15", "@popperjs/core": "^2.11.8",
"lucide-react": "^0.292.0", "@types/react": "^18.2.37",
"openai": "^4.16.1", "@types/react-dom": "^18.2.15",
"react": "^18.2.0", "lucide-react": "^0.292.0",
"react-dom": "^18.2.0", "openai": "^4.16.1",
"react-markdown": "^9.0.1", "react": "^18.2.0",
"react-spinners": "^0.13.8", "react-dom": "^18.2.0",
"react-tooltip": "^5.23.0", "react-markdown": "^9.0.1",
"yup": "^1.3.2" "react-spinners": "^0.13.8",
} "react-tooltip": "^5.23.0",
"yup": "^1.3.2"
}
} }

View File

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

View File

@ -1,33 +1,33 @@
import { ItemView, WorkspaceLeaf } from "obsidian"; import { ItemView, WorkspaceLeaf } from 'obsidian';
import * as React from "react"; import * as React from 'react';
import { Root, createRoot } from "react-dom/client"; import { Root, createRoot } from 'react-dom/client';
import PluginView from "./PluginView"; import PluginView from './PluginView';
import { App } from "obsidian"; import { App } from 'obsidian';
import ObsidianIntelligence from "../../main"; import ObsidianIntelligence from '../../main';
import OpenAI from "openai"; 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 AppContext = React.createContext<App | undefined>(undefined);
export const PluginContext = React.createContext<ObsidianIntelligence | undefined>( export const PluginContext = React.createContext<
undefined ObsidianIntelligence | undefined
); >(undefined);
export const OpenAIContext = React.createContext<OpenAI | undefined>(undefined); export const OpenAIContext = React.createContext<OpenAI | undefined>(undefined);
// export const CommandsContext = React.createContext<ICommandPayload | undefined>(undefined); // export const CommandsContext = React.createContext<ICommandPayload | undefined>(undefined);
export const useApp = (): App | undefined => { export const useApp = (): App | undefined => {
return React.useContext(AppContext); return React.useContext(AppContext);
}; };
export const usePlugin = (): ObsidianIntelligence | undefined => { export const usePlugin = (): ObsidianIntelligence | undefined => {
return React.useContext(PluginContext); return React.useContext(PluginContext);
}; };
export const useOpenAI = (): OpenAI | undefined => { export const useOpenAI = (): OpenAI | undefined => {
return React.useContext(OpenAIContext); return React.useContext(OpenAIContext);
}; };
// export const useCommands = (): ICommandPayload | undefined => { // export const useCommands = (): ICommandPayload | undefined => {
@ -35,49 +35,49 @@ export const useOpenAI = (): OpenAI | undefined => {
// }; // };
export class AppView extends ItemView { export class AppView extends ItemView {
root: Root | null = null; root: Root | null = null;
plugin: ObsidianIntelligence; plugin: ObsidianIntelligence;
openAI: OpenAI; openAI: OpenAI;
// commands: ICommandPayload | undefined; // commands: ICommandPayload | undefined;
constructor(leaf: WorkspaceLeaf, plugin: ObsidianIntelligence) { constructor(leaf: WorkspaceLeaf, plugin: ObsidianIntelligence) {
super(leaf); super(leaf);
this.plugin = plugin; this.plugin = plugin;
const openaiKey = plugin.settings.openaiKey; const openaiKey = plugin.settings.openaiKey;
const openAIInstance = new OpenAI({ const openAIInstance = new OpenAI({
apiKey: openaiKey, apiKey: openaiKey,
dangerouslyAllowBrowser: true, dangerouslyAllowBrowser: true,
}); });
this.openAI = openAIInstance; this.openAI = openAIInstance;
// this.addCommands(); // this.addCommands();
} }
getViewType() { getViewType() {
return VIEW_TYPE; return VIEW_TYPE;
} }
getDisplayText() { getDisplayText() {
return "Obsidian Intelligence"; return 'Obsidian Intelligence';
} }
getIcon(): string { getIcon(): string {
return "bot"; return 'bot';
} }
async onOpen() { async onOpen() {
this.root = createRoot(this.containerEl.children[1]); this.root = createRoot(this.containerEl.children[1]);
this.root.render( this.root.render(
<AppContext.Provider value={this.app}> <AppContext.Provider value={this.app}>
<PluginContext.Provider value={this.plugin}> <PluginContext.Provider value={this.plugin}>
<OpenAIContext.Provider value={this.openAI}> <OpenAIContext.Provider value={this.openAI}>
{/* <CommandsContext.Provider value={this.commands}> */} {/* <CommandsContext.Provider value={this.commands}> */}
<PluginView /> <PluginView />
{/* </CommandsContext.Provider> */} {/* </CommandsContext.Provider> */}
</OpenAIContext.Provider> </OpenAIContext.Provider>
</PluginContext.Provider> </PluginContext.Provider>
</AppContext.Provider> </AppContext.Provider>,
); );
} }
async onClose() { async onClose() {
this.root?.unmount(); this.root?.unmount();
} }
} }

View File

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

View File

@ -1,527 +1,523 @@
import React, { useEffect, useMemo } from "react"; import React, { useEffect, useMemo } from 'react';
import OpenAI from "openai"; import OpenAI from 'openai';
import { useApp, useOpenAI, usePlugin } from "../AppView"; import { useApp, useOpenAI, usePlugin } from '../AppView';
import { IThread } from "../types"; import { IThread } from '../types';
import DropdownSelect from "./DropdownSelect"; import DropdownSelect from './DropdownSelect';
import { MarkdownView } from "obsidian"; import { MarkdownView } from 'obsidian';
import { Bot, MessageSquare, Plus, Pencil, Trash2 } from "lucide-react"; import { Bot, MessageSquare, Plus, Pencil, Trash2 } from 'lucide-react';
import { import {
AssistantEditModal, AssistantEditModal,
IAssistantEditModalValues, IAssistantEditModalValues,
} from "./modals/AssistantEditModal"; } from './modals/AssistantEditModal';
import {} from "./modals/AssistantEditModal"; import {} from './modals/AssistantEditModal';
import { import {
ThreadEditModal, ThreadEditModal,
IThreadEditModalValues, IThreadEditModalValues,
} from "./modals/ThreadEditModal"; } from './modals/ThreadEditModal';
import { createNotice } from "@/utils/Logs"; import { createNotice } from '@/utils/Logs';
import { defaultAssistantInstructions } from "@/utils/templates"; import { defaultAssistantInstructions } from '@/utils/templates';
interface AssistantManagerProps { interface AssistantManagerProps {
assistants: OpenAI.Beta.Assistant[]; assistants: OpenAI.Beta.Assistant[];
updateAssistants: (assistants: OpenAI.Beta.Assistant[]) => void; updateAssistants: (assistants: OpenAI.Beta.Assistant[]) => void;
threads: IThread[]; threads: IThread[];
updateThreads: (threads: IThread[]) => void; updateThreads: (threads: IThread[]) => void;
activeAssistant: OpenAI.Beta.Assistant | undefined; activeAssistant: OpenAI.Beta.Assistant | undefined;
updateActiveAssistant: (assistant: OpenAI.Beta.Assistant) => void; updateActiveAssistant: (assistant: OpenAI.Beta.Assistant) => void;
activeThread: IThread | undefined; activeThread: IThread | undefined;
updateActiveThread: (thread: IThread) => void; updateActiveThread: (thread: IThread) => void;
} }
const AssistantManager = ({ const AssistantManager = ({
assistants, assistants,
updateAssistants, updateAssistants,
threads, threads,
updateThreads, updateThreads,
activeAssistant, activeAssistant,
activeThread, activeThread,
updateActiveAssistant, updateActiveAssistant,
updateActiveThread, updateActiveThread,
}: AssistantManagerProps) => { }: AssistantManagerProps) => {
const app = useApp(); const app = useApp();
const plugin = usePlugin(); const plugin = usePlugin();
const openaiInstance = useOpenAI(); const openaiInstance = useOpenAI();
useEffect(() => { useEffect(() => {
if (!plugin || !app) { if (!plugin || !app) {
return; return;
} }
plugin.addCommand({ plugin.addCommand({
id: "create-assistant-from-active-note", id: 'create-assistant-from-active-note',
name: "Create Assistant from Active Note", name: 'Create Assistant from Active Note',
checkCallback: (checking: boolean) => { checkCallback: (checking: boolean) => {
// Conditions to check // Conditions to check
const markdownView = const markdownView =
app.workspace.getActiveViewOfType(MarkdownView); app.workspace.getActiveViewOfType(MarkdownView);
const openFile = markdownView?.file; const openFile = markdownView?.file;
if (openFile) { if (openFile) {
if (!checking) { if (!checking) {
// const links = app.metadataCache.getFileCache(openFile)?.links?.map( // const links = app.metadataCache.getFileCache(openFile)?.links?.map(
// (link) => addFileType(truncateLink(link.link)) // (link) => addFileType(truncateLink(link.link))
// ) || []; // ) || [];
const links = Object.keys( 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
).map((file) => file);
console.log(
"file metadata cache",
app.metadataCache.getCache(openFile.path)?.links
);
const currentFile = openFile.path;
const filesToUpload = new Set([
currentFile,
...links,
...backlinks,
]);
handleCreateAssistant({
name: `${openFile.path} Assistant`,
instructions: defaultAssistantInstructions,
files: Array.from(filesToUpload)
.filter((file) => file.endsWith(".md"))
.map((file) => ({
filename: file,
})),
});
}
// This command will only show up in Command Palette when the check function returns true
return true;
}
},
});
}, [plugin]);
useEffect(() => {}, [plugin]); const backlinks = Object.keys(
//@ts-ignore
app.metadataCache.getBacklinksForFile(openFile)
.data,
).map((file) => file);
console.log(
'file metadata cache',
app.metadataCache.getCache(openFile.path)?.links,
);
const currentFile = openFile.path;
const filesToUpload = new Set([
currentFile,
...links,
...backlinks,
]);
const createThread = async () => { handleCreateAssistant({
if (!openaiInstance || !plugin) { name: `${openFile.path} Assistant`,
return; instructions: defaultAssistantInstructions,
} files: Array.from(filesToUpload)
.filter((file) => file.endsWith('.md'))
.map((file) => ({
filename: file,
})),
});
}
// This command will only show up in Command Palette when the check function returns true
return true;
}
},
});
}, [plugin]);
const newThreadName = "New Thread"; useEffect(() => {}, [plugin]);
await openaiInstance.beta.threads const createThread = async () => {
.create({ if (!openaiInstance || !plugin) {
metadata: { return;
name: newThreadName, }
},
})
.then((res) => {
const newThread = {
...res,
metadata: {
name: newThreadName,
},
};
updateThreads([...threads, newThread]);
updateActiveThread(newThread);
plugin.saveSettings();
});
};
const editThread = async (values: IThreadEditModalValues) => { const newThreadName = 'New Thread';
if (!openaiInstance || !app || !activeThread) {
return;
}
const newThread = { await openaiInstance.beta.threads
...activeThread, .create({
metadata: { metadata: {
name: values.metadata.name, name: newThreadName,
}, },
}; })
.then((res) => {
const newThread = {
...res,
metadata: {
name: newThreadName,
},
};
updateThreads([...threads, newThread]);
updateActiveThread(newThread);
plugin.saveSettings();
});
};
await openaiInstance.beta.threads const editThread = async (values: IThreadEditModalValues) => {
.update(activeThread.id, { if (!openaiInstance || !app || !activeThread) {
metadata: { return;
name: values.metadata.name, }
},
})
.then((res) => {
updateThreads(
threads.map((thread) => {
if (thread.id === activeThread.id) {
return newThread;
}
return thread;
})
);
updateActiveThread(newThread);
});
};
const handleEditThread = async () => { const newThread = {
if (!openaiInstance || !app || !activeThread) { ...activeThread,
return; metadata: {
} name: values.metadata.name,
},
};
// Get the previous values of the thread await openaiInstance.beta.threads
const previousValues = { .update(activeThread.id, {
metadata: { metadata: {
name: activeThread.metadata.name || "", name: values.metadata.name,
}, },
}; })
.then((res) => {
updateThreads(
threads.map((thread) => {
if (thread.id === activeThread.id) {
return newThread;
}
return thread;
}),
);
updateActiveThread(newThread);
});
};
new ThreadEditModal({ const handleEditThread = async () => {
app, if (!openaiInstance || !app || !activeThread) {
title: "Edit Thread", return;
submitButtonText: "Edit", }
previousValues,
onSubmit: editThread,
}).open();
};
const deleteThread = async () => { // Get the previous values of the thread
if (!openaiInstance || !plugin || !activeThread) { const previousValues = {
return; metadata: {
} name: activeThread.metadata.name || '',
},
};
await openaiInstance.beta.threads new ThreadEditModal({
.del(activeThread.id) app,
.then((res) => { title: 'Edit Thread',
const newThreadsList = threads.filter( submitButtonText: 'Edit',
(thread) => thread.id !== activeThread.id previousValues,
); onSubmit: editThread,
updateThreads(newThreadsList); }).open();
updateActiveThread(newThreadsList?.[0]); };
plugin.saveSettings();
})
.catch((error) => {
console.error(error);
});
};
const createAssistant = async (values: IAssistantEditModalValues) => { const deleteThread = async () => {
if (!openaiInstance || !app) { if (!openaiInstance || !plugin || !activeThread) {
return; return;
} }
// use uploadFileToOpenAI to upload files to openai await openaiInstance.beta.threads
const uploadedFiles: string[] = []; .del(activeThread.id)
await Promise.all( .then((res) => {
values.files.map(async (file) => { const newThreadsList = threads.filter(
if (file.filename) { (thread) => thread.id !== activeThread.id,
const uploadedFile = await uploadFileToOpenAI( );
file?.filename updateThreads(newThreadsList);
); updateActiveThread(newThreadsList?.[0]);
plugin.saveSettings();
if (uploadedFile) { })
.catch((error) => {
uploadedFiles.push(uploadedFile.id); console.error(error);
} });
} };
})
);
const createAssistant = async (values: IAssistantEditModalValues) => {
await openaiInstance.beta.assistants if (!openaiInstance || !app) {
.create({ return;
name: values.name, }
description: values.description,
instructions: values.instructions,
tools: [{ type: "code_interpreter" }, { type: "retrieval" }],
model: "gpt-4-1106-preview",
file_ids: uploadedFiles,
})
.then((res) => {
createNotice(`Assistant "${values.name}" created`);
updateAssistants([...assistants, res]);
updateActiveAssistant(res);
});
};
const handleCreateAssistant = async ( // use uploadFileToOpenAI to upload files to openai
assistant?: IAssistantEditModalValues const uploadedFiles: string[] = [];
) => { await Promise.all(
if (!openaiInstance || !app) { values.files.map(async (file) => {
return; if (file.filename) {
} const uploadedFile = await uploadFileToOpenAI(
file?.filename,
// const assistant = await openaiInstance.beta.assistants.create({ );
// name: "Math Tutor",
// instructions:
// "You are a personal math tutor. Write and run code to answer math questions.",
// tools: [{ type: "code_interpreter" }],
// model: "gpt-4-1106-preview",
// });
// setActiveAssistant(assistant);
new AssistantEditModal({
app,
title: "Create New Assistant",
submitButtonText: "Create",
previousValues: assistant,
onSubmit: createAssistant,
}).open();
};
const editAssistant = async (values: IAssistantEditModalValues) => { if (uploadedFile) {
if (!openaiInstance || !app || !activeAssistant) { uploadedFiles.push(uploadedFile.id);
return; }
} }
}),
);
// Check each values.files element to see if there is an id. If there is, then it is already uploaded to openai and we don't need to upload it again. await openaiInstance.beta.assistants
const filesToUpload: string[] = []; .create({
const assistantFiles: string[] = []; name: values.name,
description: values.description,
instructions: values.instructions,
tools: [{ type: 'code_interpreter' }, { type: 'retrieval' }],
model: 'gpt-4-1106-preview',
file_ids: uploadedFiles,
})
.then((res) => {
createNotice(`Assistant "${values.name}" created`);
updateAssistants([...assistants, res]);
updateActiveAssistant(res);
});
};
values.files.forEach((file) => { const handleCreateAssistant = async (
assistant?: IAssistantEditModalValues,
if (file.id) { ) => {
assistantFiles.push(file.id); if (!openaiInstance || !app) {
} else if (file.filename) { return;
filesToUpload.push(file.filename); }
}
});
await Promise.all( // const assistant = await openaiInstance.beta.assistants.create({
filesToUpload.map(async (file) => { // name: "Math Tutor",
const uploadedFile = await uploadFileToOpenAI(file); // instructions:
// "You are a personal math tutor. Write and run code to answer math questions.",
// tools: [{ type: "code_interpreter" }],
// model: "gpt-4-1106-preview",
// });
// setActiveAssistant(assistant);
new AssistantEditModal({
app,
title: 'Create New Assistant',
submitButtonText: 'Create',
previousValues: assistant,
onSubmit: createAssistant,
}).open();
};
if (uploadedFile) { const editAssistant = async (values: IAssistantEditModalValues) => {
assistantFiles.push(uploadedFile.id); if (!openaiInstance || !app || !activeAssistant) {
} return;
}) }
);
await openaiInstance.beta.assistants // Check each values.files element to see if there is an id. If there is, then it is already uploaded to openai and we don't need to upload it again.
.update(activeAssistant.id, { const filesToUpload: string[] = [];
name: values.name, const assistantFiles: string[] = [];
description: values.description,
instructions: values.instructions,
file_ids: assistantFiles,
})
.then((res) => {
updateActiveAssistant(res);
updateAssistants(
assistants.map((assistant) => {
if (assistant.id === activeAssistant.id) {
return res;
}
return assistant;
})
);
});
};
const handleEditAssistant = async () => { values.files.forEach((file) => {
if (!openaiInstance || !app || !activeAssistant) { if (file.id) {
return; assistantFiles.push(file.id);
} } else if (file.filename) {
filesToUpload.push(file.filename);
}
});
const files = await getAssistantFiles(); await Promise.all(
filesToUpload.map(async (file) => {
const uploadedFile = await uploadFileToOpenAI(file);
const previousValues = { if (uploadedFile) {
name: activeAssistant.name || "", assistantFiles.push(uploadedFile.id);
description: activeAssistant.description || "", }
instructions: activeAssistant.instructions || "", }),
files: files || [], );
};
new AssistantEditModal({ await openaiInstance.beta.assistants
app, .update(activeAssistant.id, {
title: "Edit Assistant", name: values.name,
submitButtonText: "Edit", description: values.description,
previousValues, instructions: values.instructions,
onSubmit: editAssistant, file_ids: assistantFiles,
}).open(); })
}; .then((res) => {
updateActiveAssistant(res);
updateAssistants(
assistants.map((assistant) => {
if (assistant.id === activeAssistant.id) {
return res;
}
return assistant;
}),
);
});
};
const deleteAssistant = async () => { const handleEditAssistant = async () => {
if (!openaiInstance || !activeAssistant) { if (!openaiInstance || !app || !activeAssistant) {
return; return;
} }
await openaiInstance.beta.assistants const files = await getAssistantFiles();
.del(activeAssistant.id)
.then((res) => {
const newAssistantsList = assistants.filter(
(assistant) => assistant.id !== activeAssistant.id
);
updateAssistants(newAssistantsList); const previousValues = {
updateActiveAssistant(newAssistantsList?.[0]); name: activeAssistant.name || '',
}); description: activeAssistant.description || '',
}; instructions: activeAssistant.instructions || '',
files: files || [],
};
const uploadFileToOpenAI = async ( new AssistantEditModal({
fileName: string app,
): Promise<OpenAI.Files.FileObject | undefined> => { title: 'Edit Assistant',
if (!openaiInstance || !plugin || !app) { submitButtonText: 'Edit',
return undefined; previousValues,
} onSubmit: editAssistant,
}).open();
};
const file = await app.vault.adapter.read(fileName); const deleteAssistant = async () => {
const blob = new File([file], fileName, { type: "text/markdown" }); if (!openaiInstance || !activeAssistant) {
return;
}
const returnedFile = await openaiInstance.files await openaiInstance.beta.assistants
.create({ .del(activeAssistant.id)
purpose: "assistants", .then((res) => {
file: blob, const newAssistantsList = assistants.filter(
}) (assistant) => assistant.id !== activeAssistant.id,
.then((res) => { );
return res;
})
.catch((error) => {
console.error(error);
return undefined;
});
return returnedFile; updateAssistants(newAssistantsList);
}; updateActiveAssistant(newAssistantsList?.[0]);
});
};
// const attachFileToAssistant = async (assistantId: string, fileId: string) => { const uploadFileToOpenAI = async (
// //file id is file-CFBYJh1WUxRWdAtdaScVZHS7 fileName: string,
// //assistant id is asst_0HHHYCL2dgImUlXbZKDRjac0 ): Promise<OpenAI.Files.FileObject | undefined> => {
// if (!openaiInstance || !plugin) { if (!openaiInstance || !plugin || !app) {
// return; return undefined;
// } }
// await openaiInstance.beta.assistants.files.create(
// assistantId,
// {
// file_id: fileId,
// }
// ).then((res) => {
// // Handle the response
// }).catch((error) => { const file = await app.vault.adapter.read(fileName);
// // Handle the error const blob = new File([file], fileName, { type: 'text/markdown' });
// console.error(error);
// });
// };
const getAssistantFiles = async (): Promise< const returnedFile = await openaiInstance.files
OpenAI.Files.FileObject[] | [] .create({
> => { purpose: 'assistants',
if (!openaiInstance || !plugin || !activeAssistant) { file: blob,
return []; })
} .then((res) => {
return res;
})
.catch((error) => {
console.error(error);
return undefined;
});
try { return returnedFile;
const assistantFilesResponse = };
await openaiInstance.beta.assistants.files.list(
activeAssistant?.id
);
const innerFiles: OpenAI.Files.FileObject[] = []; // const attachFileToAssistant = async (assistantId: string, fileId: string) => {
// //file id is file-CFBYJh1WUxRWdAtdaScVZHS7
// //assistant id is asst_0HHHYCL2dgImUlXbZKDRjac0
// if (!openaiInstance || !plugin) {
// return;
// }
// await openaiInstance.beta.assistants.files.create(
// assistantId,
// {
// file_id: fileId,
// }
// ).then((res) => {
// // Handle the response
await Promise.all( // }).catch((error) => {
assistantFilesResponse.data.map(async (file) => { // // Handle the error
try { // console.error(error);
const fileInfoResponse = // });
await openaiInstance.files.retrieve(file.id); // };
innerFiles.push(fileInfoResponse); const getAssistantFiles = async (): Promise<
} catch (error) { OpenAI.Files.FileObject[] | []
console.error(error); > => {
} if (!openaiInstance || !plugin || !activeAssistant) {
}) return [];
); }
return innerFiles; try {
} catch (error) { const assistantFilesResponse =
console.error(error); await openaiInstance.beta.assistants.files.list(
return []; activeAssistant?.id,
} );
};
//format assistants into ISelectOption const innerFiles: OpenAI.Files.FileObject[] = [];
const assistantOptions = useMemo(() => {
return assistants.map((assistant) => {
return {
label: assistant.name || "",
value: assistant.id,
};
});
}, [assistants]);
const threadOptions = useMemo(() => { await Promise.all(
return threads.map((thread) => { assistantFilesResponse.data.map(async (file) => {
return { try {
label: thread.metadata.name, const fileInfoResponse =
value: thread.id, await openaiInstance.files.retrieve(file.id);
};
});
}, [threads]);
const onUpdateActiveAssistant = (assistantId: string) => { innerFiles.push(fileInfoResponse);
if (!openaiInstance || !activeAssistant) { } catch (error) {
return; console.error(error);
} }
}),
const assistant = assistants.find( );
(assistant) => assistant.id === assistantId
);
updateActiveAssistant(assistant ?? assistants?.[0]); return innerFiles;
}; } catch (error) {
console.error(error);
return [];
}
};
const onUpdateActiveThread = (threadId: string) => { //format assistants into ISelectOption
if (!openaiInstance || !activeThread) { const assistantOptions = useMemo(() => {
return; return assistants.map((assistant) => {
} return {
const newActiveThread = threads.find( label: assistant.name || '',
(thread) => thread.id === threadId value: assistant.id,
); };
updateActiveThread(newActiveThread ?? activeThread); });
}; }, [assistants]);
const threadOptions = useMemo(() => {
return threads.map((thread) => {
return {
label: thread.metadata.name,
value: thread.id,
};
});
}, [threads]);
return ( const onUpdateActiveAssistant = (assistantId: string) => {
<div className="chat-top-section-container"> if (!openaiInstance || !activeAssistant) {
<div className="dropdowns-container"> return;
<div className="dropdown-container"> }
<Bot size={16} />
<DropdownSelect const assistant = assistants.find(
items={assistantOptions} (assistant) => assistant.id === assistantId,
onChange={onUpdateActiveAssistant} );
activeItem={activeAssistant?.id || ""}
/> updateActiveAssistant(assistant ?? assistants?.[0]);
<div className="dropdown-buttons-container"> };
<button className="create" onClick={deleteAssistant}>
<Trash2 size={16} /> const onUpdateActiveThread = (threadId: string) => {
</button> if (!openaiInstance || !activeThread) {
<button return;
className="create" }
onClick={handleEditAssistant} const newActiveThread = threads.find(
> (thread) => thread.id === threadId,
<Pencil size={16} /> );
</button> updateActiveThread(newActiveThread ?? activeThread);
<button };
className="create"
onClick={() => handleCreateAssistant()} return (
> <div className="chat-top-section-container">
<Plus size={16} /> <div className="dropdowns-container">
</button> <div className="dropdown-container">
</div> <Bot size={16} />
</div> <DropdownSelect
<div className="dropdown-container"> items={assistantOptions}
<MessageSquare size={16} /> onChange={onUpdateActiveAssistant}
<DropdownSelect activeItem={activeAssistant?.id || ''}
items={threadOptions} />
onChange={onUpdateActiveThread} <div className="dropdown-buttons-container">
activeItem={activeThread?.id || ""} <button className="create" onClick={deleteAssistant}>
/> <Trash2 size={16} />
<div className="dropdown-buttons-container"> </button>
<button className="create" onClick={deleteThread}> <button
<Trash2 size={16} /> className="create"
</button> onClick={handleEditAssistant}
<button className="create" onClick={handleEditThread}> >
<Pencil size={16} /> <Pencil size={16} />
</button> </button>
<button className="create" onClick={createThread}> <button
<Plus size={16} /> className="create"
</button> onClick={() => handleCreateAssistant()}
</div> >
</div> <Plus size={16} />
</div> </button>
</div> </div>
); </div>
<div className="dropdown-container">
<MessageSquare size={16} />
<DropdownSelect
items={threadOptions}
onChange={onUpdateActiveThread}
activeItem={activeThread?.id || ''}
/>
<div className="dropdown-buttons-container">
<button className="create" onClick={deleteThread}>
<Trash2 size={16} />
</button>
<button className="create" onClick={handleEditThread}>
<Pencil size={16} />
</button>
<button className="create" onClick={createThread}>
<Plus size={16} />
</button>
</div>
</div>
</div>
</div>
);
}; };
export default AssistantManager; export default AssistantManager;

View File

@ -1,240 +1,240 @@
import React, { CSSProperties, useEffect, useRef, useState } from "react"; import React, { CSSProperties, useEffect, useRef, useState } from 'react';
import OpenAI from "openai"; import OpenAI from 'openai';
import { ChevronDown, ClipboardCopy } from "lucide-react"; import { ChevronDown, ClipboardCopy } from 'lucide-react';
import BeatLoader from "react-spinners/BeatLoader"; import BeatLoader from 'react-spinners/BeatLoader';
import { Tooltip } from "react-tooltip"; import { Tooltip } from 'react-tooltip';
import Markdown from "react-markdown"; import Markdown from 'react-markdown';
import { useApp } from "../AppView"; import { useApp } from '../AppView';
import { createNotice } from "@/utils/Logs"; import { createNotice } from '@/utils/Logs';
import { TFile } from "obsidian"; import { TFile } from 'obsidian';
import { ThreadAnnotationFile } from "../types"; import { ThreadAnnotationFile } from '../types';
const override: CSSProperties = { const override: CSSProperties = {
display: "block", display: 'block',
margin: "0 auto", margin: '0 auto',
borderColor: "white", borderColor: 'white',
}; };
interface ChatboxProps { interface ChatboxProps {
messages: OpenAI.Beta.Threads.ThreadMessage[]; messages: OpenAI.Beta.Threads.ThreadMessage[];
isResponding: boolean; isResponding: boolean;
annotationFiles?: ThreadAnnotationFile[]; annotationFiles?: ThreadAnnotationFile[];
} }
const Chatbox = ({ annotationFiles, isResponding, messages }: ChatboxProps) => { const Chatbox = ({ annotationFiles, isResponding, messages }: ChatboxProps) => {
const app = useApp(); const app = useApp();
const messagesContainerRef = useRef<HTMLDivElement>(null); const messagesContainerRef = useRef<HTMLDivElement>(null);
const [showScrollButton, setShowScrollButton] = useState(false); const [showScrollButton, setShowScrollButton] = useState(false);
useEffect(() => { useEffect(() => {
if (messagesContainerRef.current) { if (messagesContainerRef.current) {
messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollTop =
messagesContainerRef.current.scrollHeight; messagesContainerRef.current.scrollHeight;
checkScrollButtonVisibility(); checkScrollButtonVisibility();
} }
const handleScroll = () => { const handleScroll = () => {
checkScrollButtonVisibility(); checkScrollButtonVisibility();
}; };
if (messagesContainerRef.current) { if (messagesContainerRef.current) {
scrollToBottom(); scrollToBottom();
messagesContainerRef.current.addEventListener( messagesContainerRef.current.addEventListener(
"scroll", 'scroll',
handleScroll handleScroll,
); );
} }
return () => { return () => {
if (messagesContainerRef.current) { if (messagesContainerRef.current) {
messagesContainerRef.current.removeEventListener( messagesContainerRef.current.removeEventListener(
"scroll", 'scroll',
handleScroll handleScroll,
); );
} }
}; };
}, [isResponding, messagesContainerRef.current]); }, [isResponding, messagesContainerRef.current]);
useEffect(() => { useEffect(() => {
// scroll to bottom when switching to different thread // scroll to bottom when switching to different thread
if (showScrollButton) { if (showScrollButton) {
scrollToBottom(); scrollToBottom();
setShowScrollButton(false); setShowScrollButton(false);
} }
}, [messages]); }, [messages]);
const getGroupMessages = () => const getGroupMessages = () =>
messages.map((message, index) => ( messages.map((message, index) => (
<div key={index} className={`chat-message ${message.role}`}> <div key={index} className={`chat-message ${message.role}`}>
{message.content.map((content, index) => { {message.content.map((content, index) => {
if (content.type === "text") { if (content.type === 'text') {
const getMessageText = () => { const getMessageText = () => {
const annotationsTexts = const annotationsTexts =
content.text.annotations.map( content.text.annotations.map(
(annotation: any) => annotation.text (annotation: any) => annotation.text,
); );
let text = content.text.value; let text = content.text.value;
annotationsTexts.forEach( annotationsTexts.forEach(
(annotationText: string, index: number) => { (annotationText: string, index: number) => {
const annotationIndex = index; const annotationIndex = index;
const regex = new RegExp( const regex = new RegExp(
annotationText, annotationText,
"g" 'g',
); );
text = text.replace( text = text.replace(
regex, regex,
`[^${annotationIndex}]` `[^${annotationIndex}]`,
); );
} },
); );
return text; return text;
}; };
const renderAnnotation = ( const renderAnnotation = (
annotation: any, annotation: any,
index: number index: number,
) => { ) => {
const { file_citation } = annotation; const { file_citation } = annotation;
const fileId = file_citation?.file_id; const fileId = file_citation?.file_id;
const fileName = annotationFiles?.find( const fileName = annotationFiles?.find(
(file) => file.fileId === fileId (file) => file.fileId === fileId,
)?.fileName; )?.fileName;
let quote = file_citation?.quote; let quote = file_citation?.quote;
// Check if quote has list markdown syntax // Check if quote has list markdown syntax
if (quote && quote.includes("- ")) { if (quote && quote.includes('- ')) {
quote = quote.replace(/- /g, "\n- "); quote = quote.replace(/- /g, '\n- ');
} }
const handleAnnotationClick = () => { const handleAnnotationClick = () => {
// open new tab and then navigate to fil // open new tab and then navigate to fil
console.log( console.log(
"handleAnnotationClick", 'handleAnnotationClick',
app, app,
fileName fileName,
); );
if (app && fileName) { if (app && fileName) {
const file = const file =
app.vault.getAbstractFileByPath( app.vault.getAbstractFileByPath(
fileName fileName,
); );
if (file && file instanceof TFile) { if (file && file instanceof TFile) {
app.workspace.getLeaf().openFile(file); app.workspace.getLeaf().openFile(file);
} }
// add leaf to parent // add leaf to parent
} }
}; };
return ( return (
<div key={index}> <div key={index}>
<a <a
className="annotation" className="annotation"
data-tooltip-id={`tooltip-${index}`} data-tooltip-id={`tooltip-${index}`}
onClick={handleAnnotationClick} onClick={handleAnnotationClick}
> >
[^{index}] [^{index}]
</a> </a>
<Tooltip id={`tooltip-${index}`}> <Tooltip id={`tooltip-${index}`}>
<div className="annotation-tooltip-container"> <div className="annotation-tooltip-container">
<strong>{fileName}</strong> <strong>{fileName}</strong>
<Markdown>{quote}</Markdown> <Markdown>{quote}</Markdown>
</div> </div>
</Tooltip> </Tooltip>
</div> </div>
); );
}; };
const renderContent = () => { const renderContent = () => {
const text = getMessageText(); const text = getMessageText();
const copyText = () => { const copyText = () => {
navigator.clipboard.writeText(text); navigator.clipboard.writeText(text);
createNotice("Copied to clipboard!", 2000); createNotice('Copied to clipboard!', 2000);
}; };
return ( return (
<div key={index} className="message-content"> <div key={index} className="message-content">
{ {
<Markdown className="message-text"> <Markdown className="message-text">
{text} {text}
</Markdown> </Markdown>
} }
<div className="message-footer"> <div className="message-footer">
{message.role === "assistant" && ( {message.role === 'assistant' && (
<div className="copy-icon-container"> <div className="copy-icon-container">
<ClipboardCopy <ClipboardCopy
className="copy-icon" className="copy-icon"
color={"#ffffff"} color={'#ffffff'}
size={16} size={16}
onClick={copyText} onClick={copyText}
/> />
</div> </div>
)} )}
{content.text.annotations.map( {content.text.annotations.map(
(annotation: any, index: number) => (annotation: any, index: number) =>
renderAnnotation( renderAnnotation(
annotation, annotation,
index index,
) ),
)} )}
</div> </div>
</div> </div>
); );
}; };
return ( return (
<div key={index} className="message-content"> <div key={index} className="message-content">
{renderContent()} {renderContent()}
</div> </div>
); );
} }
})} })}
</div> </div>
)); ));
const scrollToBottom = () => { const scrollToBottom = () => {
if (messagesContainerRef.current) { if (messagesContainerRef.current) {
messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollTop =
messagesContainerRef.current.scrollHeight; messagesContainerRef.current.scrollHeight;
checkScrollButtonVisibility(); checkScrollButtonVisibility();
} }
}; };
const checkScrollButtonVisibility = () => { const checkScrollButtonVisibility = () => {
if (messagesContainerRef.current) { if (messagesContainerRef.current) {
const containerHeight = messagesContainerRef.current.offsetHeight; const containerHeight = messagesContainerRef.current.offsetHeight;
const scrollHeight = messagesContainerRef.current.scrollHeight; const scrollHeight = messagesContainerRef.current.scrollHeight;
const scrollTop = messagesContainerRef.current.scrollTop; const scrollTop = messagesContainerRef.current.scrollTop;
const gap = scrollHeight - scrollTop - containerHeight; const gap = scrollHeight - scrollTop - containerHeight;
setShowScrollButton(gap > containerHeight); setShowScrollButton(gap > containerHeight);
} }
}; };
return ( return (
<div className="chatbox-container"> <div className="chatbox-container">
<div ref={messagesContainerRef} className="messages-container"> <div ref={messagesContainerRef} className="messages-container">
{getGroupMessages()} {getGroupMessages()}
{isResponding && ( {isResponding && (
<div className="loader-container"> <div className="loader-container">
<BeatLoader <BeatLoader
color="#ffffff" color="#ffffff"
loading={true} loading={true}
cssOverride={override} cssOverride={override}
size={12} size={12}
/> />
</div> </div>
)} )}
</div> </div>
{showScrollButton && ( {showScrollButton && (
<button <button
className="scroll-to-bottom-button" className="scroll-to-bottom-button"
onClick={scrollToBottom} onClick={scrollToBottom}
> >
<ChevronDown size={16} /> <ChevronDown size={16} />
</button> </button>
)} )}
</div> </div>
); );
}; };
export default Chatbox; export default Chatbox;

View File

@ -1,43 +1,43 @@
import React, { useEffect, useRef } from "react"; import React, { useEffect, useRef } from 'react';
import { DropdownComponent } from "obsidian"; import { DropdownComponent } from 'obsidian';
interface ISelectOption { interface ISelectOption {
label: string; label: string;
value: string; value: string;
} }
interface DropdownSelectProps { interface DropdownSelectProps {
items: ISelectOption[]; items: ISelectOption[];
onChange: (item: string) => void; onChange: (item: string) => void;
activeItem: string; activeItem: string;
} }
const DropdownSelect: React.FC<DropdownSelectProps> = ({ const DropdownSelect: React.FC<DropdownSelectProps> = ({
items, items,
onChange, onChange,
activeItem, activeItem,
}) => { }) => {
const selectElementRef = useRef<HTMLSelectElement>(null); const selectElementRef = useRef<HTMLSelectElement>(null);
useEffect(() => { useEffect(() => {
if (selectElementRef.current) { if (selectElementRef.current) {
new DropdownComponent(selectElementRef.current); new DropdownComponent(selectElementRef.current);
} }
}, [selectElementRef.current]); }, [selectElementRef.current]);
return ( return (
<select <select
ref={selectElementRef} ref={selectElementRef}
onChange={(e) => onChange(e.target.value)} onChange={(e) => onChange(e.target.value)}
value={activeItem} value={activeItem}
className={"dropdown-select"} className={'dropdown-select'}
> >
{items.map((item) => ( {items.map((item) => (
<option className="" key={item.value} value={item.value}> <option className="" key={item.value} value={item.value}>
{item.label} {item.label}
</option> </option>
))} ))}
</select> </select>
); );
}; };
export default DropdownSelect; export default DropdownSelect;

View File

@ -1,31 +1,31 @@
import { SendHorizontal } from "lucide-react"; import { SendHorizontal } from 'lucide-react';
import React, { useState } from "react"; import React, { useState } from 'react';
interface MessageInputProps { interface MessageInputProps {
onMessageSend: (message: string) => void; onMessageSend: (message: string) => void;
} }
const MessageInput: React.FC<MessageInputProps> = ({ onMessageSend }) => { const MessageInput: React.FC<MessageInputProps> = ({ onMessageSend }) => {
const [newMessage, setNewMessage] = useState(""); const [newMessage, setNewMessage] = useState('');
const handleSend = () => { const handleSend = () => {
onMessageSend(newMessage); onMessageSend(newMessage);
setNewMessage(""); setNewMessage('');
}; };
return ( return (
<div className="message-input container"> <div className="message-input container">
<textarea <textarea
className="message-input input" className="message-input input"
placeholder="Type your message here..." placeholder="Type your message here..."
value={newMessage} value={newMessage}
onChange={(e) => setNewMessage(e.target.value)} onChange={(e) => setNewMessage(e.target.value)}
/> />
<button className="message-input send" onClick={handleSend}> <button className="message-input send" onClick={handleSend}>
<SendHorizontal size={16} /> <SendHorizontal size={16} />
</button> </button>
</div> </div>
); );
}; };
export default MessageInput; export default MessageInput;

View File

@ -2,14 +2,14 @@ import { App, ButtonComponent, Modal, Notice, Setting } from 'obsidian';
import OpenAI from 'openai'; import OpenAI from 'openai';
import { FileSuggest } from '../suggesters/FileSuggester'; import { FileSuggest } from '../suggesters/FileSuggester';
import * as yup from 'yup'; import * as yup from 'yup';
import { defaultAssistantInstructions } from '../../../utils/templates' import { defaultAssistantInstructions } from '../../../utils/templates';
interface AssistantEditModalProps { interface AssistantEditModalProps {
app: App, app: App;
title: string, title: string;
submitButtonText?: string, submitButtonText?: string;
previousValues?: IAssistantEditModalValues, previousValues?: IAssistantEditModalValues;
onSubmit: (values: IAssistantEditModalValues) => void, onSubmit: (values: IAssistantEditModalValues) => void;
} }
export interface IAssistantEditModalValues { export interface IAssistantEditModalValues {
@ -60,7 +60,7 @@ export class AssistantEditModal extends Modal {
new Setting(contentEl) new Setting(contentEl)
.setName('Name (required)') .setName('Name (required)')
.setDesc('The name of the assistant') .setDesc('The name of the assistant')
.addText(text => { .addText((text) => {
text.setPlaceholder('Enter name...') text.setPlaceholder('Enter name...')
.onChange((value) => { .onChange((value) => {
this.values.name = value; this.values.name = value;
@ -74,7 +74,7 @@ export class AssistantEditModal extends Modal {
.setName('Description') .setName('Description')
.setDesc('The description of the assistant') .setDesc('The description of the assistant')
.setClass('form-setting-textarea') .setClass('form-setting-textarea')
.addTextArea(text => { .addTextArea((text) => {
text.setPlaceholder('Enter description...') text.setPlaceholder('Enter description...')
.onChange((value) => { .onChange((value) => {
this.values.description = value; this.values.description = value;
@ -88,7 +88,7 @@ export class AssistantEditModal extends Modal {
.setName('Instructions (required)') .setName('Instructions (required)')
.setDesc('The instructions you want the assistant to follow') .setDesc('The instructions you want the assistant to follow')
.setClass('form-setting-textarea') .setClass('form-setting-textarea')
.addTextArea(text => { .addTextArea((text) => {
text.setPlaceholder('Enter instructions...') text.setPlaceholder('Enter instructions...')
.onChange((value) => { .onChange((value) => {
this.values.instructions = value; this.values.instructions = value;
@ -101,10 +101,12 @@ export class AssistantEditModal extends Modal {
// Function to add a file to the list // Function to add a file to the list
const addFileToList = (fileName: string) => { const addFileToList = (fileName: string) => {
// if filename already is in values, replace it. this prevents duplicates and allows for re-uploading // 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) { if (fileIndex !== -1) {
this.values.files[fileIndex] = { this.values.files[fileIndex] = {
filename: fileName, filename: fileName,
@ -120,13 +122,14 @@ export class AssistantEditModal extends Modal {
}; };
const updateFileCountText = () => { 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 createFileListElement = (fileName: string) => {
const fileDiv = this.fileListDiv.createDiv({ cls: 'file-item' }); const fileDiv = this.fileListDiv.createDiv({ cls: 'file-item' });
fileDiv.createEl('span', { text: fileName }); fileDiv.createEl('span', { text: fileName });
new ButtonComponent(fileDiv) new ButtonComponent(fileDiv)
.setIcon('trash-2') .setIcon('trash-2')
@ -134,7 +137,9 @@ export class AssistantEditModal extends Modal {
.onClick(() => { .onClick(() => {
fileDiv.remove(); fileDiv.remove();
// Remove the file from the values object // 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) { if (file) {
this.values.files.remove(file); this.values.files.remove(file);
updateFileCountText(); updateFileCountText();
@ -144,11 +149,13 @@ export class AssistantEditModal extends Modal {
new Setting(contentEl) new Setting(contentEl)
.setName(`Files`) .setName(`Files`)
.setDesc('The files uploaded to the assistant (not real-time synced, so you will need to re-upload). Max 20.') .setDesc(
.addSearch(search => { 'The files uploaded to the assistant (not real-time synced, so you will need to re-upload). Max 20.',
search.setPlaceholder('Enter file IDs separated by commas...') )
.addSearch((search) => {
search.setPlaceholder('Enter file IDs separated by commas...');
// .onChange((value) => { // .onChange((value) => {
// this.values.file_ids.push(value); // this.values.file_ids.push(value);
// }); // });
@ -160,10 +167,8 @@ export class AssistantEditModal extends Modal {
updateFileCountText(); updateFileCountText();
this.fileListDiv = contentEl.createDiv({ cls: 'file-list' }); this.fileListDiv = contentEl.createDiv({ cls: 'file-list' });
// Add the files that were already selected // Add the files that were already selected
this.values.files.forEach(file => { this.values.files.forEach((file) => {
if (file.filename) { if (file.filename) {
createFileListElement(file.filename); createFileListElement(file.filename);
} }
@ -171,7 +176,6 @@ export class AssistantEditModal extends Modal {
} }
addSubmitButton(contentEl: HTMLElement) { addSubmitButton(contentEl: HTMLElement) {
const validationSchema = yup.object().shape({ const validationSchema = yup.object().shape({
name: yup.string().required('Name is required'), name: yup.string().required('Name is required'),
instructions: yup.string().required('Instructions are required'), instructions: yup.string().required('Instructions are required'),
@ -180,7 +184,9 @@ export class AssistantEditModal extends Modal {
const checkRequiredFields = async (): Promise<string[]> => { const checkRequiredFields = async (): Promise<string[]> => {
try { try {
await validationSchema.validate(this.values, { abortEarly: false }); await validationSchema.validate(this.values, {
abortEarly: false,
});
return []; return [];
} catch (error) { } catch (error) {
if (error instanceof yup.ValidationError) { if (error instanceof yup.ValidationError) {
@ -192,7 +198,7 @@ export class AssistantEditModal extends Modal {
const handleSubmit = async () => { const handleSubmit = async () => {
const missingFields = await checkRequiredFields(); const missingFields = await checkRequiredFields();
if (missingFields.length > 0) { if (missingFields.length > 0) {
new Notice(`Submit Error: \n${missingFields.join('\n')}`); new Notice(`Submit Error: \n${missingFields.join('\n')}`);
return; return;
@ -201,7 +207,7 @@ export class AssistantEditModal extends Modal {
this.onClose(); this.onClose();
this.close(); this.close();
} };
const modalFooterEl = contentEl.createDiv({ cls: 'modal-footer' }); const modalFooterEl = contentEl.createDiv({ cls: 'modal-footer' });
new ButtonComponent(modalFooterEl) new ButtonComponent(modalFooterEl)
.setButtonText(this.submitButtonText) .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'; import * as yup from 'yup';
interface ThreadEditModalProps { interface ThreadEditModalProps {
app: App, app: App;
title: string, title: string;
submitButtonText?: string, submitButtonText?: string;
previousValues?: IThreadEditModalValues, previousValues?: IThreadEditModalValues;
onSubmit: (values: IThreadEditModalValues) => void, onSubmit: (values: IThreadEditModalValues) => void;
} }
export interface IThreadEditModalValues { export interface IThreadEditModalValues {
metadata: { metadata: {
name: string; name: string;
[key: string]: unknown; [key: string]: unknown;
}, };
[key: string]: unknown; [key: string]: unknown;
} }
@ -46,7 +46,7 @@ export class ThreadEditModal extends Modal {
new Setting(contentEl) new Setting(contentEl)
.setName('Name') .setName('Name')
.setDesc('The name of the thread') .setDesc('The name of the thread')
.addText(text => { .addText((text) => {
text.setPlaceholder('Enter thread name...') text.setPlaceholder('Enter thread name...')
.onChange((value) => { .onChange((value) => {
this.values.metadata.name = value; this.values.metadata.name = value;
@ -54,7 +54,6 @@ export class ThreadEditModal extends Modal {
.setValue(this.values.metadata.name); .setValue(this.values.metadata.name);
}); });
const validationSchema = yup.object().shape({ const validationSchema = yup.object().shape({
metadata: yup.object().shape({ metadata: yup.object().shape({
name: yup.string().required('Name is required'), name: yup.string().required('Name is required'),
@ -63,7 +62,9 @@ export class ThreadEditModal extends Modal {
const checkRequiredFields = async (): Promise<string[]> => { const checkRequiredFields = async (): Promise<string[]> => {
try { try {
await validationSchema.validate(this.values, { abortEarly: false }); await validationSchema.validate(this.values, {
abortEarly: false,
});
return []; return [];
} catch (error) { } catch (error) {
if (error instanceof yup.ValidationError) { if (error instanceof yup.ValidationError) {
@ -75,7 +76,7 @@ export class ThreadEditModal extends Modal {
const handleSubmit = async () => { const handleSubmit = async () => {
const missingFields = await checkRequiredFields(); const missingFields = await checkRequiredFields();
if (missingFields.length > 0) { if (missingFields.length > 0) {
new Notice(`Submit Error: \n${missingFields.join('\n')}`); new Notice(`Submit Error: \n${missingFields.join('\n')}`);
return; return;
@ -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 // Credits go to Liam's Periodic Notes Plugin: https://github.com/liamcain/obsidian-periodic-notes
import { App, TAbstractFile, TFile } from "obsidian"; import { App, TAbstractFile, TFile } from 'obsidian';
import { TextInputSuggest } from "./suggest"; import { TextInputSuggest } from './suggest';
import { get_tfiles_from_folder } from "@/utils/utils"; import { get_tfiles_from_folder } from '@/utils/utils';
export class FileSuggest extends TextInputSuggest<TFile> { export class FileSuggest extends TextInputSuggest<TFile> {
private onSelect: (file: string) => void; private onSelect: (file: string) => void;
@ -10,8 +10,7 @@ export class FileSuggest extends TextInputSuggest<TFile> {
constructor( constructor(
app: App, app: App,
public inputEl: HTMLInputElement, public inputEl: HTMLInputElement,
onSelect: (file: string) => void onSelect: (file: string) => void,
) { ) {
super(app, inputEl); super(app, inputEl);
this.onSelect = onSelect; this.onSelect = onSelect;
@ -30,7 +29,7 @@ export class FileSuggest extends TextInputSuggest<TFile> {
all_files.forEach((file: TAbstractFile) => { all_files.forEach((file: TAbstractFile) => {
if ( if (
file instanceof TFile && file instanceof TFile &&
file.extension === "md" && file.extension === 'md' &&
file.path.toLowerCase().contains(lower_input_str) file.path.toLowerCase().contains(lower_input_str)
) { ) {
files.push(file); files.push(file);

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import { Notice } from "obsidian"; import { Notice } from 'obsidian';
export const createNotice = (message: string, timeout = 5000): void => { export const createNotice = (message: string, timeout = 5000): void => {
new Notice(`Obsidian Intelligence: ${message}`, timeout); 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 { export class TemplaterError extends Error {
constructor(msg: string, public console_msg?: string) { constructor(
msg: string,
public console_msg?: string,
) {
super(msg); super(msg);
this.name = this.constructor.name; this.name = this.constructor.name;
Error.captureStackTrace(this, this.constructor); Error.captureStackTrace(this, this.constructor);
@ -10,7 +13,7 @@ export class TemplaterError extends Error {
export async function errorWrapper<T>( export async function errorWrapper<T>(
fn: () => Promise<T>, fn: () => Promise<T>,
msg: string msg: string,
): Promise<T> { ): Promise<T> {
try { try {
return await fn(); return await fn();

View File

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

View File

@ -1,3 +1,3 @@
export const defaultAssistantInstructions = `- the uploaded files you have are my notes - if i mention the titles or contents of files i have given you in my response or with [[this syntax]], please look through relevant notes and use them in your response export const defaultAssistantInstructions = `- the uploaded files you have are my notes - if i mention the titles or contents of files i have given you in my response or with [[this syntax]], please look through relevant notes and use them in your response
- include annotations to the files you retrieve for your response. include them in your response format [^x] where x is the index starting from 0 - include annotations to the files you retrieve for your response. include them in your response format [^x] where x is the index starting from 0
- Use markdown format by adding syntax and \n in your responses before bullet lists, numbers, and paragraphs.`; - Use markdown format by adding syntax and \n in your responses before bullet lists, numbers, and paragraphs.`;

View File

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

View File

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

View File

@ -1,30 +1,22 @@
{ {
"compilerOptions": { "compilerOptions": {
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"baseUrl": ".", "baseUrl": ".",
"inlineSourceMap": true, "inlineSourceMap": true,
"inlineSources": true, "inlineSources": true,
"module": "ESNext", "module": "ESNext",
"target": "ES6", "target": "ES6",
"allowJs": true, "allowJs": true,
"noImplicitAny": true, "noImplicitAny": true,
"moduleResolution": "node", "moduleResolution": "node",
"importHelpers": true, "importHelpers": true,
"isolatedModules": true, "isolatedModules": true,
"jsx": "react", "jsx": "react",
"strictNullChecks": true, "strictNullChecks": true,
"lib": [ "lib": ["DOM", "ES5", "ES6", "ES7"],
"DOM", "paths": {
"ES5", "@/*": ["src/*"]
"ES6", }
"ES7" },
], "include": ["**/*.ts", "src/ui/AppView.tsx"]
"paths": {
"@/*": ["src/*"]
}
},
"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; const targetVersion = process.env.npm_package_version;
// read minAppVersion from manifest.json and bump version to target 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; const { minAppVersion } = manifest;
manifest.version = targetVersion; 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 // 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; versions[targetVersion] = minAppVersion;
writeFileSync("versions.json", JSON.stringify(versions, null, "\t")); writeFileSync('versions.json', JSON.stringify(versions, null, '\t'));

View File

@ -1,3 +1,3 @@
{ {
"1.0.0": "0.15.0" "1.0.0": "0.15.0"
} }