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,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();
} }

139
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) {
@ -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,30 +112,34 @@ 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;
@ -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) {
const messageObject =
await openaiInstance.beta.threads.messages.create(
activeThread.id,
{
role: 'user',
content: message,
},
);
if (openaiInstance && activeThread && activeAssistant) { setMessages([...messages, messageObject]);
const messageObject = await openaiInstance.beta.threads.messages.create(activeThread.id, { const run = await openaiInstance.beta.threads.runs.create(
role: "user", activeThread.id,
content: message, {
}); assistant_id: activeAssistant.id,
},
);
setMessages([...messages, messageObject]); let runStatus = await openaiInstance.beta.threads.runs.retrieve(
activeThread.id,
run.id,
);
// Initialize a counter and max attempts for the polling logic, and how long to wait each try
let attempts = 0;
const maxAttempts = 20;
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,
);
attempts++;
}
const run = await openaiInstance.beta.threads.runs.create(activeThread.id, { // Get latest messages
assistant_id: activeAssistant.id, await openaiInstance.beta.threads.messages
}); .list(activeThread.id, listQueryOptions)
.then((res) => {
let runStatus = await openaiInstance.beta.threads.runs.retrieve( setMessages(res.data);
activeThread.id, const files: ThreadAnnotationFile[] = [];
run.id //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
); res.data.forEach((message) => {
if (message.content) {
// Initialize a counter and max attempts for the polling logic, and how long to wait each try message.content.forEach((content) => {
let attempts = 0; if (content.type === 'text') {
const maxAttempts = 20; content.text.annotations.forEach(
const timoutWaitTimeMs = 2000; (annotation) => {
setIsResponding(true); // @ts-ignore
if (annotation.file_citation) {
while (runStatus.status !== "completed" && attempts < maxAttempts) { const fileId: string =
await new Promise((resolve) => setTimeout(resolve, timoutWaitTimeMs)); // @ts-ignore
runStatus = await openaiInstance.beta.threads.runs.retrieve(activeThread.id, run.id); annotation.file_citation
attempts++; .file_id;
} const file =
activeAssistantFiles?.find(
// Get latest messages (file) =>
await openaiInstance.beta.threads.messages.list(activeThread.id, listQueryOptions).then((res) => { file.id ===
setMessages(res.data); fileId,
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 if (file) {
res.data.forEach((message) => { files.push({
if (message.content) { fileId: fileId,
message.content.forEach((content) => { fileName:
if (content.type === 'text') { file.filename,
content.text.annotations.forEach((annotation) => { });
// @ts-ignore }
if (annotation.file_citation) { }
// @ts-ignore },
const fileId: string = annotation.file_citation.file_id; );
const file = activeAssistantFiles?.find((file) => file.id === fileId);
if (file) {
files.push({
fileId: fileId,
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,12 +307,13 @@ 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>

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(
const backlinks = Object.keys( //@ts-ignore
app.metadataCache.getBacklinksForFile(openFile).data app.metadataCache.getBacklinksForFile(openFile)
).map((file) => file); .data,
console.log( ).map((file) => file);
"file metadata cache", console.log(
app.metadataCache.getCache(openFile.path)?.links 'file metadata cache',
); app.metadataCache.getCache(openFile.path)?.links,
const currentFile = openFile.path; );
const filesToUpload = new Set([ const currentFile = openFile.path;
currentFile, const filesToUpload = new Set([
...links, currentFile,
...backlinks, ...links,
]); ...backlinks,
]);
handleCreateAssistant({ handleCreateAssistant({
name: `${openFile.path} Assistant`, name: `${openFile.path} Assistant`,
instructions: defaultAssistantInstructions, instructions: defaultAssistantInstructions,
files: Array.from(filesToUpload) files: Array.from(filesToUpload)
.filter((file) => file.endsWith(".md")) .filter((file) => file.endsWith('.md'))
.map((file) => ({ .map((file) => ({
filename: file, filename: file,
})), })),
}); });
} }
// This command will only show up in Command Palette when the check function returns true // This command will only show up in Command Palette when the check function returns true
return true; return true;
} }
}, },
}); });
}, [plugin]); }, [plugin]);
useEffect(() => {}, [plugin]); useEffect(() => {}, [plugin]);
const createThread = async () => { const createThread = async () => {
if (!openaiInstance || !plugin) { if (!openaiInstance || !plugin) {
return; return;
} }
const newThreadName = "New Thread"; const newThreadName = 'New Thread';
await openaiInstance.beta.threads await openaiInstance.beta.threads
.create({ .create({
metadata: { metadata: {
name: newThreadName, name: newThreadName,
}, },
}) })
.then((res) => { .then((res) => {
const newThread = { const newThread = {
...res, ...res,
metadata: { metadata: {
name: newThreadName, name: newThreadName,
}, },
}; };
updateThreads([...threads, newThread]); updateThreads([...threads, newThread]);
updateActiveThread(newThread); updateActiveThread(newThread);
plugin.saveSettings(); plugin.saveSettings();
}); });
}; };
const editThread = async (values: IThreadEditModalValues) => { const editThread = async (values: IThreadEditModalValues) => {
if (!openaiInstance || !app || !activeThread) { if (!openaiInstance || !app || !activeThread) {
return; return;
} }
const newThread = { const newThread = {
...activeThread, ...activeThread,
metadata: { metadata: {
name: values.metadata.name, name: values.metadata.name,
}, },
}; };
await openaiInstance.beta.threads await openaiInstance.beta.threads
.update(activeThread.id, { .update(activeThread.id, {
metadata: { metadata: {
name: values.metadata.name, name: values.metadata.name,
}, },
}) })
.then((res) => { .then((res) => {
updateThreads( updateThreads(
threads.map((thread) => { threads.map((thread) => {
if (thread.id === activeThread.id) { if (thread.id === activeThread.id) {
return newThread; return newThread;
} }
return thread; return thread;
}) }),
); );
updateActiveThread(newThread); updateActiveThread(newThread);
}); });
}; };
const handleEditThread = async () => { const handleEditThread = async () => {
if (!openaiInstance || !app || !activeThread) { if (!openaiInstance || !app || !activeThread) {
return; return;
} }
// Get the previous values of the thread // Get the previous values of the thread
const previousValues = { const previousValues = {
metadata: { metadata: {
name: activeThread.metadata.name || "", name: activeThread.metadata.name || '',
}, },
}; };
new ThreadEditModal({ new ThreadEditModal({
app, app,
title: "Edit Thread", title: 'Edit Thread',
submitButtonText: "Edit", submitButtonText: 'Edit',
previousValues, previousValues,
onSubmit: editThread, onSubmit: editThread,
}).open(); }).open();
}; };
const deleteThread = async () => { const deleteThread = async () => {
if (!openaiInstance || !plugin || !activeThread) { if (!openaiInstance || !plugin || !activeThread) {
return; return;
} }
await openaiInstance.beta.threads await openaiInstance.beta.threads
.del(activeThread.id) .del(activeThread.id)
.then((res) => { .then((res) => {
const newThreadsList = threads.filter( const newThreadsList = threads.filter(
(thread) => thread.id !== activeThread.id (thread) => thread.id !== activeThread.id,
); );
updateThreads(newThreadsList); updateThreads(newThreadsList);
updateActiveThread(newThreadsList?.[0]); updateActiveThread(newThreadsList?.[0]);
plugin.saveSettings(); plugin.saveSettings();
}) })
.catch((error) => { .catch((error) => {
console.error(error); console.error(error);
}); });
}; };
const createAssistant = async (values: IAssistantEditModalValues) => { const createAssistant = async (values: IAssistantEditModalValues) => {
if (!openaiInstance || !app) { if (!openaiInstance || !app) {
return; return;
} }
// use uploadFileToOpenAI to upload files to openai // use uploadFileToOpenAI to upload files to openai
const uploadedFiles: string[] = []; const uploadedFiles: string[] = [];
await Promise.all( await Promise.all(
values.files.map(async (file) => { values.files.map(async (file) => {
if (file.filename) { if (file.filename) {
const uploadedFile = await uploadFileToOpenAI( const uploadedFile = await uploadFileToOpenAI(
file?.filename file?.filename,
); );
if (uploadedFile) { if (uploadedFile) {
uploadedFiles.push(uploadedFile.id);
}
}
}),
);
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',
file_ids: uploadedFiles,
})
.then((res) => {
createNotice(`Assistant "${values.name}" created`);
updateAssistants([...assistants, res]);
updateActiveAssistant(res);
});
};
const handleCreateAssistant = async (
assistant?: IAssistantEditModalValues,
) => {
if (!openaiInstance || !app) {
return;
}
await openaiInstance.beta.assistants // const assistant = await openaiInstance.beta.assistants.create({
.create({ // name: "Math Tutor",
name: values.name, // instructions:
description: values.description, // "You are a personal math tutor. Write and run code to answer math questions.",
instructions: values.instructions, // tools: [{ type: "code_interpreter" }],
tools: [{ type: "code_interpreter" }, { type: "retrieval" }], // model: "gpt-4-1106-preview",
model: "gpt-4-1106-preview", // });
file_ids: uploadedFiles, // setActiveAssistant(assistant);
}) new AssistantEditModal({
.then((res) => { app,
createNotice(`Assistant "${values.name}" created`); title: 'Create New Assistant',
updateAssistants([...assistants, res]); submitButtonText: 'Create',
updateActiveAssistant(res); previousValues: assistant,
}); onSubmit: createAssistant,
}; }).open();
};
const handleCreateAssistant = async ( const editAssistant = async (values: IAssistantEditModalValues) => {
assistant?: IAssistantEditModalValues if (!openaiInstance || !app || !activeAssistant) {
) => { return;
if (!openaiInstance || !app) { }
return;
}
// const assistant = await openaiInstance.beta.assistants.create({ // 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.
// name: "Math Tutor", const filesToUpload: string[] = [];
// instructions: const assistantFiles: string[] = [];
// "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) => { values.files.forEach((file) => {
if (!openaiInstance || !app || !activeAssistant) { if (file.id) {
return; assistantFiles.push(file.id);
} } else if (file.filename) {
filesToUpload.push(file.filename);
}
});
// 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 Promise.all(
const filesToUpload: string[] = []; filesToUpload.map(async (file) => {
const assistantFiles: string[] = []; const uploadedFile = await uploadFileToOpenAI(file);
values.files.forEach((file) => { if (uploadedFile) {
assistantFiles.push(uploadedFile.id);
}
}),
);
if (file.id) { await openaiInstance.beta.assistants
assistantFiles.push(file.id); .update(activeAssistant.id, {
} else if (file.filename) { name: values.name,
filesToUpload.push(file.filename); 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;
}),
);
});
};
await Promise.all( const handleEditAssistant = async () => {
filesToUpload.map(async (file) => { if (!openaiInstance || !app || !activeAssistant) {
const uploadedFile = await uploadFileToOpenAI(file); return;
}
if (uploadedFile) { const files = await getAssistantFiles();
assistantFiles.push(uploadedFile.id);
}
})
);
await openaiInstance.beta.assistants const previousValues = {
.update(activeAssistant.id, { name: activeAssistant.name || '',
name: values.name, description: activeAssistant.description || '',
description: values.description, instructions: activeAssistant.instructions || '',
instructions: values.instructions, files: files || [],
file_ids: assistantFiles, };
})
.then((res) => {
updateActiveAssistant(res);
updateAssistants(
assistants.map((assistant) => {
if (assistant.id === activeAssistant.id) {
return res;
}
return assistant;
})
);
});
};
const handleEditAssistant = async () => { new AssistantEditModal({
if (!openaiInstance || !app || !activeAssistant) { app,
return; title: 'Edit Assistant',
} submitButtonText: 'Edit',
previousValues,
onSubmit: editAssistant,
}).open();
};
const files = await getAssistantFiles(); const deleteAssistant = async () => {
if (!openaiInstance || !activeAssistant) {
return;
}
const previousValues = { await openaiInstance.beta.assistants
name: activeAssistant.name || "", .del(activeAssistant.id)
description: activeAssistant.description || "", .then((res) => {
instructions: activeAssistant.instructions || "", const newAssistantsList = assistants.filter(
files: files || [], (assistant) => assistant.id !== activeAssistant.id,
}; );
new AssistantEditModal({ updateAssistants(newAssistantsList);
app, updateActiveAssistant(newAssistantsList?.[0]);
title: "Edit Assistant", });
submitButtonText: "Edit", };
previousValues,
onSubmit: editAssistant,
}).open();
};
const deleteAssistant = async () => { const uploadFileToOpenAI = async (
if (!openaiInstance || !activeAssistant) { fileName: string,
return; ): Promise<OpenAI.Files.FileObject | undefined> => {
} if (!openaiInstance || !plugin || !app) {
return undefined;
}
await openaiInstance.beta.assistants const file = await app.vault.adapter.read(fileName);
.del(activeAssistant.id) const blob = new File([file], fileName, { type: 'text/markdown' });
.then((res) => {
const newAssistantsList = assistants.filter(
(assistant) => assistant.id !== activeAssistant.id
);
updateAssistants(newAssistantsList); const returnedFile = await openaiInstance.files
updateActiveAssistant(newAssistantsList?.[0]); .create({
}); purpose: 'assistants',
}; file: blob,
})
.then((res) => {
return res;
})
.catch((error) => {
console.error(error);
return undefined;
});
const uploadFileToOpenAI = async ( return returnedFile;
fileName: string };
): Promise<OpenAI.Files.FileObject | undefined> => {
if (!openaiInstance || !plugin || !app) {
return undefined;
}
const file = await app.vault.adapter.read(fileName); // const attachFileToAssistant = async (assistantId: string, fileId: string) => {
const blob = new File([file], fileName, { type: "text/markdown" }); // //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
const returnedFile = await openaiInstance.files // }).catch((error) => {
.create({ // // Handle the error
purpose: "assistants", // console.error(error);
file: blob, // });
}) // };
.then((res) => {
return res;
})
.catch((error) => {
console.error(error);
return undefined;
});
return returnedFile; const getAssistantFiles = async (): Promise<
}; OpenAI.Files.FileObject[] | []
> => {
if (!openaiInstance || !plugin || !activeAssistant) {
return [];
}
// const attachFileToAssistant = async (assistantId: string, fileId: string) => { try {
// //file id is file-CFBYJh1WUxRWdAtdaScVZHS7 const assistantFilesResponse =
// //assistant id is asst_0HHHYCL2dgImUlXbZKDRjac0 await openaiInstance.beta.assistants.files.list(
// if (!openaiInstance || !plugin) { activeAssistant?.id,
// return; );
// }
// await openaiInstance.beta.assistants.files.create(
// assistantId,
// {
// file_id: fileId,
// }
// ).then((res) => {
// // Handle the response
// }).catch((error) => { const innerFiles: OpenAI.Files.FileObject[] = [];
// // Handle the error
// console.error(error);
// });
// };
const getAssistantFiles = async (): Promise< await Promise.all(
OpenAI.Files.FileObject[] | [] assistantFilesResponse.data.map(async (file) => {
> => { try {
if (!openaiInstance || !plugin || !activeAssistant) { const fileInfoResponse =
return []; await openaiInstance.files.retrieve(file.id);
}
try { innerFiles.push(fileInfoResponse);
const assistantFilesResponse = } catch (error) {
await openaiInstance.beta.assistants.files.list( console.error(error);
activeAssistant?.id }
); }),
);
const innerFiles: OpenAI.Files.FileObject[] = []; return innerFiles;
} catch (error) {
console.error(error);
return [];
}
};
await Promise.all( //format assistants into ISelectOption
assistantFilesResponse.data.map(async (file) => { const assistantOptions = useMemo(() => {
try { return assistants.map((assistant) => {
const fileInfoResponse = return {
await openaiInstance.files.retrieve(file.id); label: assistant.name || '',
value: assistant.id,
};
});
}, [assistants]);
innerFiles.push(fileInfoResponse); const threadOptions = useMemo(() => {
} catch (error) { return threads.map((thread) => {
console.error(error); return {
} label: thread.metadata.name,
}) value: thread.id,
); };
});
}, [threads]);
return innerFiles; const onUpdateActiveAssistant = (assistantId: string) => {
} catch (error) { if (!openaiInstance || !activeAssistant) {
console.error(error); return;
return []; }
}
};
//format assistants into ISelectOption const assistant = assistants.find(
const assistantOptions = useMemo(() => { (assistant) => assistant.id === assistantId,
return assistants.map((assistant) => { );
return {
label: assistant.name || "",
value: assistant.id,
};
});
}, [assistants]);
const threadOptions = useMemo(() => { updateActiveAssistant(assistant ?? assistants?.[0]);
return threads.map((thread) => { };
return {
label: thread.metadata.name,
value: thread.id,
};
});
}, [threads]);
const onUpdateActiveAssistant = (assistantId: string) => { const onUpdateActiveThread = (threadId: string) => {
if (!openaiInstance || !activeAssistant) { if (!openaiInstance || !activeThread) {
return; return;
} }
const newActiveThread = threads.find(
(thread) => thread.id === threadId,
);
updateActiveThread(newActiveThread ?? activeThread);
};
const assistant = assistants.find( return (
(assistant) => assistant.id === assistantId <div className="chat-top-section-container">
); <div className="dropdowns-container">
<div className="dropdown-container">
updateActiveAssistant(assistant ?? assistants?.[0]); <Bot size={16} />
}; <DropdownSelect
items={assistantOptions}
const onUpdateActiveThread = (threadId: string) => { onChange={onUpdateActiveAssistant}
if (!openaiInstance || !activeThread) { activeItem={activeAssistant?.id || ''}
return; />
} <div className="dropdown-buttons-container">
const newActiveThread = threads.find( <button className="create" onClick={deleteAssistant}>
(thread) => thread.id === threadId <Trash2 size={16} />
); </button>
updateActiveThread(newActiveThread ?? activeThread); <button
}; className="create"
onClick={handleEditAssistant}
>
<Pencil size={16} />
return ( </button>
<div className="chat-top-section-container"> <button
<div className="dropdowns-container"> className="create"
<div className="dropdown-container"> onClick={() => handleCreateAssistant()}
<Bot size={16} /> >
<DropdownSelect <Plus size={16} />
items={assistantOptions} </button>
onChange={onUpdateActiveAssistant} </div>
activeItem={activeAssistant?.id || ""} </div>
/> <div className="dropdown-container">
<div className="dropdown-buttons-container"> <MessageSquare size={16} />
<button className="create" onClick={deleteAssistant}> <DropdownSelect
<Trash2 size={16} /> items={threadOptions}
</button> onChange={onUpdateActiveThread}
<button activeItem={activeThread?.id || ''}
className="create" />
onClick={handleEditAssistant} <div className="dropdown-buttons-container">
> <button className="create" onClick={deleteThread}>
<Pencil size={16} /> <Trash2 size={16} />
</button> </button>
<button <button className="create" onClick={handleEditThread}>
className="create" <Pencil size={16} />
onClick={() => handleCreateAssistant()} </button>
> <button className="create" onClick={createThread}>
<Plus size={16} /> <Plus size={16} />
</button> </button>
</div> </div>
</div> </div>
<div className="dropdown-container"> </div>
<MessageSquare size={16} /> </div>
<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,21 +122,24 @@ 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')
.setClass('remove-button') .setClass('remove-button')
.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,9 +149,11 @@ 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) {
@ -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) {
@ -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,4 +1,4 @@
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,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"
} }