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,
"parser": "@typescript-eslint/parser",
"env": { "node": true },
"plugins": [
"@typescript-eslint"
],
"plugins": ["@typescript-eslint"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"parserOptions": {
"sourceType": "module"
},
"rules": {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
"@typescript-eslint/ban-ts-comment": "off",
"no-prototype-builtins": "off",
"@typescript-eslint/no-empty-function": "off"
}
}
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
"@typescript-eslint/ban-ts-comment": "off",
"no-prototype-builtins": "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
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 process from "process";
import builtins from "builtin-modules";
import esbuild from 'esbuild';
import process from 'process';
import builtins from 'builtin-modules';
const banner =
`/*
const banner = `/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
if you want to view the source, please visit the github repository of this plugin
*/
`;
const prod = (process.argv[2] === "production");
const prod = process.argv[2] === 'production';
const context = await esbuild.context({
banner: {
js: banner,
},
entryPoints: ["main.ts"],
bundle: true,
external: [
"obsidian",
"electron",
"@codemirror/autocomplete",
"@codemirror/collab",
"@codemirror/commands",
"@codemirror/language",
"@codemirror/lint",
"@codemirror/search",
"@codemirror/state",
"@codemirror/view",
"@lezer/common",
"@lezer/highlight",
"@lezer/lr",
...builtins],
format: "cjs",
target: "es2018",
logLevel: "info",
sourcemap: prod ? false : "inline",
treeShaking: true,
outfile: "main.js",
banner: {
js: banner,
},
entryPoints: ['main.ts'],
bundle: true,
external: [
'obsidian',
'electron',
'@codemirror/autocomplete',
'@codemirror/collab',
'@codemirror/commands',
'@codemirror/language',
'@codemirror/lint',
'@codemirror/search',
'@codemirror/state',
'@codemirror/view',
'@lezer/common',
'@lezer/highlight',
'@lezer/lr',
...builtins,
],
format: 'cjs',
target: 'es2018',
logLevel: 'info',
sourcemap: prod ? false : 'inline',
treeShaking: true,
outfile: 'main.js',
});
if (prod) {
await context.rebuild();
process.exit(0);
await context.rebuild();
process.exit(0);
} 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';
interface ObsidianIntelligenceSettings {
openaiKey: string;
threads: IThread[];
activeThread: IThread | undefined;
activeAssistant: OpenAI.Beta.Assistant | undefined;
activeAssistantFiles: OpenAI.Files.FileObject[] | undefined;
openaiKey: string;
threads: IThread[];
activeThread: IThread | undefined;
activeAssistant: OpenAI.Beta.Assistant | undefined;
activeAssistantFiles: OpenAI.Files.FileObject[] | undefined;
}
const DEFAULT_SETTINGS: ObsidianIntelligenceSettings = {
openaiKey: '',
threads: [],
activeThread: undefined,
activeAssistant: undefined,
activeAssistantFiles: undefined,
}
openaiKey: '',
threads: [],
activeThread: undefined,
activeAssistant: undefined,
activeAssistantFiles: undefined,
};
export default class ObsidianIntelligence extends Plugin {
settings: ObsidianIntelligenceSettings;
view: AppView;
settings: ObsidianIntelligenceSettings;
view: AppView;
async onload() {
await this.loadSettings();
this.registerView(
VIEW_TYPE,
(leaf) => (new AppView(leaf, this))
async onload() {
await this.loadSettings();
this.registerView(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.activateView();
});
// 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.
const statusBarItemEl = this.addStatusBarItem();
statusBarItemEl.setText('Status Bar Text');
// This adds a status bar item to the bottom of the app. Does not work on mobile apps.
const statusBarItemEl = this.addStatusBarItem();
statusBarItemEl.setText('Status Bar Text');
this.addCommand({
id: "obsidian-intelligence-view-open",
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: () => {
this.activateView();
}
},
});
// This adds a settings tab so the user can configure various aspects of the plugin
this.addSettingTab(new OISettingTab(this.app, this));
}
// This adds a settings tab so the user can configure various aspects of the plugin
this.addSettingTab(new OISettingTab(this.app, this));
}
onunload() {
onunload() {}
}
async loadSettings() {
this.settings = Object.assign(
{},
DEFAULT_SETTINGS,
await this.loadData(),
);
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings() {
await this.saveData(this.settings);
}
async saveSettings() {
await this.saveData(this.settings);
}
async activateView() {
async activateView() {
this.app.workspace.detachLeavesOfType(VIEW_TYPE);
await this.app.workspace.getRightLeaf(false).setViewState({
type: VIEW_TYPE,
active: true,
type: VIEW_TYPE,
active: true,
});
this.app.workspace.revealLeaf(
this.app.workspace.getLeavesOfType(VIEW_TYPE)[0]
this.app.workspace.getLeavesOfType(VIEW_TYPE)[0],
);
}
}
class OISettingTab extends PluginSettingTab {
plugin: ObsidianIntelligence;
plugin: ObsidianIntelligence;
constructor(app: App, plugin: ObsidianIntelligence) {
super(app, plugin);
this.plugin = plugin;
}
constructor(app: App, plugin: ObsidianIntelligence) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const {containerEl} = this;
display(): void {
const { containerEl } = this;
containerEl.empty();
containerEl.empty();
new Setting(containerEl)
.setName('OpenAI Key')
.setDesc('Can find it https://platform.openai.com/api-keys')
.addText(text => text
.setPlaceholder('Enter your API Key')
.setValue(this.plugin.settings.openaiKey)
.onChange(async (value) => {
this.plugin.settings.openaiKey = value;
await this.plugin.saveSettings();
}));
}
}
new Setting(containerEl)
.setName('OpenAI Key')
.setDesc('Can find it https://platform.openai.com/api-keys')
.addText((text) =>
text
.setPlaceholder('Enter your API Key')
.setValue(this.plugin.settings.openaiKey)
.onChange(async (value) => {
this.plugin.settings.openaiKey = value;
await this.plugin.saveSettings();
}),
);
}
}

View File

@ -1,11 +1,11 @@
{
"id": "obsidian-intelligence",
"name": "Obsidian Intelligence",
"version": "1.0.0",
"minAppVersion": "0.15.0",
"description": "AI-powered assistants inside Obsidian",
"author": "John Mavrick",
"authorUrl": "https://beacons.ai/johnmavrick",
"fundingUrl": "https://patreon.com/johnmavrick",
"isDesktopOnly": false
"id": "obsidian-intelligence",
"name": "Obsidian Intelligence",
"version": "1.0.0",
"minAppVersion": "0.15.0",
"description": "AI-powered assistants inside Obsidian",
"author": "John Mavrick",
"authorUrl": "https://beacons.ai/johnmavrick",
"fundingUrl": "https://patreon.com/johnmavrick",
"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",
"version": "1.0.0",
"description": "AI-powered assistants inside Obsidian",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"version": "node version-bump.mjs && git add manifest.json versions.json"
},
"keywords": [],
"author": "",
"license": "MIT",
"devDependencies": {
"@types/node": "^16.11.6",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"builtin-modules": "3.3.0",
"esbuild": "0.17.3",
"obsidian": "latest",
"tslib": "2.4.0",
"typescript": "4.7.4"
},
"dependencies": {
"@popperjs/core": "^2.11.8",
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"lucide-react": "^0.292.0",
"openai": "^4.16.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-markdown": "^9.0.1",
"react-spinners": "^0.13.8",
"react-tooltip": "^5.23.0",
"yup": "^1.3.2"
}
"name": "obsidian-intelligence",
"version": "1.0.0",
"description": "AI-powered assistants inside Obsidian",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"version": "node version-bump.mjs && git add manifest.json versions.json",
"prettier": "prettier --write ."
},
"keywords": [],
"author": "",
"license": "MIT",
"devDependencies": {
"@types/node": "^16.11.6",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"builtin-modules": "3.3.0",
"esbuild": "0.17.3",
"obsidian": "latest",
"prettier": "^3.1.0",
"tslib": "2.4.0",
"typescript": "4.7.4"
},
"dependencies": {
"@popperjs/core": "^2.11.8",
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"lucide-react": "^0.292.0",
"openai": "^4.16.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-markdown": "^9.0.1",
"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
wow
wow

View File

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

View File

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

View File

@ -1,527 +1,523 @@
import React, { useEffect, useMemo } from "react";
import OpenAI from "openai";
import { useApp, useOpenAI, usePlugin } from "../AppView";
import { IThread } from "../types";
import DropdownSelect from "./DropdownSelect";
import { MarkdownView } from "obsidian";
import { Bot, MessageSquare, Plus, Pencil, Trash2 } from "lucide-react";
import React, { useEffect, useMemo } from 'react';
import OpenAI from 'openai';
import { useApp, useOpenAI, usePlugin } from '../AppView';
import { IThread } from '../types';
import DropdownSelect from './DropdownSelect';
import { MarkdownView } from 'obsidian';
import { Bot, MessageSquare, Plus, Pencil, Trash2 } from 'lucide-react';
import {
AssistantEditModal,
IAssistantEditModalValues,
} from "./modals/AssistantEditModal";
import {} from "./modals/AssistantEditModal";
AssistantEditModal,
IAssistantEditModalValues,
} from './modals/AssistantEditModal';
import {} from './modals/AssistantEditModal';
import {
ThreadEditModal,
IThreadEditModalValues,
} from "./modals/ThreadEditModal";
import { createNotice } from "@/utils/Logs";
import { defaultAssistantInstructions } from "@/utils/templates";
ThreadEditModal,
IThreadEditModalValues,
} from './modals/ThreadEditModal';
import { createNotice } from '@/utils/Logs';
import { defaultAssistantInstructions } from '@/utils/templates';
interface AssistantManagerProps {
assistants: OpenAI.Beta.Assistant[];
updateAssistants: (assistants: OpenAI.Beta.Assistant[]) => void;
threads: IThread[];
updateThreads: (threads: IThread[]) => void;
activeAssistant: OpenAI.Beta.Assistant | undefined;
updateActiveAssistant: (assistant: OpenAI.Beta.Assistant) => void;
activeThread: IThread | undefined;
updateActiveThread: (thread: IThread) => void;
assistants: OpenAI.Beta.Assistant[];
updateAssistants: (assistants: OpenAI.Beta.Assistant[]) => void;
threads: IThread[];
updateThreads: (threads: IThread[]) => void;
activeAssistant: OpenAI.Beta.Assistant | undefined;
updateActiveAssistant: (assistant: OpenAI.Beta.Assistant) => void;
activeThread: IThread | undefined;
updateActiveThread: (thread: IThread) => void;
}
const AssistantManager = ({
assistants,
updateAssistants,
threads,
updateThreads,
activeAssistant,
activeThread,
updateActiveAssistant,
updateActiveThread,
assistants,
updateAssistants,
threads,
updateThreads,
activeAssistant,
activeThread,
updateActiveAssistant,
updateActiveThread,
}: AssistantManagerProps) => {
const app = useApp();
const plugin = usePlugin();
const openaiInstance = useOpenAI();
const app = useApp();
const plugin = usePlugin();
const openaiInstance = useOpenAI();
useEffect(() => {
if (!plugin || !app) {
return;
}
useEffect(() => {
if (!plugin || !app) {
return;
}
plugin.addCommand({
id: "create-assistant-from-active-note",
name: "Create Assistant from Active Note",
checkCallback: (checking: boolean) => {
// Conditions to check
const markdownView =
app.workspace.getActiveViewOfType(MarkdownView);
const openFile = markdownView?.file;
plugin.addCommand({
id: 'create-assistant-from-active-note',
name: 'Create Assistant from Active Note',
checkCallback: (checking: boolean) => {
// Conditions to check
const markdownView =
app.workspace.getActiveViewOfType(MarkdownView);
const openFile = markdownView?.file;
if (openFile) {
if (!checking) {
// const links = app.metadataCache.getFileCache(openFile)?.links?.map(
// (link) => addFileType(truncateLink(link.link))
// ) || [];
const links = Object.keys(
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]);
if (openFile) {
if (!checking) {
// const links = app.metadataCache.getFileCache(openFile)?.links?.map(
// (link) => addFileType(truncateLink(link.link))
// ) || [];
const links = Object.keys(
app.metadataCache.resolvedLinks[openFile.path],
);
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 () => {
if (!openaiInstance || !plugin) {
return;
}
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]);
const newThreadName = "New Thread";
useEffect(() => {}, [plugin]);
await openaiInstance.beta.threads
.create({
metadata: {
name: newThreadName,
},
})
.then((res) => {
const newThread = {
...res,
metadata: {
name: newThreadName,
},
};
updateThreads([...threads, newThread]);
updateActiveThread(newThread);
plugin.saveSettings();
});
};
const createThread = async () => {
if (!openaiInstance || !plugin) {
return;
}
const editThread = async (values: IThreadEditModalValues) => {
if (!openaiInstance || !app || !activeThread) {
return;
}
const newThreadName = 'New Thread';
const newThread = {
...activeThread,
metadata: {
name: values.metadata.name,
},
};
await openaiInstance.beta.threads
.create({
metadata: {
name: newThreadName,
},
})
.then((res) => {
const newThread = {
...res,
metadata: {
name: newThreadName,
},
};
updateThreads([...threads, newThread]);
updateActiveThread(newThread);
plugin.saveSettings();
});
};
await openaiInstance.beta.threads
.update(activeThread.id, {
metadata: {
name: values.metadata.name,
},
})
.then((res) => {
updateThreads(
threads.map((thread) => {
if (thread.id === activeThread.id) {
return newThread;
}
return thread;
})
);
updateActiveThread(newThread);
});
};
const editThread = async (values: IThreadEditModalValues) => {
if (!openaiInstance || !app || !activeThread) {
return;
}
const handleEditThread = async () => {
if (!openaiInstance || !app || !activeThread) {
return;
}
const newThread = {
...activeThread,
metadata: {
name: values.metadata.name,
},
};
// Get the previous values of the thread
const previousValues = {
metadata: {
name: activeThread.metadata.name || "",
},
};
await openaiInstance.beta.threads
.update(activeThread.id, {
metadata: {
name: values.metadata.name,
},
})
.then((res) => {
updateThreads(
threads.map((thread) => {
if (thread.id === activeThread.id) {
return newThread;
}
return thread;
}),
);
updateActiveThread(newThread);
});
};
new ThreadEditModal({
app,
title: "Edit Thread",
submitButtonText: "Edit",
previousValues,
onSubmit: editThread,
}).open();
};
const handleEditThread = async () => {
if (!openaiInstance || !app || !activeThread) {
return;
}
const deleteThread = async () => {
if (!openaiInstance || !plugin || !activeThread) {
return;
}
// Get the previous values of the thread
const previousValues = {
metadata: {
name: activeThread.metadata.name || '',
},
};
await openaiInstance.beta.threads
.del(activeThread.id)
.then((res) => {
const newThreadsList = threads.filter(
(thread) => thread.id !== activeThread.id
);
updateThreads(newThreadsList);
updateActiveThread(newThreadsList?.[0]);
plugin.saveSettings();
})
.catch((error) => {
console.error(error);
});
};
new ThreadEditModal({
app,
title: 'Edit Thread',
submitButtonText: 'Edit',
previousValues,
onSubmit: editThread,
}).open();
};
const createAssistant = async (values: IAssistantEditModalValues) => {
if (!openaiInstance || !app) {
return;
}
const deleteThread = async () => {
if (!openaiInstance || !plugin || !activeThread) {
return;
}
// use uploadFileToOpenAI to upload files to openai
const uploadedFiles: string[] = [];
await Promise.all(
values.files.map(async (file) => {
if (file.filename) {
const uploadedFile = await uploadFileToOpenAI(
file?.filename
);
if (uploadedFile) {
uploadedFiles.push(uploadedFile.id);
}
}
})
);
await openaiInstance.beta.threads
.del(activeThread.id)
.then((res) => {
const newThreadsList = threads.filter(
(thread) => thread.id !== activeThread.id,
);
updateThreads(newThreadsList);
updateActiveThread(newThreadsList?.[0]);
plugin.saveSettings();
})
.catch((error) => {
console.error(error);
});
};
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 createAssistant = async (values: IAssistantEditModalValues) => {
if (!openaiInstance || !app) {
return;
}
const handleCreateAssistant = async (
assistant?: IAssistantEditModalValues
) => {
if (!openaiInstance || !app) {
return;
}
// 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();
};
// use uploadFileToOpenAI to upload files to openai
const uploadedFiles: string[] = [];
await Promise.all(
values.files.map(async (file) => {
if (file.filename) {
const uploadedFile = await uploadFileToOpenAI(
file?.filename,
);
const editAssistant = async (values: IAssistantEditModalValues) => {
if (!openaiInstance || !app || !activeAssistant) {
return;
}
if (uploadedFile) {
uploadedFiles.push(uploadedFile.id);
}
}
}),
);
// 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.
const filesToUpload: string[] = [];
const assistantFiles: string[] = [];
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);
});
};
values.files.forEach((file) => {
if (file.id) {
assistantFiles.push(file.id);
} else if (file.filename) {
filesToUpload.push(file.filename);
}
});
const handleCreateAssistant = async (
assistant?: IAssistantEditModalValues,
) => {
if (!openaiInstance || !app) {
return;
}
await Promise.all(
filesToUpload.map(async (file) => {
const uploadedFile = await uploadFileToOpenAI(file);
// 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();
};
if (uploadedFile) {
assistantFiles.push(uploadedFile.id);
}
})
);
const editAssistant = async (values: IAssistantEditModalValues) => {
if (!openaiInstance || !app || !activeAssistant) {
return;
}
await openaiInstance.beta.assistants
.update(activeAssistant.id, {
name: values.name,
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;
})
);
});
};
// 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.
const filesToUpload: string[] = [];
const assistantFiles: string[] = [];
const handleEditAssistant = async () => {
if (!openaiInstance || !app || !activeAssistant) {
return;
}
values.files.forEach((file) => {
if (file.id) {
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 = {
name: activeAssistant.name || "",
description: activeAssistant.description || "",
instructions: activeAssistant.instructions || "",
files: files || [],
};
if (uploadedFile) {
assistantFiles.push(uploadedFile.id);
}
}),
);
new AssistantEditModal({
app,
title: "Edit Assistant",
submitButtonText: "Edit",
previousValues,
onSubmit: editAssistant,
}).open();
};
await openaiInstance.beta.assistants
.update(activeAssistant.id, {
name: values.name,
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 deleteAssistant = async () => {
if (!openaiInstance || !activeAssistant) {
return;
}
const handleEditAssistant = async () => {
if (!openaiInstance || !app || !activeAssistant) {
return;
}
await openaiInstance.beta.assistants
.del(activeAssistant.id)
.then((res) => {
const newAssistantsList = assistants.filter(
(assistant) => assistant.id !== activeAssistant.id
);
const files = await getAssistantFiles();
updateAssistants(newAssistantsList);
updateActiveAssistant(newAssistantsList?.[0]);
});
};
const previousValues = {
name: activeAssistant.name || '',
description: activeAssistant.description || '',
instructions: activeAssistant.instructions || '',
files: files || [],
};
const uploadFileToOpenAI = async (
fileName: string
): Promise<OpenAI.Files.FileObject | undefined> => {
if (!openaiInstance || !plugin || !app) {
return undefined;
}
new AssistantEditModal({
app,
title: 'Edit Assistant',
submitButtonText: 'Edit',
previousValues,
onSubmit: editAssistant,
}).open();
};
const file = await app.vault.adapter.read(fileName);
const blob = new File([file], fileName, { type: "text/markdown" });
const deleteAssistant = async () => {
if (!openaiInstance || !activeAssistant) {
return;
}
const returnedFile = await openaiInstance.files
.create({
purpose: "assistants",
file: blob,
})
.then((res) => {
return res;
})
.catch((error) => {
console.error(error);
return undefined;
});
await openaiInstance.beta.assistants
.del(activeAssistant.id)
.then((res) => {
const newAssistantsList = assistants.filter(
(assistant) => assistant.id !== activeAssistant.id,
);
return returnedFile;
};
updateAssistants(newAssistantsList);
updateActiveAssistant(newAssistantsList?.[0]);
});
};
// 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
const uploadFileToOpenAI = async (
fileName: string,
): Promise<OpenAI.Files.FileObject | undefined> => {
if (!openaiInstance || !plugin || !app) {
return undefined;
}
// }).catch((error) => {
// // Handle the error
// console.error(error);
// });
// };
const file = await app.vault.adapter.read(fileName);
const blob = new File([file], fileName, { type: 'text/markdown' });
const getAssistantFiles = async (): Promise<
OpenAI.Files.FileObject[] | []
> => {
if (!openaiInstance || !plugin || !activeAssistant) {
return [];
}
const returnedFile = await openaiInstance.files
.create({
purpose: 'assistants',
file: blob,
})
.then((res) => {
return res;
})
.catch((error) => {
console.error(error);
return undefined;
});
try {
const assistantFilesResponse =
await openaiInstance.beta.assistants.files.list(
activeAssistant?.id
);
return returnedFile;
};
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(
assistantFilesResponse.data.map(async (file) => {
try {
const fileInfoResponse =
await openaiInstance.files.retrieve(file.id);
// }).catch((error) => {
// // Handle the error
// console.error(error);
// });
// };
innerFiles.push(fileInfoResponse);
} catch (error) {
console.error(error);
}
})
);
const getAssistantFiles = async (): Promise<
OpenAI.Files.FileObject[] | []
> => {
if (!openaiInstance || !plugin || !activeAssistant) {
return [];
}
return innerFiles;
} catch (error) {
console.error(error);
return [];
}
};
try {
const assistantFilesResponse =
await openaiInstance.beta.assistants.files.list(
activeAssistant?.id,
);
//format assistants into ISelectOption
const assistantOptions = useMemo(() => {
return assistants.map((assistant) => {
return {
label: assistant.name || "",
value: assistant.id,
};
});
}, [assistants]);
const innerFiles: OpenAI.Files.FileObject[] = [];
const threadOptions = useMemo(() => {
return threads.map((thread) => {
return {
label: thread.metadata.name,
value: thread.id,
};
});
}, [threads]);
await Promise.all(
assistantFilesResponse.data.map(async (file) => {
try {
const fileInfoResponse =
await openaiInstance.files.retrieve(file.id);
const onUpdateActiveAssistant = (assistantId: string) => {
if (!openaiInstance || !activeAssistant) {
return;
}
const assistant = assistants.find(
(assistant) => assistant.id === assistantId
);
innerFiles.push(fileInfoResponse);
} catch (error) {
console.error(error);
}
}),
);
updateActiveAssistant(assistant ?? assistants?.[0]);
};
return innerFiles;
} catch (error) {
console.error(error);
return [];
}
};
const onUpdateActiveThread = (threadId: string) => {
if (!openaiInstance || !activeThread) {
return;
}
const newActiveThread = threads.find(
(thread) => thread.id === threadId
);
updateActiveThread(newActiveThread ?? activeThread);
};
//format assistants into ISelectOption
const assistantOptions = useMemo(() => {
return assistants.map((assistant) => {
return {
label: assistant.name || '',
value: assistant.id,
};
});
}, [assistants]);
const threadOptions = useMemo(() => {
return threads.map((thread) => {
return {
label: thread.metadata.name,
value: thread.id,
};
});
}, [threads]);
return (
<div className="chat-top-section-container">
<div className="dropdowns-container">
<div className="dropdown-container">
<Bot size={16} />
<DropdownSelect
items={assistantOptions}
onChange={onUpdateActiveAssistant}
activeItem={activeAssistant?.id || ""}
/>
<div className="dropdown-buttons-container">
<button className="create" onClick={deleteAssistant}>
<Trash2 size={16} />
</button>
<button
className="create"
onClick={handleEditAssistant}
>
<Pencil size={16} />
</button>
<button
className="create"
onClick={() => handleCreateAssistant()}
>
<Plus size={16} />
</button>
</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>
);
const onUpdateActiveAssistant = (assistantId: string) => {
if (!openaiInstance || !activeAssistant) {
return;
}
const assistant = assistants.find(
(assistant) => assistant.id === assistantId,
);
updateActiveAssistant(assistant ?? assistants?.[0]);
};
const onUpdateActiveThread = (threadId: string) => {
if (!openaiInstance || !activeThread) {
return;
}
const newActiveThread = threads.find(
(thread) => thread.id === threadId,
);
updateActiveThread(newActiveThread ?? activeThread);
};
return (
<div className="chat-top-section-container">
<div className="dropdowns-container">
<div className="dropdown-container">
<Bot size={16} />
<DropdownSelect
items={assistantOptions}
onChange={onUpdateActiveAssistant}
activeItem={activeAssistant?.id || ''}
/>
<div className="dropdown-buttons-container">
<button className="create" onClick={deleteAssistant}>
<Trash2 size={16} />
</button>
<button
className="create"
onClick={handleEditAssistant}
>
<Pencil size={16} />
</button>
<button
className="create"
onClick={() => handleCreateAssistant()}
>
<Plus size={16} />
</button>
</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;

View File

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

View File

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

View File

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

View File

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

View File

@ -2,18 +2,18 @@ import { App, ButtonComponent, Modal, Notice, Setting } from 'obsidian';
import * as yup from 'yup';
interface ThreadEditModalProps {
app: App,
title: string,
submitButtonText?: string,
previousValues?: IThreadEditModalValues,
onSubmit: (values: IThreadEditModalValues) => void,
app: App;
title: string;
submitButtonText?: string;
previousValues?: IThreadEditModalValues;
onSubmit: (values: IThreadEditModalValues) => void;
}
export interface IThreadEditModalValues {
metadata: {
name: string;
[key: string]: unknown;
},
};
[key: string]: unknown;
}
@ -46,7 +46,7 @@ export class ThreadEditModal extends Modal {
new Setting(contentEl)
.setName('Name')
.setDesc('The name of the thread')
.addText(text => {
.addText((text) => {
text.setPlaceholder('Enter thread name...')
.onChange((value) => {
this.values.metadata.name = value;
@ -54,7 +54,6 @@ export class ThreadEditModal extends Modal {
.setValue(this.values.metadata.name);
});
const validationSchema = yup.object().shape({
metadata: yup.object().shape({
name: yup.string().required('Name is required'),
@ -63,7 +62,9 @@ export class ThreadEditModal extends Modal {
const checkRequiredFields = async (): Promise<string[]> => {
try {
await validationSchema.validate(this.values, { abortEarly: false });
await validationSchema.validate(this.values, {
abortEarly: false,
});
return [];
} catch (error) {
if (error instanceof yup.ValidationError) {
@ -75,7 +76,7 @@ export class ThreadEditModal extends Modal {
const handleSubmit = async () => {
const missingFields = await checkRequiredFields();
if (missingFields.length > 0) {
new Notice(`Submit Error: \n${missingFields.join('\n')}`);
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
import { App, TAbstractFile, TFile } from "obsidian";
import { TextInputSuggest } from "./suggest";
import { get_tfiles_from_folder } from "@/utils/utils";
import { App, TAbstractFile, TFile } from 'obsidian';
import { TextInputSuggest } from './suggest';
import { get_tfiles_from_folder } from '@/utils/utils';
export class FileSuggest extends TextInputSuggest<TFile> {
private onSelect: (file: string) => void;
@ -10,8 +10,7 @@ export class FileSuggest extends TextInputSuggest<TFile> {
constructor(
app: App,
public inputEl: HTMLInputElement,
onSelect: (file: string) => void
onSelect: (file: string) => void,
) {
super(app, inputEl);
this.onSelect = onSelect;
@ -30,7 +29,7 @@ export class FileSuggest extends TextInputSuggest<TFile> {
all_files.forEach((file: TAbstractFile) => {
if (
file instanceof TFile &&
file.extension === "md" &&
file.extension === 'md' &&
file.path.toLowerCase().contains(lower_input_str)
) {
files.push(file);

View File

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

View File

@ -1,12 +1,12 @@
import OpenAI from "openai";
import OpenAI from 'openai';
export interface IThread extends OpenAI.Beta.Thread {
metadata: {
name: string;
annotationFiles?: ThreadAnnotationFile[];
}
metadata: {
name: string;
annotationFiles?: ThreadAnnotationFile[];
};
}
export interface ThreadAnnotationFile {
fileName: string;
fileId: string;
}
fileName: 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 => {
new Notice(`Obsidian Intelligence: ${message}`, timeout);
};
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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