diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..84b8a66 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,10 @@ +# top-most EditorConfig file +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = tab +indent_size = 4 +tab_width = 4 diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..e019f3c --- /dev/null +++ b/.eslintignore @@ -0,0 +1,3 @@ +node_modules/ + +main.js diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..0807290 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,23 @@ +{ + "root": true, + "parser": "@typescript-eslint/parser", + "env": { "node": true }, + "plugins": [ + "@typescript-eslint" + ], + "extends": [ + "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 newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8cc8b14 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# vscode +.vscode + +# Intellij +*.iml +.idea + +# npm +node_modules + +# Don't include the compiled main.js file in the repo. +# They should be uploaded to GitHub releases instead. +main.js + +# Exclude sourcemaps +*.map + +# obsidian +data.json + +# Exclude macOS Finder (System Explorer) View States +.DS_Store + +package-lock.json diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..b973752 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +tag-version-prefix="" \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..d2b5978 --- /dev/null +++ b/README.md @@ -0,0 +1,100 @@ +# AI Note Suggestion Plugin for Obsidian + +**AI Note Suggestion** plugin for Obsidian is designed to make your note-taking experience even more seamless. It harnesses the power of AI vector search using [Weaviate](https://weaviate.io/) to suggest similar and related notes as you type, reducing your dependency on traditional tagging systems. You can also filter notes by tags, giving you the flexibility you need. + +## Features: +- **AI-Powered Suggestions:** The plugin suggests similar notes based on the content you're currently typing. +- **Related Notes:** Discover related notes that you might have missed, enhancing your note-taking context. +- **Tag Filtering:** If you still prefer using tags, you can filter notes by tags as well. + +## Setting Up AI Note Suggestion + +To use the AI Note Suggestion plugin, you'll need to set up [Weaviate](https://weaviate.io/), an AI vector search engine. We recommend using [Docker Compose](https://docs.docker.com/compose/) for an easier setup. You can also use weaviate cloud service if you don't want to use your local machine as a server. Here are the steps to get started: + +**Step 1: Install Docker** +If you don't have [Docker](https://docs.docker.com/) installed on your machine, you'll need to do so. Docker provides a platform for running [Weaviate](https://weaviate.io/) + +**Step 2: Download Weaviate Using Docker Compose** +You can check out [weaviate's install guides](https://weaviate.io/developers/weaviate/installation) for in depth information or if you are new to this follow instruction bellow, + +1. Create a `docker-compose.yml` file with the following content: + +```yaml + weaviate-obsidian: + container_name: weaviate-obsidian + depends_on: + - t2v-transformers-obsidian + command: + - --host + - 0.0.0.0 + - --port + - '8080' + - --scheme + - http + image: semitechnologies/weaviate + ports: + - 3636:8080 + volumes: + - ./data/weaviate-data:/var/lib/weaviate + restart: unless-stopped + environment: + TRANSFORMERS_INFERENCE_API: 'http://t2v-transformers-obsidian:8080' + QUERY_DEFAULTS_LIMIT: 25 + AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: 'true' + PERSISTENCE_DATA_PATH: '/var/lib/weaviate' + DEFAULT_VECTORIZER_MODULE: 'text2vec-transformers' + ENABLE_MODULES: 'text2vec-transformers' + CLUSTER_HOSTNAME: 'node1' + + + t2v-transformers-obsidian: + container_name: t2v-transformers-obsidian + image: semitechnologies/transformers-inference:sentence-transformers-multi-qa-MiniLM-L6-cos-v1 + restart: unless-stopped + environment: + ENABLE_CUDA: '0' +``` + +2. In the directory where you saved the `docker-compose.yml` file, run the following command +```bash +docker-compose up -d +``` +This command pulls the Weaviate image from Docker Hub and runs it as a container on your local machine. + +**Step 3: Configure AI Note Suggestion** + +1. Once you have Weaviate up and running, go to the settings of the **AI Note Suggestion** plugin in Obsidian. +2. In the plugin settings, provide the **Weaviate Address** where your Weaviate instance is running (usually `http://localhost:3636` if you followed the default settings) + + +Now, you're all set to enjoy the enhanced note-taking experience provided by the AI Note Suggestion plugin! + +## Code blocks for query +This is a simple code blocks for querying similar notes based on given texts + + +~~~markdown +```match +text: one +showPercentage: true +limit: 10 +distanceLimit: .98 +autoCut: 2 +``` +~~~ + + + +## Todo's +- [x] Side pane list +- [x] add yaml for code query for tags +- [x] Code query inside files like +- [x] remove code blocks when update files on weaviate +- [x] extract tags and update file with tags +- [x] Similar notes inside note +- [x] add autocut settings and yaml code +- [x] add distance threshold on settings and yaml code +- [ ] Split notes by regex and upload splits note in vector for long notes +- [ ] add a search command to search by similar text +- [ ] show status on every events (update,sync etc) + diff --git a/SettingTab.ts b/SettingTab.ts new file mode 100644 index 0000000..af3f947 --- /dev/null +++ b/SettingTab.ts @@ -0,0 +1,127 @@ +import { App, Notice, PluginSettingTab, Setting } from 'obsidian'; +import MyPlugin from 'main'; + +export class MatchUpSettingTab extends PluginSettingTab { + plugin: MyPlugin; + + constructor(app: App, plugin: MyPlugin) { + super(app, plugin); + this.plugin = plugin; + } + + display(): void { + const {containerEl} = this; + + containerEl.empty(); + + new Setting(containerEl) + .setName('Weaviate Address') + .setDesc('Enter your weaviate address ex: localhost:3636') + .addText(text => text + .setPlaceholder('ex: localhost:6636') + .setValue(this.plugin.settings.weaviateAddress) + .onChange(async (value) => { + this.plugin.settings.weaviateAddress = value; + await this.plugin.saveSettings(); + })); + + new Setting(containerEl) + .setName('Class') + .setDesc('Weaviate class name') + .addText(text => text + .setPlaceholder('ex: Obsidian') + .setValue(this.plugin.settings.weaviateClass) + .onChange(async (value) => { + this.plugin.settings.weaviateClass = value; + await this.plugin.saveSettings(); + })); + + new Setting(containerEl) + .setName('Suggestion Limit') + .setDesc('Limit for how much result you want to see (max 30)') + .addText(t => t + .setPlaceholder("ex: 30") + .setValue(`${this.plugin.settings.limit}`) + .onChange(async (value) => { + if(parseInt(value)>30){ + t.setValue("30") + }else if (parseInt(value)<=0){ + t.setValue("1") + } + this.plugin.settings.limit = parseInt(value); + await this.plugin.saveSettings(); + })); + + new Setting(containerEl) + .setName('Similarity Distance') + .setDesc('Modify this if you want to set a similarity threshold, 2 is the lowest value (0 to disable). for more information check here \ + https://weaviate.io/developers/weaviate/search/similarity#distance-threshold') + .addText(t => t + .setPlaceholder("ex: 70 ") + .setValue(`${this.plugin.settings.distanceLimit}`) + .onChange(async (value) => { + let newval = value + if(parseFloat(value)>2){ + t.setValue("2") + newval=2 + }else if (parseFloat(value)<0){ + t.setValue("0") + newval=0 + } + this.plugin.settings.distanceLimit = parseFloat(newval); + await this.plugin.saveSettings(); + })); + + new Setting(containerEl) + .setName('Enable Autocut') + .setDesc('Leave it 0 to disable if you don\'t know what is it. For info check here https://weaviate.io/developers/weaviate/search/similarity#autocut ') + .addText(t => t + .setPlaceholder("ex: 1 ") + .setValue(`${this.plugin.settings.autoCut}`) + .onChange(async (value) => { + if (parseInt(value)<0){ + t.setValue("0") + } + this.plugin.settings.autoCut = parseInt(value); + await this.plugin.saveSettings(); + })); + + new Setting(containerEl) + .setName('Show Percentage on query') + .setDesc('Enable this if you want to get the match percentage info in code query') + .addToggle( + t=>t + .setValue(this.plugin.settings.showPercentageOnCodeQuery) + .onChange(async v=>{ + this.plugin.settings.showPercentageOnCodeQuery = v; + await this.plugin.saveSettings(); + })) + + + + new Setting(containerEl) + .setName('Show similar notes on top') + .setDesc('If you enable this , plugin will show related notes on top of the current note') + .addToggle( + t=>t + .setValue(this.plugin.settings.inDocMatchNotes) + .onChange(async v=>{ + this.plugin.settings.inDocMatchNotes = v; + await this.plugin.saveSettings(); + })) + + + + new Setting(containerEl) + .setName('Re-build') + .setDesc('Remove everything from weaviate and rebuild') + .addButton(btn=> btn + .setButtonText("Delete all") + .onClick(()=>{ + new Notice("Removing everything from weaviate") + this.plugin.vectorHelper.deleteAll() + }) + ) + + } +} diff --git a/SimilarNotesPane.ts b/SimilarNotesPane.ts new file mode 100644 index 0000000..af0c985 --- /dev/null +++ b/SimilarNotesPane.ts @@ -0,0 +1,152 @@ +import MyPlugin, { SUGGESTION_EXTENSION_ID } from "main"; +import { ItemView, MarkdownView, WorkspaceLeaf } from "obsidian"; +export const VIEW_TYPE = "similar-notes"; + + +export class SimilarNotesPane extends ItemView { + listEl: HTMLElement; + leaf: WorkspaceLeaf; + myPlugin: MyPlugin; + + constructor(leaf: WorkspaceLeaf, myplugin:MyPlugin) { + super(leaf); + this.leaf=leaf + this.myPlugin=myplugin + } + + + getViewType() { + return VIEW_TYPE; + } + + + async onOpen() { + const container = this.containerEl.children[1]; + container.empty(); + + + this.listEl = container.createDiv() + this.updateView() + + + this.registerEvent(this.app.vault.on('create', (f) => { + this.updateView() + })); + + this.registerEvent(this.app.vault.on('modify', (f) => { + this.updateView() + })); + + this.registerEvent(this.app.vault.on('delete', (f) => { + this.updateView() + })); + + this.registerEvent(this.app.vault.on('rename', (f) => { + this.updateView() + })); + + this.registerEvent(this.app.workspace.on('active-leaf-change',f=>{ + const view = this.app.workspace.getActiveViewOfType(MarkdownView); + const isFile = view?.file?.path + if(isFile){ + this.updateView() + } + })) + + + + } + + + async updateView(){ + if(this.myPlugin.vectorHelper){ + const queryText= await this.myPlugin.getCurrentQuery() + + if(queryText && queryText.trim()){ + + const view = this.app.workspace.getActiveViewOfType(MarkdownView); + const currentFilePath = view?.file?.path + if(!currentFilePath){return} + + // this.myPlugin.vectorHelper.queryText(queryText,this.myPlugin.settings.limit) + this.myPlugin.vectorHelper.queryWithNoteId(currentFilePath,this.myPlugin.settings.limit+1,this.myPlugin.settings.distanceLimit,this.myPlugin.settings.autoCut ) // add one so current can be removed + .then(similarFiles=>{ + if(!similarFiles){return} + + this.listEl.empty() + this.listEl.createEl("h5",{text:"Suggestions",cls:"similar_head"}) + + const fileFromDatabase = similarFiles['data']['Get'][this.myPlugin.settings.weaviateClass] + + // + const view = this.app.workspace.getActiveViewOfType(MarkdownView); + const currentFilePath = view?.file?.path + const cleanFileList = fileFromDatabase.filter(item=>currentFilePath && currentFilePath != item.path) + + cleanFileList.map(file=>{ + + const file_name = file['filename'] + const file_path = file['path'] + const file_similarity = this.myPlugin.convertToSimilarPercentage(file["_additional"]["distance"]) + const opacity_val = parseFloat(file_similarity)*.01 + const itemElement= this.listEl.createEl("div",{cls:"similar_item"}) + + itemElement.createEl("p",{text:file_name,cls:"similar_file_name"}) + itemElement.createEl("p",{text:file_similarity,cls:"similar_percent"}) + itemElement.style.opacity = `${opacity_val}` + + + + itemElement.addEventListener('click', (event: MouseEvent) => { + this.myPlugin.focusFile(file_path) + }); + + itemElement.addEventListener('mouseenter',(event)=>{ + app.workspace.trigger("hover-link",{ + source: SUGGESTION_EXTENSION_ID, + event:event, + hoverParent: itemElement.parentElement, + targetEl: itemElement, + linktext: file['filename'], + sourcePath: file['path'] + }) + }) + + }) + + }) + + }else{ + this.renderEmpty() + } + }else{ + this.renderEmpty() + } + + } + + renderEmpty(){ + this.listEl.empty() + this.listEl.createEl("h5",{text:"Suggestions",cls:"similar_head"}) + + this.listEl.createEl("p",{text:"Nothing to show"}) + this.listEl.createEl("small",{text:"Select any file that have any content in it to show suggestions"}) + + } + + + + async onClose() { + // Nothing to clean up. + } + + getDisplayText() { + return "Suggestion" + } + + getIcon(): string { + return "search" + } + + } + \ No newline at end of file diff --git a/SuggestionExtension.ts b/SuggestionExtension.ts new file mode 100644 index 0000000..b15bd22 --- /dev/null +++ b/SuggestionExtension.ts @@ -0,0 +1,177 @@ +import { + ViewUpdate, + PluginValue, + EditorView, + ViewPlugin + +} from "@codemirror/view"; +import MyPlugin, { SUGGESTION_EXTENSION_ID } from "main"; +import { MarkdownView ,editorInfoField} from "obsidian"; + + + + +export const GetSuggestionExtension=(app:MyPlugin)=> +ViewPlugin.fromClass(class SuggExtension implements PluginValue { + el:HTMLElement + currentFilePath:string + view:EditorView + + + constructor(view: EditorView) { + this.view=view + + const oldEl = view.dom.querySelector("#top-text") as HTMLElement + + if(oldEl){ + this.el=oldEl + return + } + + + const parent = view.dom.querySelector(".cm-sizer") + const addTextBeforeThis = view.dom.querySelector(".cm-contentContainer"); + + this.el = document.createElement("div"); + this.el.addClass("suggestion_on_note") + this.el.id="top-text" + + if(addTextBeforeThis){ + parent?.insertBefore(this.el,addTextBeforeThis) + this.updateList() + } + + this.registerObsidianEvents() + + } + + registerObsidianEvents(){ + app.registerEvent(app.app.vault.on('create', (f) => { + this.updateList() + })); + + app.registerEvent(app.app.vault.on('modify', (f) => { + + this.updateList() + })); + + app.registerEvent(app.app.vault.on('delete', (f) => { + this.updateList() + })); + + app.registerEvent(app.app.vault.on('rename', (f) => { + this.updateList() + })); + + app.registerEvent(app.app.workspace.on('active-leaf-change',f=>{ + const view = app.app.workspace.getActiveViewOfType(MarkdownView); + const isFile = view?.file?.path + if(isFile){ + this.updateList() + } + })) + + } + + async updateList(){ + + if(!app.settings.inDocMatchNotes){ + return + } + const editorField = this.view.state.field(editorInfoField) + if(!editorField){ + return + } + + const file = editorField.file; + if(!file){return} + const currentFilePath = file.path + + + const queryText= await app.getCurrentQuery() + + if(queryText && queryText.trim()){ + + + app.vectorHelper.queryWithNoteId(currentFilePath,app.settings.limit+1, app.settings.distanceLimit,app.settings.autoCut) // adding 1 to exclude current note + // app.vectorHelper.queryWithNoteId(currentFilePath,3) + .then(similarFiles=>{ + if(!similarFiles){return} + console.log("sugg ex similarFiles",similarFiles) + const fileFromDatabase = similarFiles['data']['Get'][app.settings.weaviateClass] + + // // + // const view = app.app.workspace.getActiveViewOfType(MarkdownView); + // const currentFilePath = view?.file?.path + + this.el.empty() + const cleanFileList = fileFromDatabase.filter(item=>currentFilePath && currentFilePath != item.path) + + if(cleanFileList.length>0){ + this.el.createEl("p",{"text":`Similar notes:`,cls:"suggestion_on_note_item_text"}) + + }else{ + this.el.createEl("p",{"text":"No similar file found",cls:"suggestion_on_note_item_text"}) + } + + + cleanFileList.map(file=>{ + + const file_name = file['filename'] + const file_similarity = app.convertToSimilarPercentage(file["_additional"]["distance"]) + const opacity_val = parseFloat(file_similarity)*.01 + // const itemElement= this.el.createEl("p",{cls:"suggestion_on_note_item"}) + + // const itemElement= this.el.createEl("p",{text:file_name,cls:"suggestion_on_note_item"}) + const itemElement= this.el.createEl("a",{"text":file_name,"href":file['filepath'],cls:"suggestion_on_note_item"}) + // itemElement.createEl("p",{text:file_similarity,cls:"similar_percent"}) + itemElement.style.opacity = `${opacity_val}` + + + + itemElement.addEventListener('click', (event: MouseEvent) => { + app.focusFile(file['path']) + }); + + itemElement.addEventListener('mouseenter',(event)=>{ + app.app.workspace.trigger("hover-link",{ + source: SUGGESTION_EXTENSION_ID, + event:event, + hoverParent: itemElement.parentElement, + targetEl: itemElement, + linktext: file['filename'], + sourcePath: file['path'] + }) + }) + + }) + + + }) + } + + } + + + + + async update(update: ViewUpdate) { + // const file:TFile = this.view.state.field(editorInfoField).file ; + + // if (!file) { + // this.decorations = Decoration.none; + // return; + // } + // if (update.docChanged) { + // console.log("doc change") + // // this.updateList() + // }else if (update.viewportChanged) { + // console.log("viewportChanged") + // // this.updateList() + // } + } + + destroy() { } + + +}) diff --git a/esbuild.config.mjs b/esbuild.config.mjs new file mode 100644 index 0000000..b13282b --- /dev/null +++ b/esbuild.config.mjs @@ -0,0 +1,48 @@ +import esbuild from "esbuild"; +import process from "process"; +import builtins from "builtin-modules"; + +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 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", +}); + +if (prod) { + await context.rebuild(); + process.exit(0); +} else { + await context.watch(); +} \ No newline at end of file diff --git a/graph.ts b/graph.ts new file mode 100644 index 0000000..3b6abd9 --- /dev/null +++ b/graph.ts @@ -0,0 +1,185 @@ +import MyPlugin from "main"; +import { ItemView, MarkdownView, Notice, WorkspaceLeaf } from "obsidian"; +import { Chart, registerables } from "chart.js"; +import zoomPlugin from 'chartjs-plugin-zoom'; + + +export const GRAPH_VIEW_TYPE = "graph-similar-notes"; + + +export class GraphSimilarView extends ItemView { + mainCanvas: HTMLElement; + leaf: WorkspaceLeaf; + myPlugin: MyPlugin; + + constructor(leaf: WorkspaceLeaf, myplugin:MyPlugin) { + super(leaf); + this.leaf=leaf + this.myPlugin=myplugin + } + + + async onOpen() { + Chart.register(zoomPlugin) + Chart.register(...registerables); + const container = this.containerEl.children[1]; + container.empty(); + // this.viewEl = container.createDiv() + // this.viewEl = this.containerEl.children[1].createDiv() + this.mainCanvas = this.containerEl.createEl("canvas",{cls:"graph_canvas"}) + this.updateView() + + } + + async updateView(){ + + + const data = { + datasets: [{ + label: 'Notes', + pointRadius: 7, + // pointHitRadius: 10, + data: await this.getDataPoints(), + backgroundColor: 'rgb(255, 99, 132)', + + }], + }; + + console.log("data config",data) + + + const ctx = this.mainCanvas.getContext('2d') + if(ctx){ + const options = { + responsive: true, + maintainAspectRatio: false, + scales: { + yAxes: [{ + ticks: { + beginAtZero:true + } + }], + x: { + display: false, + }, + y: { + display: false, + }, + }, + onClick:function(ev,ctx){ + const index= parseInt(ctx[0]["index"]) + const path = data["datasets"][0]["data"][index]["file_path"] + // if(path){ + + // } + + }, + plugins: { + zoom:{ + zoom: { + wheel: { + enabled: true, + }, + pinch: { + enabled: true + }, + mode: 'xy', + drag:true, + sensitivity:0.2 + // drag:{ + // enabled:true + // } + } + } + , + legend: { + display: false, // Hide the legend + }, + tooltip: { + dragData: { + showTooltip: true, // Display tooltips during dragging + }, + zoom: { + zoom: { + wheel: { + enabled: true, + }, + pinch: { + enabled: true + }, + mode: 'xy', + } + }, + callbacks: { + label: function(ctx) { + // console.log(ctx); + // let label = ctx.dataset.labels[ctx.dataIndex]; + const label = data["datasets"][0]["data"][ctx.dataIndex]["name"] + const path = data["datasets"][0]["data"][ctx.dataIndex]["path"] + + return label; + } + } + } + } + } + + new Chart(ctx, { + type: 'scatter', + data: data, + options: options + }); + } + + } + + + async getDataPoints(){ + const files= this.app.vault.getMarkdownFiles() + let file_path = "" + + for (let i =0;i{ + const x = v["_additional"]["featureProjection"]["vector"][0] + const y = v["_additional"]["featureProjection"]["vector"][1] + const path = v["path"] + const name = v["filename"] + + dp.push({x:x,y:y,file_path:path,name:name}) + }) + + return dp + } + + + + async onClose() { + // Nothing to clean up. + } + + getIcon(): string { + return "search" + } + + getViewType(): string { + return GRAPH_VIEW_TYPE + } + getDisplayText(): string { + return "Match up" + } + +} \ No newline at end of file diff --git a/main.ts b/main.ts new file mode 100644 index 0000000..8831ca5 --- /dev/null +++ b/main.ts @@ -0,0 +1,413 @@ + +import { MarkdownView, Plugin, SuggestModal, Notice, parseYaml, TFile } from 'obsidian'; +import { SimilarNotesPane } from 'SimilarNotesPane'; +import VectorHelper from 'vec'; +import { MatchUpSettingTab } from 'SettingTab'; +import { GRAPH_VIEW_TYPE, GraphSimilarView } from 'graph'; +import { GetSuggestionExtension } from 'SuggestionExtension'; + +export const VIEW_TYPE = "similar-notes"; +export const SUGGESTION_EXTENSION_ID = "similar-SUGGESTION_EXTENSION_ID"; + + +interface CodeYaml { + text: string; + tags: string[]; + limit: number + showPercentage:boolean + autoCut: number + distanceLimit: number +} + +interface MyPluginSettings { + weaviateAddress: string + weaviateClass: string + limit: number + inDocMatchNotes: boolean + showPercentageOnCodeQuery:boolean + autoCut: number + distanceLimit: number +} + + + +const DEFAULT_SETTINGS: MyPluginSettings = { + weaviateAddress: 'http://192.168.0.120:3636', + weaviateClass: 'ObsidianVectors', + limit: 30, + inDocMatchNotes: true, + showPercentageOnCodeQuery:false, + autoCut: 0, + distanceLimit: 0 +} + +export default class MyPlugin extends Plugin { + settings: MyPluginSettings; + vectorHelper: VectorHelper + statusBarItemEl: HTMLElement + similarNotesPane: SimilarNotesPane + + // + timeOut = 1000 * 2 + + async onload() { + await this.loadSettings(); + this.registerView( + VIEW_TYPE, + (leaf) => new SimilarNotesPane(leaf, this) + ); + this.registerView( + GRAPH_VIEW_TYPE, + (leaf) => new GraphSimilarView(leaf, this) + ); + this.addSettingTab(new MatchUpSettingTab(this.app, this)); + this.statusBarItemEl = this.addStatusBarItem() + this.statusBarItemEl.setText('Starting...'); + + + this.app.workspace.onLayoutReady(() => { + this.vectorHelper = new VectorHelper( + this.settings.weaviateAddress, + this.settings.weaviateClass, + this.settings.limit, + this + ) + this.vectorHelper.initClass() + .then(classExist => this.scanAllFile()) + + this.registerEvents() + this.registerCommands() + this.registerCodeView() + this.registerEditorExtension(GetSuggestionExtension(this)); + + this.registerHoverLinkSource(SUGGESTION_EXTENSION_ID,{ + display: 'Match Up', + defaultMod: true, + }) + + + }) + } + + async scanAllFile() { + const files = this.app.vault.getMarkdownFiles() + const count = files.length + let doneScanning = 0 + const countOnDatabase = await this.vectorHelper.countOnDatabase() + + // push new files to database _____________________________________________________ + files.map(async (f, i) => { + + await this.app.vault.cachedRead(f).then(async content => { + await this.vectorHelper.onUpdateFile(content, f.path, f.name, f.stat.mtime) + .then(() => { + doneScanning++ + this.updateStatus(`Scanning ${doneScanning}/${count}`) + }) + .catch((e) => { + doneScanning++ + this.updateStatus(`Scanning ${doneScanning}/${count}`) + }) + }) + + if (count == (i + 1)) { + this.updateStatus(`Done scanning`, true) + this.updateStatus(`Sitting idle`) + } + }) + + // delete old file from database _____________________________________________________ + if (countOnDatabase > count) { + console.log(`Something has been deleted on local, ${countOnDatabase}, ${count}`) + const weaviateFiles = await this.vectorHelper.readAllPaths() + const extraFiles = this.findExtraFiles(weaviateFiles, files) + + extraFiles.map(extra => { + const extraPath = extra["path"] + console.log("delete with path", extraPath) + this.vectorHelper.onDeleteFile(extraPath) + }) + } + + // if no file is there________________________________________________________________ + if (count == 0) { + this.updateStatus(`Nothing to scan`, true) + this.updateStatus(`Sitting idle`) + } + } + + updateStatus(text: string, delay = false) { + if (delay) { + this.statusBarItemEl.setText(text) + } else { + setTimeout(() => this.statusBarItemEl.setText(text), this.timeOut) + } + } + + findExtraFiles(weaviateFiles, localFiles: TFile[]) { + console.log("weaviate", weaviateFiles) + const extraFiles = weaviateFiles.filter((weaviateFile) => !localFiles.some((file) => file.path === weaviateFile["path"])); + return extraFiles + } + + registerEvents() { + + this.registerEvent(this.app.vault.on('create', (f) => { + this.app.vault.cachedRead(f).then(content => { + if (content) { + console.log(`create: ${f.path}`) + this.vectorHelper.onUpdateFile(content, f.path, f.name, f.stat.mtime) + } + }) + })) + + this.registerEvent(this.app.vault.on('modify', (f) => { + this.app.vault.cachedRead(f).then(content => { + + + if (content) { + console.log(`update: ${f.path}`) + this.vectorHelper.onUpdateFile(content, f.path, f.name, f.stat.mtime) + } else { + console.log(`delete file on update: ${f.path}`) + this.vectorHelper.onDeleteFile(f.path) + } + }) + })) + + this.registerEvent(this.app.vault.on('rename', (f, oldPath) => { + this.app.vault.cachedRead(f).then(content => { + if (content) { + console.log(`rename: ${f.path}`) + this.vectorHelper.onRename(content, f.path, f.name, f.stat.mtime, oldPath) + } + }) + + })) + + this.registerEvent(this.app.vault.on('delete', (f) => { + this.vectorHelper.onDeleteFile(f.path) + })) + } + + registerCommands() { + this.addCommand({ + id: 'open-note-suggestion', + name: 'Open note suggestions (Match up)', + callback: () => { + this.activateView() + } + }); + + // this.addCommand({ + // id: 'open--big-note-suggestion', + // name: 'Open notes cluster (Match up)', + // callback: () => { + // this.activeBigView() + // } + // }); + + // + // this.addCommand({ + // id: 'search-modal-note-suggestion', + // name: 'Search similar notes (Match up)', + // callback: () => { + // new ExampleModal(this.app).open() + // } + // }); + + } + + + + registerCodeView() { + this.registerMarkdownCodeBlockProcessor("match", (source, el, ctx) => { + + const codeYaml: CodeYaml = parseYaml(source) + // console.log("codeYaml",codeYaml) + const text = codeYaml.text ? codeYaml.text : "" + const tags = codeYaml.tags ? codeYaml.tags : [] + const limit = codeYaml.limit ? codeYaml.limit : this.settings.limit + const yamlAutoCut = codeYaml.autoCut != null ? codeYaml.autoCut : this.settings.autoCut + const yamlDistanceLimit = codeYaml.distanceLimit != null ? codeYaml.distanceLimit : this.settings.distanceLimit + const showPercentage = codeYaml.showPercentage ? codeYaml.showPercentage : this.settings.showPercentageOnCodeQuery + + + this.vectorHelper.queryText(text, tags, limit,yamlDistanceLimit,yamlAutoCut) + .then(similarFiles => { + if (!similarFiles) { + el.createEl('div', { text: "Match up: No file matches!", cls: "empty_match" }) + return + } + // listEl.createEl("h5",{text:"Suggestions",cls:"similar_head"}) + const fileFromDatabase = similarFiles['data']['Get'][this.settings.weaviateClass] + + + const view = this.app.workspace.getActiveViewOfType(MarkdownView); + const currentFilePath = view?.file?.path + const cleanFileList = fileFromDatabase.filter(item => currentFilePath && currentFilePath != item.path) + + if (cleanFileList.length === 0) { + el.createEl('small', { text: "Match up: No file matches!", cls: "empty_match" }) + } + + const listEl = el.createEl('ul', { cls: "similar_list_parent" }) + cleanFileList.map(file => { + + const file_name = file['filename'] + const file_similarity = this.convertToSimilarPercentage(file["_additional"]["distance"]) + + const i = listEl.createEl("li") + + + + const itemElement = i.createEl("a", { + "text": showPercentage? `${file_name} - ${file_similarity}`: `${file_name}`, + "href": file['filepath'] + }) + + itemElement.addEventListener('click', (event: MouseEvent) => { + this.focusFile(file['path']) + }); + + itemElement.addEventListener('mouseenter',(event)=>{ + this.app.workspace.trigger("hover-link",{ + source: SUGGESTION_EXTENSION_ID, + event:event, + hoverParent: itemElement.parentElement, + targetEl: itemElement, + linktext: file['filename'], + sourcePath: file['path'] + }) + }) + + }) + + }) + }) + } + + + async loadSettings() { + this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); + } + + async saveSettings() { + await this.saveData(this.settings); + console.log("save settings") + } + + async activateView() { + this.app.workspace.detachLeavesOfType(VIEW_TYPE); + + await this.app.workspace.getRightLeaf(false).setViewState({ + type: VIEW_TYPE, + active: true, + }); + + this.app.workspace.revealLeaf( + this.app.workspace.getLeavesOfType(VIEW_TYPE)[0] + ); + + } + async activeBigView() { + const leaf = this.app.workspace.getLeaf("tab") + leaf.setViewState({ "type": GRAPH_VIEW_TYPE, active: true }) + } + + focusFile(filePath: string, shouldSplit = false) { + const targetFile = this.app.vault + .getFiles() + .find((f) => f.path === filePath) + // const otherLeaf = this.app.workspace.getLeaf('split'); + const currentLeaf = this.app.workspace.getMostRecentLeaf() + + if (targetFile) { + currentLeaf?.openFile(targetFile, { active: true }) + } + } + + async getCurrentQuery() { + + const view = this.app.workspace.getActiveViewOfType(MarkdownView); + const isFile = view?.file?.path + + if (isFile) { + const activeFile = this.app.workspace.getActiveFile() + if (!activeFile) { return null } + const query = await this.app.vault.cachedRead(activeFile) + return query + } else { + return null + } + } + + convertToSimilarPercentage(cosine: number) { + const percentage = ((50 * cosine) - 100) * -1; + return percentage.toFixed(2) + "%"; + } + + + async readFileWithPath(path: string) { + const file: TFile = this.app.vault.getAbstractFileByPath(path) + return this.app.vault.cachedRead(file) + } + + + onunload() { + + } + +} + + +interface Book { + title: string; + author: string; +} + +const ALL_BOOKS = [ + { + title: "How to Take Smart Notes", + author: "Sönke Ahrens", + }, + { + title: "Thinking, Fast and Slow", + author: "Daniel Kahneman", + }, + { + title: "Deep Work", + author: "Cal Newport", + }, +]; + + +export class ExampleModal extends SuggestModal { + // Returns all available suggestions. + getSuggestions(query: string): Book[] { + return ALL_BOOKS.filter((book) => + book.title.toLowerCase().includes(query.toLowerCase()) + ); + } + + + + // Renders each suggestion item. + renderSuggestion(book: Book, el: HTMLElement) { + const c = this.inputEl.getText() + if (c) { + el.createEl("div", { text: c }); + } else { + + el.createEl("div", { text: book.title }); + } + el.createEl("small", { text: book.author }); + } + + // Perform action on the selected suggestion. + onChooseSuggestion(book: Book, evt: MouseEvent | KeyboardEvent) { + new Notice(`Selected ${book.title}`); + } +} + + diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..29a5a5a --- /dev/null +++ b/manifest.json @@ -0,0 +1,11 @@ +{ + "id": "ai-note-suggestion", + "name": "AI Note Suggestion", + "version": "0.0.1", + "minAppVersion": "0.15.0", + "description": "This plugin shows semantically related notes and filer notes by tags using power of A.I (Vector search)", + "author": "echo-saurav", + "authorUrl": "", + "fundingUrl": "", + "isDesktopOnly": false +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..b1acc33 --- /dev/null +++ b/package.json @@ -0,0 +1,32 @@ +{ + "name": "match-up", + "version": "1.0.0", + "description": "Its a plugin that shows semantically related notes using power of A.I", + "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": { + "chart.js": "^4.4.0", + "chartjs-plugin-zoom": "^2.0.1", + "marked": "^9.1.2", + "md-2-json": "^2.0.0", + "weaviate-ts-client": "^1.5.0", + "yaml": "^2.3.3" + } +} diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..c39bbb0 --- /dev/null +++ b/styles.css @@ -0,0 +1,76 @@ +.similar_file_name{ + color: var(--color-accent-1); + margin: 0; +} + +.similar_percent{ + font-size: 10px; + margin-top: 5px; + margin-bottom: 0; + font-style: italic; +} + +.similar_item{ + /* margin-top: 13px; */ + cursor: pointer; + padding: 7px; +} +.similar_item:hover{ + background: var(--background-modifier-hover); + border-radius: 5px; +} + +.graph_canvas{ + + /* width:100svw !important; */ + /* height:100svh !important; */ +} + +#top-text{ + +} + + +/* code view */ + +.similar_list_parent{ + +} + +.similar_list_item{ + cursor: pointer; + color: var(--color-accent-1); +} + + +.suggestion_on_note{ + overflow-x: scroll; + white-space: nowrap; + padding-bottom: 15px; + width: var(--file-line-width) !important; + margin: auto; + +} + +.suggestion_on_note_item{ + display: inline; + margin: 5px; + + color: var(--color-accent-1); +} + +.suggestion_on_note_item_text{ + display: inline; + margin: 0px; + color: var(--text-muted); + font-weight: bold; +} + + +.empty_match{ + padding: 40px; + font-style: italic; + color: var(--text-muted); + width: var(--file-line-width) !important; + /* background-color: antiquewhite; */ +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..2d6fbdf --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "inlineSourceMap": true, + "inlineSources": true, + "module": "ESNext", + "target": "ES6", + "allowJs": true, + "noImplicitAny": true, + "moduleResolution": "node", + "importHelpers": true, + "isolatedModules": true, + "strictNullChecks": true, + "lib": [ + "DOM", + "ES5", + "ES6", + "ES7" + ] + }, + "include": [ + "**/*.ts" + ] +} diff --git a/vec.ts b/vec.ts new file mode 100644 index 0000000..e2f9944 --- /dev/null +++ b/vec.ts @@ -0,0 +1,514 @@ +import MyPlugin from "main" +import { Notice } from "obsidian" +import weaviate, { WeaviateClient, generateUuid5 } from "weaviate-ts-client" + + + +export default class VectorHelper { + private client: WeaviateClient + private plugin: MyPlugin + private weaviateAddress: string + private weaviateClass: string + private limit: number + + + constructor(weaviateAddress: string, weaviateClass: string, limit: number, plugin: MyPlugin) { + this.weaviateAddress = weaviateAddress + this.weaviateClass = weaviateClass + this.limit = limit + this.plugin = plugin + + // this.client = weaviate.client({ + // scheme: scheme, + // host: weaviateAddress, // Replace with your endpoint + // }); + this.client = weaviate.client(this.getWeaviateConf(weaviateAddress)) + + } + + getWeaviateConf(weaviateAddress: string) { + const scheme = weaviateAddress.startsWith("http://") ? 'http' : 'https' + let host = "" + + if (weaviateAddress.startsWith('http://')) { + host = weaviateAddress.slice(7); // Remove the first 7 characters (http://:) + } else { + host = weaviateAddress.slice(8); // Remove the first 8 characters (https://:) + } + console.log(`host ${host}, scheme ${scheme}`) + return { + host: host, + scheme: scheme + } + } + + async initClass() { + // get classes + const classDefinition = await this.client + .schema + .getter() + .do(); + + // check if class exist + let classExist = false + classDefinition.classes?.forEach(classObj => { + + if (classObj.class == this.weaviateClass) { + classExist = true + } + }) + + if (!classExist) { + + const result = await this.client.schema + .classCreator() + .withClass(this.getDefaultClassDefinition()) + .do() + console.log("create class", JSON.stringify(result, null, 2)); + } + return classExist + } + + + + async onRename(content: string, path: string, filename: string, mtime: number, oldPath: string) { + this.doesExist(oldPath).then(response => { + + this.client.data + .merger() // merges properties into the object + .withId(response[1]).withClassName(this.weaviateClass) + .withProperties({ + path: path, + filename: filename, + content: content, + mtime: this.unixTimestampToRFC3339(mtime) + }) + .do(); + }) + + + } + + // update notes if needed , add new if not exist in weaviate database + async onUpdateFile(content: string, path: string, filename: string, mtime: number) { + const res = await this.doesExist(path) + const doesExist = res[0] + const id = res[1] + const oldMtime = res[2] + const isUpdated = (mtime - this.rfc3339ToUnixTimestamp(oldMtime)) > 0 + + const cleanContent = this.getCleanDoc(content) + const tags = this.getAllTags(content) + const metadata = this.extractYAMLWithoutDashes(content) + + // const yamlContent = this.objectToArray(this.extractYAMLWithoutDashes(content)) + + if (doesExist && isUpdated) { + console.log("updating " + path) + const newValue = { + content: cleanContent, + metadata: metadata, + tags: tags, + mtime: this.unixTimestampToRFC3339(mtime) + } + + console.log("newValue",newValue) + + await this.client.data + .merger() // merges properties into the object + .withId(id).withClassName(this.weaviateClass) + .withProperties(newValue) + .do(); + + console.log("update note: " + filename + " time:" + this.unixTimestampToRFC3339(mtime)) + } else if (!doesExist && isUpdated) { + console.log("adding " + path) + this.addNew(content, path, filename, mtime) + } + } + + async countOnDatabase() { + const response = await this.client.graphql + .aggregate() + .withClassName(this.weaviateClass) + .withFields('meta { count }') + .do(); + + const count = response.data["Aggregate"][this.weaviateClass][0]["meta"]["count"] + return count + } + + + + + async onDeleteFile(path: string) { + + return this.client.data + .deleter() + .withClassName(this.weaviateClass) + .withId(generateUuid5(path)) + .do() + } + + async deleteAll() { + const result = await this.client.schema.classDeleter() + .withClassName(this.weaviateClass) + .do(); + + console.log("delete all", result) + this.initClass().then(() => { + this.addAllFiles() + }) + } + + async doesExist(path: string) { + const result = await this.client.graphql + .get() + .withClassName(this.weaviateClass) + .withWhere({ + path: ['path'], + operator: 'Equal', + valueText: path, + }) + .withFields(["filename", "mtime"].join(' ') + ' _additional { id }') + .do() + + const resultLength = result.data["Get"][this.weaviateClass].length + + if (resultLength > 0) { + const id = result.data["Get"][this.weaviateClass][0]["_additional"]["id"] + const mtime = result.data["Get"][this.weaviateClass][0]["mtime"] + + return [true, id, mtime] + } else { + return [false, 0, 0] + } + } + + async readAllPaths() { + const classProperties = ["path"]; + + const query = await this.client.graphql.get() + .withClassName(this.weaviateClass) + .withFields(classProperties.join(' ') + ' _additional { id }') + .withLimit(this.limit).do(); + + const files: Array = query['data']['Get'][this.plugin.settings.weaviateClass] + return files + + } + + + + async addNew(content: string, path: string, filename: string, mtime: number) { + + const cleanContent = this.getCleanDoc(content) + + const dataObj = { + filename: filename, + content: cleanContent, + path: path, + mtime: this.unixTimestampToRFC3339(mtime) + } + + const note_id = generateUuid5(path) + + console.log("add new note: " + filename + " time:" + this.unixTimestampToRFC3339(mtime)) + return this.client.data + .creator() + .withClassName(this.weaviateClass) + .withProperties(dataObj) + .withId(note_id) + .do() + .catch(e => { }) + + + } + + async addAllFiles() { + + new Notice("Start adding all files, please wait before searching...") + const files = await this.plugin.app.vault.getMarkdownFiles() + this.plugin.statusBarItemEl.setText(`Uplaoding ${files.length}`) + + + files.map((f, i) => { + this.plugin.app.vault.cachedRead(f).then(content => { + + this.addNew(content, f.path, f.basename, f.stat.mtime) + .then(() => { + this.plugin.statusBarItemEl.setText(`Uplaoding ${i + 1}/${files.length}`) + }) + .catch((e) => { + this.plugin.statusBarItemEl.setText(`Uplaoding ${i + 1}/${files.length}`) + }) + + }); + + }); + } + + async queryText(text: string,tags:string[], limit: number,distanceLimit:number,autoCut:number) { + console.log(`auto cut: ${autoCut}, dis: ${distanceLimit}`) + let nearText: { concepts: string[], distance?: number } = { concepts: [text] }; + + if(distanceLimit>0){ + nearText = { concepts: [text] , distance: distanceLimit} + } + + const result = await this.client.graphql + .get() + .withClassName(this.weaviateClass) + .withNearText(nearText) + + if(tags && tags.length >0){ + result.withWhere({ + path: ["tags"], + operator: "ContainsAny", + valueTextArray: tags + }) + } + if(autoCut>0){ + result.withAutocut(autoCut) + } + + result.withLimit(limit) + .withFields('filename path _additional { distance }') + // .do() + // .catch(e => { }) + const response = await result + .do() + .catch(e => { }) + + return response + + } + + + async queryWithNoteId(filePath: string, limit: number, distanceLimit:number,autoCut:number) { + const note_id = generateUuid5(filePath) + + let nearObject: { id: string, distance?: number } = { id: note_id }; + + if(distanceLimit>0){ + nearObject = { id: note_id , distance: distanceLimit} + } + + + const result = this.client.graphql + .get() + .withClassName(this.weaviateClass) + .withNearObject(nearObject) + .withLimit(limit) + + if(autoCut>0){ + result.withAutocut(autoCut) + } + + const response = result + .withFields('filename path _additional { distance }') + .do() + .catch(e => { }) + + return response + + + // const result = await this.client.graphql + // .get() + // .withClassName(this.weaviateClass) + // .withNearObject({ id: note_id }) + // .withLimit(limit) + // .withFields('filename path _additional { distance }') + // .do() + // .catch(e => { }) + + // return result + } + + async twoDimensionQuery(filePath: string, limit: number) { + const note_id = generateUuid5(filePath) + + const result = await this.client.graphql + .get() + .withClassName(this.weaviateClass) + .withNearObject({ id: note_id }) + .withLimit(limit) + .withFields('filename path _additional { featureProjection(dimensions: 2) { vector } }') + .do() + .catch(e => { }) + + return result + + } + + + + + unixTimestampToRFC3339(unixTimestamp: number): string { + const date = new Date(unixTimestamp); + const isoString = date.toISOString(); + return isoString; + } + + rfc3339ToUnixTimestamp(rfc3339: string): number { + const date = new Date(rfc3339); + return date.getTime(); + } + + + getCleanDoc(markdownContent: string) { + // Define a regular expression to match YAML front matter + const yamlFrontMatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/; + + // Define a regular expression to match code blocks + const codeBlockRegex = /```[^`]*```/g; + + // Remove YAML front matter + const markdownWithoutYAML = markdownContent.replace(yamlFrontMatterRegex, ''); + + // Remove code blocks + const markdownWithoutCodeBlocks = markdownWithoutYAML.replace(codeBlockRegex, ''); + + return markdownWithoutCodeBlocks + } + + extractYAMLWithoutDashes(markdownContent: string) { + // Define a regular expression to match YAML front matter without the dashes + const yamlFrontMatterRegex = /^---\s*\n([\s\S]*?)\n---\s*\n/; + + // Use the regular expression to extract YAML content + const match = markdownContent.match(yamlFrontMatterRegex); + + // If a match is found, return the YAML content without dashes + + if (match && match[1]) { + const yaml_string = match[1].trim(); + return yaml_string + // return parseYaml(yaml_string) + } else { + return ""; + } + } + + objectToArray(obj) { + const result = []; + if (!obj || obj.length === 0) { + return result + } + + for (const key in obj) { + if (obj.hasOwnProperty(key)) { + // Skip the "tags" key + if (key === 'tags') { + continue; + } + + let value = obj[key]; + + if (value) { + if (value.length === 1) { + value = value[0]; + } + result.push({ name: key, value }); + } + } + } + + return result; + } + + + getAllTags(inputString: string) { + const yaml = this.extractYAMLWithoutDashes(inputString) + const yamlTags: Array = yaml["tags"] ? yaml["tags"] : [] + + const regex = /#(\w+)/g; + + const tags = inputString.match(regex); + const cleanTags = tags ? tags.map(match => match.slice(1)) : [] + + if (tags || yamlTags) { + return yamlTags.concat(cleanTags) + } else { + return [] + } + } + + + getDefaultClassDefinition() { + + const classDefinition = { + class: this.weaviateClass, + properties: [ + { + "name": "content", + "datatype": ["text"], + "moduleConfig": { + "text2vec-transformers": { + "skip": false, + "vectorizePropertyName": false + } + } + }, + { + "name": "metadata", + "datatype": ["text"], + "moduleConfig": { + "text2vec-transformers": { + "skip": false, + "vectorizePropertyName": false + } + } + }, + { + "name": "tags", + "datatype": ["text[]"], + "moduleConfig": { + "text2vec-transformers": { + "skip": false, + "vectorizePropertyName": false + } + } + }, + + { + "name": "path", + "datatype": ["text"], + "moduleConfig": { + "text2vec-transformers": { + "skip": true, + "vectorizePropertyName": false + } + } + }, + { + "name": "filename", + "datatype": ["text"], + "moduleConfig": { + "text2vec-transformers": { + "skip": false, + "vectorizePropertyName": false + } + } + }, + { + "name": "mtime", + "datatype": ["date"], + "moduleConfig": { + "text2vec-transformers": { + "skip": false, + "vectorizePropertyName": false + } + } + }, + + ], + "vectorizer": "text2vec-transformers" + }; + + return classDefinition + } + +} + + diff --git a/version-bump.mjs b/version-bump.mjs new file mode 100644 index 0000000..d409fa0 --- /dev/null +++ b/version-bump.mjs @@ -0,0 +1,14 @@ +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")); +const { minAppVersion } = manifest; +manifest.version = targetVersion; +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")); +versions[targetVersion] = minAppVersion; +writeFileSync("versions.json", JSON.stringify(versions, null, "\t")); diff --git a/versions.json b/versions.json new file mode 100644 index 0000000..26382a1 --- /dev/null +++ b/versions.json @@ -0,0 +1,3 @@ +{ + "1.0.0": "0.15.0" +}