From 0aee3e1cff2fb2ec7fca1c2f941b9d362e168f7e Mon Sep 17 00:00:00 2001 From: celeste Date: Tue, 3 Sep 2024 20:07:46 -0700 Subject: [PATCH] Add OpenRouter + other minor improvements --- common.ts | 28 +- main.ts | 2135 +++++++++++++++++++++++++++++-------------------- manifest.json | 2 +- package.json | 2 +- views.ts | 1259 ++++++++++++++++------------- 5 files changed, 1975 insertions(+), 1451 deletions(-) diff --git a/common.ts b/common.ts index c964b0d..f08cef2 100644 --- a/common.ts +++ b/common.ts @@ -1,13 +1,24 @@ -export const PROVIDERS = ["cohere", "textsynth", "ocp", "openai", "openai-chat", "azure", "azure-chat", "anthropic"]; +export const PROVIDERS = [ + "cohere", + "textsynth", + "openai-compat", + "openai", + "openai-chat", + "azure", + "azure-chat", + "anthropic", + "openrouter", +]; export type Provider = (typeof PROVIDERS)[number]; type ProviderProps = { - "openai": { organization: string }; + openai: { organization: string }; "openai-chat": { organization: string }; - "ocp": { url: string }; - "azure": { url: string }; + "openai-compat": { url: string }; + azure: { url: string }; "azure-chat": { url: string }; - "anthropic": { url: string };//, systemPrompt: string, userMessage: string }; + anthropic: { url: string }; + openrouter: { quantization: string }; }; type SharedPresetSettings = { @@ -18,7 +29,8 @@ type SharedPresetSettings = { apiKey: string; }; -export type ModelPreset

= SharedPresetSettings & (P extends keyof ProviderProps ? ProviderProps[P] : {}) & { provider: P }; +export type ModelPreset

= SharedPresetSettings & + (P extends keyof ProviderProps ? ProviderProps[P] : {}) & { provider: P }; export interface LoomSettings { passageFolder: string; @@ -46,10 +58,10 @@ export interface LoomSettings { showSearchBar: boolean; showNodeBorders: boolean; showExport: boolean; - } -export const getPreset = (settings: LoomSettings) => settings.modelPresets[settings.modelPreset]; +export const getPreset = (settings: LoomSettings) => + settings.modelPresets[settings.modelPreset]; export type SearchResultState = "result" | "ancestor" | "none" | null; diff --git a/main.ts b/main.ts index dfdf680..f31d5c1 100644 --- a/main.ts +++ b/main.ts @@ -4,7 +4,7 @@ import { LoomEditorPlugin, loomEditorPluginSpec, MakePromptFromPassagesModal, -} from './views'; +} from "./views"; import { Provider, ModelPreset, @@ -13,12 +13,7 @@ import { Node, NoteState, getPreset, -} from './common'; - -// import { -// claudeCompletion, -// } from './claude'; - +} from "./common"; import { App, Editor, @@ -33,11 +28,13 @@ import { } from "obsidian"; import { ViewPlugin } from "@codemirror/view"; -import { Configuration as AzureConfiguration, OpenAIApi as AzureOpenAIApi} from "azure-openai"; +import { + Configuration as AzureConfiguration, + OpenAIApi as AzureOpenAIApi, +} from "azure-openai"; import { Configuration, OpenAIApi } from "openai"; import * as cohere from "cohere-ai"; -import Anthropic from '@anthropic-ai/sdk'; - +import Anthropic from "@anthropic-ai/sdk"; import cl100k from "gpt-tokenizer"; import p50k from "gpt-tokenizer/esm/model/text-davinci-003"; @@ -62,18 +59,18 @@ const DEFAULT_SETTINGS: LoomSettings = { modelPreset: -1, visibility: { - "visibility": true, - "modelPreset": true, - "maxTokens": true, - "n": true, - "bestOf": false, - "temperature": true, - "topP": false, - "frequencyPenalty": false, - "presencePenalty": false, - "prepend": false, - "systemPrompt": false, - "userMessage": false, + visibility: true, + modelPreset: true, + maxTokens: true, + n: true, + bestOf: false, + temperature: true, + topP: false, + frequencyPenalty: false, + presencePenalty: false, + prepend: false, + systemPrompt: false, + userMessage: false, }, maxTokens: 60, temperature: 1, @@ -83,16 +80,18 @@ const DEFAULT_SETTINGS: LoomSettings = { prepend: "<|endoftext|>", bestOf: 0, n: 5, - systemPrompt: "The assistant is in CLI simulation mode, and responds to the user's CLI commands only with the output of the command.", + systemPrompt: + "The assistant is in CLI simulation mode, and responds to the user's CLI commands only with the output of the command.", userMessage: "cat untitled.txt", - showSettings: false, showSearchBar: false, showNodeBorders: false, showExport: false, }; -type CompletionResult = { ok: true; completions: string[] } | { ok: false; status: number; message: string }; +type CompletionResult = + | { ok: true; completions: string[] } + | { ok: false; status: number; message: string }; export default class LoomPlugin extends Plugin { settings: LoomSettings; @@ -115,21 +114,20 @@ export default class LoomPlugin extends Plugin { } saveAndRender() { - this.save(); + this.save(); if (this.rendering) return; this.rendering = true; - this.renderLoomViews(); - this.renderLoomSiblingsViews(); + this.renderLoomViews(); + this.renderLoomSiblingsViews(); this.rendering = false; - } thenSaveAndRender(callback: () => void) { callback(); - this.saveAndRender(); + this.saveAndRender(); } wftsar(callback: (file: TFile) => void) { @@ -139,39 +137,46 @@ export default class LoomPlugin extends Plugin { } renderLoomViews() { - const views = this.app.workspace.getLeavesOfType("loom").map((leaf) => leaf.view) as LoomView[]; - views.forEach((view) => view.render()); + const views = this.app.workspace + .getLeavesOfType("loom") + .map((leaf) => leaf.view) as LoomView[]; + views.forEach((view) => view.render()); } renderLoomSiblingsViews() { - const views = this.app.workspace.getLeavesOfType("loom-siblings").map((leaf) => leaf.view) as LoomSiblingsView[]; - views.forEach((view) => view.render()); + const views = this.app.workspace + .getLeavesOfType("loom-siblings") + .map((leaf) => leaf.view) as LoomSiblingsView[]; + views.forEach((view) => view.render()); } initializeProviders() { const preset = getPreset(this.settings); if (preset === undefined) return; - - if (preset.provider === "openai") { - this.openai = new OpenAIApi(new Configuration({ - apiKey: preset.apiKey, - // @ts-expect-error TODO - organization: preset.organization, - })); - } else if (preset.provider == "cohere") - cohere.init(preset.apiKey); - else if (preset.provider == "azure") { + + if (["openai", "openai-chat"].includes(preset.provider)) { + this.openai = new OpenAIApi( + new Configuration({ + apiKey: preset.apiKey, + // @ts-expect-error TODO + organization: preset.organization, + }) + ); + } else if (preset.provider == "cohere") cohere.init(preset.apiKey); + else if (preset.provider == "azure") { // @ts-expect-error TODO const url = preset.url; if (!preset.apiKey || !url) return; - this.azure = new AzureOpenAIApi(new AzureConfiguration({ - apiKey: preset.apiKey, - azure: { + this.azure = new AzureOpenAIApi( + new AzureConfiguration({ apiKey: preset.apiKey, - endpoint: url, - }, - })); + azure: { + apiKey: preset.apiKey, + endpoint: url, + }, + }) + ); } else if (preset.provider == "anthropic") { //(property) ClientOptions.fetch?: Fetch | undefined //Specify a custom fetch function implementation. @@ -183,8 +188,8 @@ export default class LoomPlugin extends Plugin { apiKey: preset.apiKey, // fetch: defaultHeaders: { - 'anthropic-version': '2023-06-01', - 'anthropic-beta': 'messages-2023-12-15', + "anthropic-version": "2023-06-01", + "anthropic-beta": "messages-2023-12-15", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Headers": "*", "Access-Control-Allow-Methods": "*", @@ -195,11 +200,15 @@ export default class LoomPlugin extends Plugin { } apiKeySet() { - if (this.settings.modelPreset == -1) return false; - return this.settings.modelPresets[this.settings.modelPreset].apiKey != ""; + if (this.settings.modelPreset == -1) return false; + return this.settings.modelPresets[this.settings.modelPreset].apiKey != ""; } - newNode(text: string, parentId: string | null, unread: boolean = false): [string, Node] { + newNode( + text: string, + parentId: string | null, + unread: boolean = false + ): [string, Node] { const id = uuidv4(); const node: Node = { text, @@ -209,30 +218,30 @@ export default class LoomPlugin extends Plugin { bookmarked: false, searchResultState: null, }; - return [id, node]; + return [id, node]; } initializeNoteState(file: TFile) { - const [rootId, root] = this.newNode(this.editor.getValue(), null); + const [rootId, root] = this.newNode(this.editor.getValue(), null); this.state[file.path] = { - current: rootId, + current: rootId, hoisted: [] as string[], - searchTerm: "", + searchTerm: "", nodes: { [rootId]: root }, - generating: null, + generating: null, }; this.saveAndRender(); } ancestors(file: TFile, id: string): string[] { const state = this.state[file.path]; - let ancestors = []; - let node: string | null = id; - while (node) { - node = state.nodes[node].parentId; - if (node) ancestors.push(node); - } - return ancestors.reverse(); + let ancestors = []; + let node: string | null = id; + while (node) { + node = state.nodes[node].parentId; + if (node) ancestors.push(node); + } + return ancestors.reverse(); } family(file: TFile, id: string): string[] { @@ -240,7 +249,7 @@ export default class LoomPlugin extends Plugin { } fullText(file: TFile, id: string | null) { - const state = this.state[file.path]; + const state = this.state[file.path]; let text = ""; let current = id; @@ -255,8 +264,8 @@ export default class LoomPlugin extends Plugin { // split the current node into: // - parent node with text before cursor // - child node with text after cursor - - const state = this.state[file.path]; + + const state = this.state[file.path]; const current = state.current; // first, get the cursor's position in the full text @@ -274,9 +283,8 @@ export default class LoomPlugin extends Plugin { let n = 0; while (true) { if (i < familyTexts[n].length) break; - // if the cursor is at the end of the last node, don't split, just return the current node - if (n === family.length - 1) - return [current, null]; + // if the cursor is at the end of the last node, don't split, just return the current node + if (n === family.length - 1) return [current, null]; i -= familyTexts[n].length; n++; } @@ -297,8 +305,8 @@ export default class LoomPlugin extends Plugin { ); // then, create a new node with the text after the cursor - const [childId, childNode] = this.newNode(after, parentNode); - this.state[file.path].nodes[childId] = childNode; + const [childId, childNode] = this.newNode(after, parentNode); + this.state[file.path].nodes[childId] = childNode; // move the children to under the after node children.forEach((child) => (child.parentId = childId)); @@ -310,50 +318,57 @@ export default class LoomPlugin extends Plugin { await this.loadSettings(); await this.loadState(); - this.app.workspace.trigger("parse-style-settings") + this.app.workspace.trigger("parse-style-settings"); this.addSettingTab(new LoomSettingTab(this.app, this)); - this.initializeProviders(); + this.initializeProviders(); this.statusBarItem = this.addStatusBarItem(); this.statusBarItem.setText("Generating..."); this.statusBarItem.style.display = "none"; - const completeCallback = (checking: boolean, callback: (file: TFile) => Promise) => { + const completeCallback = ( + checking: boolean, + callback: (file: TFile) => Promise + ) => { const file = this.app.workspace.getActiveFile(); if (!file || file.extension !== "md") return; - if (!this.apiKeySet()) return false; - if (!checking) callback(file); - return true; - } + if (!this.apiKeySet()) return false; + if (!checking) callback(file); + return true; + }; this.addCommand({ id: "complete", name: "Complete from current point", - checkCallback: (checking: boolean) => completeCallback(checking, this.complete.bind(this)), + checkCallback: (checking: boolean) => + completeCallback(checking, this.complete.bind(this)), hotkeys: [{ modifiers: ["Ctrl"], key: " " }], }); - this.addCommand({ - id: "generate-siblings", - name: "Generate siblings of the current node", - checkCallback: (checking: boolean) => completeCallback(checking, this.generateSiblings.bind(this)), - hotkeys: [{ modifiers: ["Ctrl", "Shift"], key: " " }], - }); - - this.addCommand({ - id: "bookmark", - name: "Bookmark current node", - checkCallback: (checking: boolean) => - withState(checking, (state) => { - this.app.workspace.trigger("loom:toggle-bookmark", state.current); - }), - hotkeys: [{ modifiers: ["Ctrl"], key: "b" }], - }); + this.addCommand({ + id: "generate-siblings", + name: "Generate siblings of the current node", + checkCallback: (checking: boolean) => + completeCallback(checking, this.generateSiblings.bind(this)), + hotkeys: [{ modifiers: ["Ctrl", "Shift"], key: " " }], + }); + this.addCommand({ + id: "bookmark", + name: "Bookmark current node", + checkCallback: (checking: boolean) => + withState(checking, (state) => { + this.app.workspace.trigger("loom:toggle-bookmark", state.current); + }), + hotkeys: [{ modifiers: ["Ctrl"], key: "b" }], + }); - const withState = (checking: boolean, callback: (state: NoteState) => void) => { + const withState = ( + checking: boolean, + callback: (state: NoteState) => void + ) => { const file = this.app.workspace.getActiveFile(); if (!file || file.extension !== "md") return false; @@ -367,7 +382,7 @@ export default class LoomPlugin extends Plugin { const withStateChecked = ( checking: boolean, checkCallback: (state: NoteState) => boolean, - callback: (state: NoteState) => void, + callback: (state: NoteState) => void ) => { const file = this.app.workspace.getActiveFile(); if (!file || file.extension !== "md") return false; @@ -382,15 +397,16 @@ export default class LoomPlugin extends Plugin { }; const openPane = (type: string, focus: boolean) => { - const panes = this.app.workspace.getLeavesOfType(type); - try { - if (panes.length === 0) - this.app.workspace.getRightLeaf(false)?.setViewState({ type }); - else if (focus) this.app.workspace.revealLeaf(panes[0]); - } catch (e) {} // expect "TypeError: Cannot read properties of null (reading 'children')" - }; + const panes = this.app.workspace.getLeavesOfType(type); + try { + if (panes.length === 0) + this.app.workspace.getRightLeaf(false)?.setViewState({ type }); + else if (focus) this.app.workspace.revealLeaf(panes[0]); + } catch (e) {} // expect "TypeError: Cannot read properties of null (reading 'children')" + }; const openLoomPane = (focus: boolean) => openPane("loom", focus); - const openLoomSiblingsPane = (focus: boolean) => openPane("loom-siblings", focus); + const openLoomSiblingsPane = (focus: boolean) => + openPane("loom-siblings", focus); this.addCommand({ id: "create-child", @@ -434,24 +450,30 @@ export default class LoomPlugin extends Plugin { name: "Split at current point and create child", checkCallback: (checking: boolean) => withState(checking, (state) => { - this.app.workspace.trigger("loom:break-at-point-create-child", state.current); + this.app.workspace.trigger( + "loom:break-at-point-create-child", + state.current + ); }), hotkeys: [{ modifiers: ["Alt"], key: "c" }], }); const canMerge = (state: NoteState, id: string, checking: boolean) => { - const parentId = state.nodes[id].parentId; - if (!parentId) { + const parentId = state.nodes[id].parentId; + if (!parentId) { if (!checking) new Notice("Can't merge a root node with its parent"); - return false; - } - const nSiblings = Object.values(state.nodes).filter((n) => n.parentId === parentId).length; - if (nSiblings > 1) { - if (!checking) new Notice("Can't merge this node with its parent; it has siblings"); - return false; - } - return true; - } + return false; + } + const nSiblings = Object.values(state.nodes).filter( + (n) => n.parentId === parentId + ).length; + if (nSiblings > 1) { + if (!checking) + new Notice("Can't merge this node with its parent; it has siblings"); + return false; + } + return true; + }; this.addCommand({ id: "merge-with-parent", @@ -466,18 +488,20 @@ export default class LoomPlugin extends Plugin { ), hotkeys: [{ modifiers: ["Alt"], key: "m" }], }); - - const switchToSibling = (state: NoteState, delta: number) => { - const parentId = state.nodes[state.current].parentId; - const siblings = Object.entries(state.nodes) - .filter(([, node]) => node.parentId === parentId) - .map(([id]) => id); - - if (siblings.length === 1) return; - - const index = (siblings.indexOf(state.current) + delta + siblings.length) % siblings.length; + + const switchToSibling = (state: NoteState, delta: number) => { + const parentId = state.nodes[state.current].parentId; + const siblings = Object.entries(state.nodes) + .filter(([, node]) => node.parentId === parentId) + .map(([id]) => id); + + if (siblings.length === 1) return; + + const index = + (siblings.indexOf(state.current) + delta + siblings.length) % + siblings.length; this.app.workspace.trigger("loom:switch-to", siblings[index]); - } + }; this.addCommand({ id: "switch-to-next-sibling", @@ -490,13 +514,16 @@ export default class LoomPlugin extends Plugin { this.addCommand({ id: "switch-to-previous-sibling", name: "Switch to previous sibling", - checkCallback: (checking: boolean) => - withState(checking, (state) => switchToSibling(state, -1)), + checkCallback: (checking: boolean) => + withState(checking, (state) => switchToSibling(state, -1)), hotkeys: [{ modifiers: ["Alt"], key: "ArrowUp" }], }); - const switchToParent = (state: NoteState) => - this.app.workspace.trigger("loom:switch-to", state.nodes[state.current].parentId); + const switchToParent = (state: NoteState) => + this.app.workspace.trigger( + "loom:switch-to", + state.nodes[state.current].parentId + ); this.addCommand({ id: "switch-to-parent", @@ -505,12 +532,12 @@ export default class LoomPlugin extends Plugin { withStateChecked( checking, (state) => state.nodes[state.current].parentId !== null, - switchToParent, + switchToParent ), hotkeys: [{ modifiers: ["Alt"], key: "ArrowLeft" }], }); - const switchToChild = (state: NoteState) => { + const switchToChild = (state: NoteState) => { const children = Object.entries(state.nodes) .filter(([, node]) => node.parentId === state.current) .sort( @@ -520,26 +547,25 @@ export default class LoomPlugin extends Plugin { if (children.length > 0) this.app.workspace.trigger("loom:switch-to", children[0][0]); - } + }; this.addCommand({ id: "switch-to-child", name: "Switch to child", - checkCallback: (checking: boolean) => - withState(checking, switchToChild), + checkCallback: (checking: boolean) => withState(checking, switchToChild), hotkeys: [{ modifiers: ["Alt"], key: "ArrowRight" }], }); - const canDelete = (state: NoteState, id: string, checking: boolean) => { - const rootNodes = Object.entries(state.nodes) - .filter(([, node]) => node.parentId === null) - .map(([id]) => id); - if (rootNodes.length === 1 && rootNodes[0] === id) { - if (!checking) new Notice("Can't delete the last root node"); - return false; - } - return true; - } + const canDelete = (state: NoteState, id: string, checking: boolean) => { + const rootNodes = Object.entries(state.nodes) + .filter(([, node]) => node.parentId === null) + .map(([id]) => id); + if (rootNodes.length === 1 && rootNodes[0] === id) { + if (!checking) new Notice("Can't delete the last root node"); + return false; + } + return true; + }; this.addCommand({ id: "delete-current-node", @@ -586,19 +612,16 @@ export default class LoomPlugin extends Plugin { const getSettings = () => this.settings; this.addCommand({ - id: "make-prompt-from-passages", - name: "Make prompt from passages", - callback: () => { - if (this.settings.passageFolder.trim() === "") { - new Notice("Please set the passage folder in settings"); - return; - } - new MakePromptFromPassagesModal( - this.app, - getSettings, - ).open(); - } - }); + id: "make-prompt-from-passages", + name: "Make prompt from passages", + callback: () => { + if (this.settings.passageFolder.trim() === "") { + new Notice("Please set the passage folder in settings"); + return; + } + new MakePromptFromPassagesModal(this.app, getSettings).open(); + }, + }); this.addCommand({ id: "open-pane", @@ -606,11 +629,11 @@ export default class LoomPlugin extends Plugin { callback: () => openLoomPane(true), }); - this.addCommand({ - id: "open-siblings-pane", - name: "Open Loom siblings pane", - callback: () => openLoomSiblingsPane(true), - }); + this.addCommand({ + id: "open-siblings-pane", + name: "Open Loom siblings pane", + callback: () => openLoomSiblingsPane(true), + }); this.addCommand({ id: "debug-reset-state", @@ -621,8 +644,9 @@ export default class LoomPlugin extends Plugin { this.addCommand({ id: "debug-reset-hoist-stack", name: "Debug: Reset hoist stack", - callback: () => this.wftsar((file) => (this.state[file.path].hoisted = [])), - }); + callback: () => + this.wftsar((file) => (this.state[file.path].hoisted = [])), + }); this.registerView( "loom", @@ -649,33 +673,33 @@ export default class LoomPlugin extends Plugin { // @ts-expect-error const editorView = editor.cm; const plugin = editorView.plugin(loomEditorPlugin); - + // get cursor position, so it can be restored later const cursor = editor.getCursor(); // if this note has no state, initialize it and return - // @ts-ignore `Object is possibly 'null'` only in github actions + // @ts-ignore `Object is possibly 'null'` only in github actions if (!this.state[view.file.path]) { const [current, node] = this.newNode(editor.getValue(), null); - // @ts-ignore + // @ts-ignore this.state[view.file.path] = { current, hoisted: [] as string[], - searchTerm: "", + searchTerm: "", nodes: { [current]: node }, generating: null, }; - return; - } + return; + } - // @ts-ignore + // @ts-ignore const current = this.state[view.file.path].current; // `ancestors`: starts with the root node, ends with the parent of the current node let ancestors: string[] = []; let node: string | null = current; while (node) { - // @ts-ignore + // @ts-ignore node = this.state[view.file.path].nodes[node].parentId; if (node) ancestors.push(node); } @@ -684,13 +708,13 @@ export default class LoomPlugin extends Plugin { // `ancestorTexts`: the text of each node in `ancestors` const text = editor.getValue(); const ancestorTexts = ancestors.map( - // @ts-ignore + // @ts-ignore (id) => this.state[view.file.path].nodes[id].text ); // `familyTexts`: `ancestorTexts` + the current node's text const familyTexts = ancestorTexts.concat( - // @ts-ignore + // @ts-ignore this.state[view.file.path].nodes[current].text ); @@ -703,14 +727,14 @@ export default class LoomPlugin extends Plugin { let newText = text.substring(prefix.length); newText = newText.substring(0, newText.length - suffix.length); - // @ts-ignore + // @ts-ignore this.state[view.file.path].nodes[ancestors[i]].text = newText; }; const updateDecorations = () => { const ancestorLengths = ancestors.map((id) => [ id, - // @ts-ignore + // @ts-ignore this.state[view.file.path].nodes[id].text.length, ]); plugin.state = { ...plugin.state, ancestorLengths }; @@ -725,7 +749,7 @@ export default class LoomPlugin extends Plugin { return; } } - // @ts-ignore + // @ts-ignore this.state[view.file.path].nodes[current].text = text.slice( ancestorTexts.join("").length ); @@ -735,7 +759,7 @@ export default class LoomPlugin extends Plugin { setTimeout(() => { this.saveAndRender(); }, 0); - + // restore cursor position editor.setCursor(cursor); } @@ -752,45 +776,45 @@ export default class LoomPlugin extends Plugin { this.state[file.path].nodes[id].unread = false; this.state[file.path].nodes[id].lastVisited = Date.now(); - // uncollapse the node's ancestors + // uncollapse the node's ancestors const ancestors = this.family(file, id).slice(0, -1); ancestors.forEach( (id) => (this.state[file.path].nodes[id].collapsed = false) ); - // update the editor's text + // update the editor's text // const cursor = this.editor.getCursor(); // const linesBefore = this.editor.getValue().split("\n"); this.editor.setValue(this.fullText(file, id)); - // always move cursor to the end of the editor - const line = this.editor.lineCount() - 1; - const ch = this.editor.getLine(line).length; - this.editor.setCursor({ line, ch }); - // return; - - // // if the cursor is at the beginning of the editor, move it to the end - // if(cursor.line === 0 && cursor.ch === 0) { - // const line = this.editor.lineCount() - 1; - // const ch = this.editor.getLine(line).length; - // this.editor.setCursor({ line, ch }); - // return; - // } - - // // if the text preceding the cursor has changed, move the cursor to the end of the text - // // otherwise, restore the cursor position - // const linesAfter = this.editor - // .getValue() - // .split("\n") - // .slice(0, cursor.line + 1); - // for (let i = 0; i < cursor.line; i++) - // if (linesBefore[i] !== linesAfter[i]) { - // const line = this.editor.lineCount() - 1; - // const ch = this.editor.getLine(line).length; - // this.editor.setCursor({ line, ch }); - // return; - // } - // this.editor.setCursor(cursor); + // always move cursor to the end of the editor + const line = this.editor.lineCount() - 1; + const ch = this.editor.getLine(line).length; + this.editor.setCursor({ line, ch }); + // return; + + // // if the cursor is at the beginning of the editor, move it to the end + // if(cursor.line === 0 && cursor.ch === 0) { + // const line = this.editor.lineCount() - 1; + // const ch = this.editor.getLine(line).length; + // this.editor.setCursor({ line, ch }); + // return; + // } + + // // if the text preceding the cursor has changed, move the cursor to the end of the text + // // otherwise, restore the cursor position + // const linesAfter = this.editor + // .getValue() + // .split("\n") + // .slice(0, cursor.line + 1); + // for (let i = 0; i < cursor.line; i++) + // if (linesBefore[i] !== linesAfter[i]) { + // const line = this.editor.lineCount() - 1; + // const ch = this.editor.getLine(line).length; + // this.editor.setCursor({ line, ch }); + // return; + // } + // this.editor.setCursor(cursor); }) ) ); @@ -835,8 +859,8 @@ export default class LoomPlugin extends Plugin { // @ts-expect-error this.app.workspace.on("loom:create-child", (id: string) => this.withFile((file) => { - const [newId, newNode] = this.newNode("", id); - this.state[file.path].nodes[newId] = newNode; + const [newId, newNode] = this.newNode("", id); + this.state[file.path].nodes[newId] = newNode; this.app.workspace.trigger("loom:switch-to", newId); }) ) @@ -846,8 +870,11 @@ export default class LoomPlugin extends Plugin { // @ts-expect-error this.app.workspace.on("loom:create-sibling", (id: string) => this.withFile((file) => { - const [newId, newNode] = this.newNode("", this.state[file.path].nodes[id].parentId); - this.state[file.path].nodes[newId] = newNode; + const [newId, newNode] = this.newNode( + "", + this.state[file.path].nodes[id].parentId + ); + this.state[file.path].nodes[newId] = newNode; this.app.workspace.trigger("loom:switch-to", newId); }) ) @@ -858,8 +885,8 @@ export default class LoomPlugin extends Plugin { this.app.workspace.on("loom:clone", (id: string) => this.withFile((file) => { const node = this.state[file.path].nodes[id]; - const [newId, newNode] = this.newNode(node.text, node.parentId); - this.state[file.path].nodes[newId] = newNode; + const [newId, newNode] = this.newNode(node.text, node.parentId); + this.state[file.path].nodes[newId] = newNode; this.app.workspace.trigger("loom:switch-to", newId); }) ) @@ -870,8 +897,8 @@ export default class LoomPlugin extends Plugin { this.app.workspace.on("loom:break-at-point", () => this.withFile((file) => { const [, childId] = this.breakAtPoint(file); - if (childId) this.app.workspace.trigger("loom:switch-to", childId); - }) + if (childId) this.app.workspace.trigger("loom:switch-to", childId); + }) ) ); @@ -881,8 +908,8 @@ export default class LoomPlugin extends Plugin { this.withFile((file) => { const [parentId] = this.breakAtPoint(file); if (parentId !== undefined) { - const [newId, newNode] = this.newNode("", parentId); - this.state[file.path].nodes[newId] = newNode; + const [newId, newNode] = this.newNode("", parentId); + this.state[file.path].nodes[newId] = newNode; this.app.workspace.trigger("loom:switch-to", newId); } }) @@ -895,21 +922,21 @@ export default class LoomPlugin extends Plugin { this.wftsar((file) => { const state = this.state[file.path]; - if (!canMerge(state, id, false)) return; + if (!canMerge(state, id, false)) return; const parentId = state.nodes[id].parentId!; - // update the merged node's text + // update the merged node's text state.nodes[parentId].text += state.nodes[id].text; - // move the children to the merged node + // move the children to the merged node const children = Object.entries(state.nodes).filter( ([, node]) => node.parentId === id ); for (const [childId] of children) this.state[file.path].nodes[childId].parentId = parentId; - // switch to the merged node and delete the child node + // switch to the merged node and delete the child node this.app.workspace.trigger("loom:switch-to", parentId); this.app.workspace.trigger("loom:delete", [id]); }) @@ -920,68 +947,69 @@ export default class LoomPlugin extends Plugin { // @ts-expect-error this.app.workspace.on("loom:delete", (ids: string[]) => this.wftsar((file) => { - const state = this.state[file.path]; + const state = this.state[file.path]; - ids = ids.filter((id) => canDelete(state, id, false)); - if (ids.length === 0) return; + ids = ids.filter((id) => canDelete(state, id, false)); + if (ids.length === 0) return; - // remove the nodes from the hoist stack - this.state[file.path].hoisted = state.hoisted.filter((id) => !ids.includes(id)); + // remove the nodes from the hoist stack + this.state[file.path].hoisted = state.hoisted.filter( + (id) => !ids.includes(id) + ); - // add the nodes and their descendants to a list of nodes to delete + // add the nodes and their descendants to a list of nodes to delete - let deleted = [...ids]; + let deleted = [...ids]; - const addChildren = (id: string) => { - const children = Object.entries(state.nodes) - .filter(([, node]) => node.parentId === id) - .map(([id]) => id); - deleted = deleted.concat(children); - children.forEach(addChildren); - } - ids.forEach(addChildren); + const addChildren = (id: string) => { + const children = Object.entries(state.nodes) + .filter(([, node]) => node.parentId === id) + .map(([id]) => id); + deleted = deleted.concat(children); + children.forEach(addChildren); + }; + ids.forEach(addChildren); - // if the current node will be deleted, switch to its next sibling or its closest ancestor - if (deleted.includes(state.current)) { + // if the current node will be deleted, switch to its next sibling or its closest ancestor + if (deleted.includes(state.current)) { const parentId = state.nodes[state.current].parentId; - const siblings = Object.entries(state.nodes) - .filter(([, node]) => node.parentId === parentId) - .map(([id]) => id); - - (() => { - // try to switch to the next sibling - if (siblings.some((id) => !deleted.includes(id))) { - const index = siblings.indexOf(state.current); - const nextSibling = siblings[(index + 1) % siblings.length]; - this.app.workspace.trigger("loom:switch-to", nextSibling); - return; - } - - // try to switch to the closest ancestor - let ancestorId = parentId; - while (ancestorId !== null) { - if (!deleted.includes(ancestorId)) { - this.app.workspace.trigger("loom:switch-to", ancestorId); - return; - } - ancestorId = state.nodes[ancestorId].parentId; - } - - // if all else fails, switch to a root node - const rootNodes = Object.entries(state.nodes) - .filter(([, node]) => node.parentId === null) - .map(([id]) => id); - for (const id of rootNodes) - if (!deleted.includes(id)) { - this.app.workspace.trigger("loom:switch-to", id); - return; - } - })(); - } - - // delete the nodes in the list - for (const id of deleted) - delete this.state[file.path].nodes[id]; + const siblings = Object.entries(state.nodes) + .filter(([, node]) => node.parentId === parentId) + .map(([id]) => id); + + (() => { + // try to switch to the next sibling + if (siblings.some((id) => !deleted.includes(id))) { + const index = siblings.indexOf(state.current); + const nextSibling = siblings[(index + 1) % siblings.length]; + this.app.workspace.trigger("loom:switch-to", nextSibling); + return; + } + + // try to switch to the closest ancestor + let ancestorId = parentId; + while (ancestorId !== null) { + if (!deleted.includes(ancestorId)) { + this.app.workspace.trigger("loom:switch-to", ancestorId); + return; + } + ancestorId = state.nodes[ancestorId].parentId; + } + + // if all else fails, switch to a root node + const rootNodes = Object.entries(state.nodes) + .filter(([, node]) => node.parentId === null) + .map(([id]) => id); + for (const id of rootNodes) + if (!deleted.includes(id)) { + this.app.workspace.trigger("loom:switch-to", id); + return; + } + })(); + } + + // delete the nodes in the list + for (const id of deleted) delete this.state[file.path].nodes[id]; }) ) ); @@ -991,9 +1019,9 @@ export default class LoomPlugin extends Plugin { this.app.workspace.on("loom:clear-children", (id: string) => this.wftsar((file) => { const children = Object.entries(this.state[file.path].nodes) - .filter(([, node]) => node.parentId === id) - .map(([id]) => id); - this.app.workspace.trigger("loom:delete", children); + .filter(([, node]) => node.parentId === id) + .map(([id]) => id); + this.app.workspace.trigger("loom:delete", children); }) ) ); @@ -1004,9 +1032,9 @@ export default class LoomPlugin extends Plugin { this.wftsar((file) => { const parentId = this.state[file.path].nodes[id].parentId; const siblings = Object.entries(this.state[file.path].nodes) - .filter(([id_, node]) => node.parentId === parentId && id_ !== id) - .map(([id]) => id); - this.app.workspace.trigger("loom:delete", siblings); + .filter(([id_, node]) => node.parentId === parentId && id_ !== id) + .map(([id]) => id); + this.app.workspace.trigger("loom:delete", siblings); }) ) ); @@ -1016,10 +1044,10 @@ export default class LoomPlugin extends Plugin { // @ts-expect-error "loom:set-setting", (setting: string, value: any) => { - this.settings = { ...this.settings, [setting]: value }; - this.saveAndRender(); + this.settings = { ...this.settings, [setting]: value }; + this.saveAndRender(); - // if changing showNodeBorders, update the editor + // if changing showNodeBorders, update the editor if (setting === "showNodeBorders") { // @ts-expect-error const editor = this.editor.cm; @@ -1039,50 +1067,55 @@ export default class LoomPlugin extends Plugin { // @ts-expect-error "loom:set-visibility-setting", (setting: string, value: boolean) => { - this.settings.visibility[setting] = value; - this.saveAndRender(); - } - ) - ); - - this.registerEvent( - // @ts-expect-error - this.app.workspace.on("loom:search", (term: string) => this.withFile((file) => { - const state = this.state[file.path]; - - this.state[file.path].searchTerm = term; - if (!term) { - Object.keys(state.nodes).forEach((id) => { - this.state[file.path].nodes[id].searchResultState = null; - }); - this.save(); // don't re-render - return; - } - - const matches = Object.entries(state.nodes) - .filter(([, node]) => node.text.toLowerCase().includes(term.toLowerCase())) - .map(([id]) => id); - - let ancestors: string[] = []; - for (const id of matches) { - let parentId = state.nodes[id].parentId; - while (parentId !== null) { - ancestors.push(parentId); - parentId = state.nodes[parentId].parentId; - } - } - - Object.keys(state.nodes).forEach((id) => { - let searchResultState: SearchResultState; - if (matches.includes(id)) searchResultState = "result"; - else if (ancestors.includes(id)) searchResultState = "ancestor"; - else searchResultState = "none"; - this.state[file.path].nodes[id].searchResultState = searchResultState; - }); - - this.save(); - })) - ); + this.settings.visibility[setting] = value; + this.saveAndRender(); + } + ) + ); + + this.registerEvent( + // @ts-expect-error + this.app.workspace.on("loom:search", (term: string) => + this.withFile((file) => { + const state = this.state[file.path]; + + this.state[file.path].searchTerm = term; + if (!term) { + Object.keys(state.nodes).forEach((id) => { + this.state[file.path].nodes[id].searchResultState = null; + }); + this.save(); // don't re-render + return; + } + + const matches = Object.entries(state.nodes) + .filter(([, node]) => + node.text.toLowerCase().includes(term.toLowerCase()) + ) + .map(([id]) => id); + + let ancestors: string[] = []; + for (const id of matches) { + let parentId = state.nodes[id].parentId; + while (parentId !== null) { + ancestors.push(parentId); + parentId = state.nodes[parentId].parentId; + } + } + + Object.keys(state.nodes).forEach((id) => { + let searchResultState: SearchResultState; + if (matches.includes(id)) searchResultState = "result"; + else if (ancestors.includes(id)) searchResultState = "ancestor"; + else searchResultState = "none"; + this.state[file.path].nodes[id].searchResultState = + searchResultState; + }); + + this.save(); + }) + ) + ); this.registerEvent( // @ts-expect-error @@ -1111,72 +1144,73 @@ export default class LoomPlugin extends Plugin { ) ); - this.registerEvent( - this.app.workspace.on( - // @ts-expect-error - "loom:make-prompt-from-passages", - ( - passages: string[], - rawSeparator: string, - rawFrontmatter: string, - ) => this.wftsar((file) => { - const separator = rawSeparator.replace(/\\n/g, "\n"); - const frontmatter = (index: number) => rawFrontmatter - .replace(/%n/g, (index + 1).toString()) - .replace(/%r/g, toRoman(index + 1)) - .replace(/\\n/g, "\n"); - - const passageTexts = passages.map((passage, index) => { - return Object.entries(this.state[passage].nodes) - .filter(([, node]) => node.parentId === null) - .map(([, node]) => frontmatter(index) + node.text); - }); - const text = `${passageTexts.join(separator)}${separator}${frontmatter(passages.length)}`; - - const state = this.state[file.path]; - const currentNode = state.nodes[state.current]; - - let id; - if (currentNode.text === "" && currentNode.parentId === null) { - this.state[file.path].nodes[state.current].text = text; - id = state.current; - } else { - const [newId, newNode] = this.newNode(text, null); - this.state[file.path].nodes[newId] = newNode; - id = newId; - } - - this.app.workspace.trigger("loom:switch-to", id); - }) - ) - ); + this.registerEvent( + this.app.workspace.on( + // @ts-expect-error + "loom:make-prompt-from-passages", + (passages: string[], rawSeparator: string, rawFrontmatter: string) => + this.wftsar((file) => { + const separator = rawSeparator.replace(/\\n/g, "\n"); + const frontmatter = (index: number) => + rawFrontmatter + .replace(/%n/g, (index + 1).toString()) + .replace(/%r/g, toRoman(index + 1)) + .replace(/\\n/g, "\n"); + + const passageTexts = passages.map((passage, index) => { + return Object.entries(this.state[passage].nodes) + .filter(([, node]) => node.parentId === null) + .map(([, node]) => frontmatter(index) + node.text); + }); + const text = `${passageTexts.join( + separator + )}${separator}${frontmatter(passages.length)}`; + + const state = this.state[file.path]; + const currentNode = state.nodes[state.current]; + + let id; + if (currentNode.text === "" && currentNode.parentId === null) { + this.state[file.path].nodes[state.current].text = text; + id = state.current; + } else { + const [newId, newNode] = this.newNode(text, null); + this.state[file.path].nodes[newId] = newNode; + id = newId; + } + + this.app.workspace.trigger("loom:switch-to", id); + }) + ) + ); const onFileOpen = (file: TFile) => { if (file.extension !== "md") return; - // if this file is new, initialize its state - if (!this.state[file.path]) - this.initializeNoteState(file); + // if this file is new, initialize its state + if (!this.state[file.path]) this.initializeNoteState(file); const state = this.state[file.path]; - // find this file's `MarkdownView`, then set `this.editor` to its editor + // find this file's `MarkdownView`, then set `this.editor` to its editor this.app.workspace.iterateRootLeaves((leaf) => { if ( leaf.view instanceof MarkdownView && - // @ts-ignore + // @ts-ignore leaf.view.file.path === file.path ) this.editor = leaf.view.editor; }); - // get the length of each ancestor's text, - // which will be passed to `LoomEditorPlugin` to mark ancestor nodes in the editor + // get the length of each ancestor's text, + // which will be passed to `LoomEditorPlugin` to mark ancestor nodes in the editor const ancestors = this.ancestors(file, state.current); - const ancestorLengths = ancestors.map((id) => - [id, state.nodes[id].text.length]); + const ancestorLengths = ancestors.map((id) => [ + id, + state.nodes[id].text.length, + ]); - // set `LoomEditorPlugin`'s state, then refresh it + // set `LoomEditorPlugin`'s state, then refresh it // @ts-expect-error const plugin = this.editor.cm.plugin(loomEditorPlugin); plugin.state = { @@ -1187,7 +1221,7 @@ export default class LoomPlugin extends Plugin { this.renderLoomViews(); this.renderLoomSiblingsViews(); - } + }; this.registerEvent( this.app.workspace.on("file-open", (file) => file && onFileOpen(file)) @@ -1227,106 +1261,115 @@ export default class LoomPlugin extends Plugin { this.app.workspace.iterateRootLeaves((leaf) => { if ( leaf.view instanceof MarkdownView && - // @ts-ignore + // @ts-ignore leaf.view.file.path === file.path ) this.editor = leaf.view.editor; - onFileOpen(file); + onFileOpen(file); }) ); } async complete(file: TFile) { - const state = this.state[file.path]; - const [parentNode] = this.breakAtPoint(file); + const state = this.state[file.path]; + const [parentNode] = this.breakAtPoint(file); // switch to the parent node this.app.workspace.trigger("loom:switch-to", parentNode); - this.saveAndRender(); + this.saveAndRender(); - await this.generate(file, state.current); + await this.generate(file, state.current); } async generateSiblings(file: TFile) { - const state = this.state[file.path]; - await this.generate(file, state.nodes[state.current].parentId); + const state = this.state[file.path]; + await this.generate(file, state.nodes[state.current].parentId); } async generate(file: TFile, rootNode: string | null) { - // show the "Generating..." indicator in the status bar - this.statusBarItem.style.display = "inline-flex"; + // show the "Generating..." indicator in the status bar + this.statusBarItem.style.display = "inline-flex"; const state = this.state[file.path]; - - this.state[file.path].generating = rootNode; - // show the "Generating..." indicator in the loom view - this.renderLoomViews(); + this.state[file.path].generating = rootNode; + + // show the "Generating..." indicator in the loom view + this.renderLoomViews(); let prompt = `${this.settings.prepend}${this.fullText(file, rootNode)}`; - // remove a trailing space if there is one // store whether there was, so it can be added back post-completion const trailingSpace = prompt.match(/\s+$/); - prompt = prompt.replace(/\s+$/, ""); - + prompt = prompt.replace(/\s+$/, ""); + // replace "\<" with "<", because obsidian tries to render html tags - // and "\[" with "[" + // and "\[" with "[" prompt = prompt.replace(/\\ Promise> = { - cohere: this.completeCohere, - textsynth: this.completeTextSynth, - ocp: this.completeOCP, - openai: this.completeOpenAI, - "openai-chat": this.completeOpenAIChat, - azure: this.completeAzure, - "azure-chat": this.completeAzureChat, - anthropic: this.completeAnthropic, - }; - let result; - try { - result = await completionMethods[getPreset(this.settings).provider].bind(this)(prompt); - } catch (e) { - new Notice(`Error: ${e}`); - this.state[file.path].generating = null; - this.saveAndRender(); - this.statusBarItem.style.display = "none"; + // the tokenization and completion depend on the provider, + // so call a different method depending on the provider + + // console.log("prompt", prompt); + + const completionMethods: Record< + Provider, + (prompt: string) => Promise + > = { + cohere: this.completeCohere, + textsynth: this.completeTextSynth, + "openai-compat": this.completeOpenAICompat, + openai: this.completeOpenAI, + "openai-chat": this.completeOpenAIChat, + azure: this.completeAzure, + "azure-chat": this.completeAzureChat, + anthropic: this.completeAnthropic, + openrouter: this.completeOpenRouter, + }; + let result; + try { + result = await completionMethods[getPreset(this.settings).provider].bind( + this + )(prompt); + } catch (e) { + new Notice(`Error: ${e}`); + this.state[file.path].generating = null; + this.saveAndRender(); + this.statusBarItem.style.display = "none"; - return; - } - if (!result.ok) { - new Notice(`Error ${result.status}: ${result.message}`); - this.state[file.path].generating = null; - this.saveAndRender(); - this.statusBarItem.style.display = "none"; + return; + } + if (!result.ok) { + new Notice(`Error ${result.status}: ${result.message}`); + this.state[file.path].generating = null; + this.saveAndRender(); + this.statusBarItem.style.display = "none"; - return; - } - const rawCompletions = result.completions; + return; + } + const rawCompletions = result.completions; - // console.log("rawCompletions", rawCompletions); + // console.log("rawCompletions", rawCompletions); - // escape and clean up the completions - const completions = rawCompletions.map((completion: string) => { + // escape and clean up the completions + const completions = rawCompletions.map((completion: string) => { if (!completion) completion = ""; // empty completions are null, apparently completion = completion.replace(/ generation.text) } - // @ts-expect-error - : { ok: false, status: response.statusCode!, message: response.body.message }; - return result; + const result: CompletionResult = + response.statusCode === 200 + ? { + ok: true, + completions: response.body.generations.map( + (generation) => generation.text + ), + } + : + { + ok: false, + status: response.statusCode!, + // @ts-expect-error + message: response.body.message, + }; + return result; } async completeTextSynth(prompt: string) { - const response = await requestUrl({ - url: `https://api.textsynth.com/v1/engines/${getPreset(this.settings).model}/completions`, + const response = await requestUrl({ + url: `https://api.textsynth.com/v1/engines/${ + getPreset(this.settings).model + }/completions`, method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${getPreset(this.settings).apiKey}`, }, - throw: false, + throw: false, body: JSON.stringify({ prompt, max_tokens: this.settings.maxTokens, best_of: this.settings.bestOf, - n: this.settings.n, + n: this.settings.n, temperature: this.settings.temperature, top_p: this.settings.topP, - frequency_penalty: this.settings.frequencyPenalty, - presence_penalty: this.settings.presencePenalty, + frequency_penalty: this.settings.frequencyPenalty, + presence_penalty: this.settings.presencePenalty, }), - }); - - let result: CompletionResult; - if (response.status === 200) { - const completions = this.settings.n === 1 ? [response.json.text] : response.json.text; - result = { ok: true, completions }; - } else { - result = { ok: false, status: response.status, message: response.json.error }; - } - return result; + }); + + let result: CompletionResult; + if (response.status === 200) { + const completions = + this.settings.n === 1 ? [response.json.text] : response.json.text; + result = { ok: true, completions }; + } else { + result = { + ok: false, + status: response.status, + message: response.json.error, + }; + } + return result; } trimOpenAIPrompt(prompt: string) { - const cl100kModels = ["gpt-4-32k", "gpt-4-0314", "gpt-4-32k-0314", "gpt-3.5-turbo", "gpt-3.5-turbo-0301", "gpt-4-base"]; - const p50kModels = ["text-davinci-003", "text-davinci-002", "code-davinci-002", "code-davinci-001", "code-cushman-002", "code-cushman-001", "davinci-codex", "cushman-codex"]; - // const r50kModels = ["text-davinci-001", "text-curie-001", "text-babbage-001", "text-ada-001", "davinci", "curie", "babbage", "ada"]; - - let tokenizer; - if (cl100kModels.includes(getPreset(this.settings).model)) tokenizer = cl100k; - else if (p50kModels.includes(getPreset(this.settings).model)) tokenizer = p50k; + const cl100kModels = [ + "gpt-4-32k", + "gpt-4-0314", + "gpt-4-32k-0314", + "gpt-3.5-turbo", + "gpt-3.5-turbo-0301", + "gpt-4-base", + ]; + const p50kModels = [ + "text-davinci-003", + "text-davinci-002", + "code-davinci-002", + "code-davinci-001", + "code-cushman-002", + "code-cushman-001", + "davinci-codex", + "cushman-codex", + ]; + // const r50kModels = ["text-davinci-001", "text-curie-001", "text-babbage-001", "text-ada-001", "davinci", "curie", "babbage", "ada"]; + + let tokenizer; + if (cl100kModels.includes(getPreset(this.settings).model)) + tokenizer = cl100k; + else if (p50kModels.includes(getPreset(this.settings).model)) + tokenizer = p50k; else tokenizer = r50k; // i expect that an unknown model will most likely be r50k - return tokenizer.decode(tokenizer.encode(prompt, { disallowedSpecial: new Set() }).slice(-(getPreset(this.settings).contextLength - this.settings.maxTokens))); + return tokenizer.decode( + tokenizer + .encode(prompt, { disallowedSpecial: new Set() }) + .slice( + -(getPreset(this.settings).contextLength - this.settings.maxTokens) + ) + ); } - async completeOCP(prompt: string) { - prompt = this.trimOpenAIPrompt(prompt); + async completeOpenAICompat(prompt: string) { + prompt = this.trimOpenAIPrompt(prompt); - // @ts-expect-error TODO + // @ts-expect-error TODO let url = getPreset(this.settings).url; if (!(url.startsWith("http://") || url.startsWith("https://"))) url = "https://" + url; if (!url.endsWith("/")) url += "/"; - url = url.replace(/v1\//, ""); + url = url.replace(/v1\//, ""); url += "v1/completions"; let body: any = { prompt, - model: getPreset(this.settings).model, // some providers such as OCP ignore model + model: getPreset(this.settings).model, max_tokens: this.settings.maxTokens, n: this.settings.n, temperature: this.settings.temperature, top_p: this.settings.topP, - best_of: this.settings.bestOf > this.settings.n ? this.settings.bestOf : this.settings.n, - } + best_of: + this.settings.bestOf > this.settings.n + ? this.settings.bestOf + : this.settings.n, + }; if (this.settings.frequencyPenalty !== 0) body.frequency_penalty = this.settings.frequencyPenalty; if (this.settings.presencePenalty !== 0) body.presence_penalty = this.settings.presencePenalty; const response = await requestUrl({ - url, + url, method: "POST", headers: { Authorization: `Bearer ${getPreset(this.settings).apiKey}`, "Content-Type": "application/json", }, - throw: false, + throw: false, body: JSON.stringify(body), }); - const result: CompletionResult = response.status === 200 - ? { ok: true, completions: response.json.choices.map((choice: any) => choice.text) } - : { ok: false, status: response.status, message: "" }; - return result; + const result: CompletionResult = + response.status === 200 + ? { + ok: true, + completions: response.json.choices.map( + (choice: any) => choice.text + ), + } + : { ok: false, status: response.status, message: "" }; + return result; + } + + async completeOpenRouter(prompt: string) { + prompt = this.trimOpenAIPrompt(prompt); + + let body: any = { + prompt, + model: getPreset(this.settings).model, + max_tokens: this.settings.maxTokens, + n: this.settings.n, + temperature: this.settings.temperature, + top_p: this.settings.topP, + best_of: this.settings.bestOf, + provider: { + // @ts-expect-error + quantizations: [getPreset(this.settings).quantization] + } + }; + if (this.settings.frequencyPenalty !== 0) + body.frequency_penalty = this.settings.frequencyPenalty; + if (this.settings.presencePenalty !== 0) + body.presence_penalty = this.settings.presencePenalty; + + const requests = Array(this.settings.n).fill(null).map(() => + requestUrl({ + url: "https://openrouter.ai/api/v1/completions", + method: "POST", + headers: { + Authorization: `Bearer ${getPreset(this.settings).apiKey}`, + "Content-Type": "application/json", + }, + throw: false, + body: JSON.stringify(body), + }) + ); + + const responses = await Promise.all(requests); + + const result: CompletionResult = responses.every(response => response.status === 200) + ? { + ok: true, + completions: responses.map(response => response.json.choices[0].text), + } + : { + ok: false, + status: responses[0].status, + message: responses[0].text, + }; + return result; } async completeOpenAI(prompt: string) { - prompt = this.trimOpenAIPrompt(prompt); - let result: CompletionResult; - try { - const response = await this.openai.createCompletion({ + prompt = this.trimOpenAIPrompt(prompt); + let result: CompletionResult; + try { + const response = await this.openai.createCompletion({ model: getPreset(this.settings).model, prompt, max_tokens: this.settings.maxTokens, n: this.settings.n, temperature: this.settings.temperature, top_p: this.settings.topP, - frequency_penalty: this.settings.frequencyPenalty, - presence_penalty: this.settings.presencePenalty, - }); - result = { ok: true, completions: response.data.choices.map((choice) => choice.text || "") }; - } catch (e) { - result = { ok: false, status: e.response.status, message: e.response.data.error.message }; - } - return result; + frequency_penalty: this.settings.frequencyPenalty, + presence_penalty: this.settings.presencePenalty, + }); + result = { + ok: true, + completions: response.data.choices.map((choice) => choice.text || ""), + }; + } catch (e) { + result = { + ok: false, + status: e.response.status, + message: e.response.data.error.message, + }; + } + return result; } async completeOpenAIChat(prompt: string) { - prompt = this.trimOpenAIPrompt(prompt); - let result: CompletionResult; - try { - const response = await this.openai.createChatCompletion({ + prompt = this.trimOpenAIPrompt(prompt); + let result: CompletionResult; + try { + const response = await this.openai.createChatCompletion({ model: getPreset(this.settings).model, messages: [{ role: "assistant", content: prompt }], max_tokens: this.settings.maxTokens, n: this.settings.n, temperature: this.settings.temperature, top_p: this.settings.topP, - frequency_penalty: this.settings.frequencyPenalty, - presence_penalty: this.settings.presencePenalty, - }); - result = { ok: true, completions: response.data.choices.map((choice) => choice.message?.content || "") }; - } catch (e) { - result = { ok: false, status: e.response.status, message: e.response.data.error.message }; - } - return result; + frequency_penalty: this.settings.frequencyPenalty, + presence_penalty: this.settings.presencePenalty, + }); + result = { + ok: true, + completions: response.data.choices.map( + (choice) => choice.message?.content || "" + ), + }; + } catch (e) { + result = { + ok: false, + status: e.response.status, + message: e.response.data.error.message, + }; + } + return result; } async completeAzure(prompt: string) { - prompt = this.trimOpenAIPrompt(prompt); - let result: CompletionResult; - try { - const response = await this.azure.createCompletion({ + prompt = this.trimOpenAIPrompt(prompt); + let result: CompletionResult; + try { + const response = await this.azure.createCompletion({ model: getPreset(this.settings).model, prompt, max_tokens: this.settings.maxTokens, n: this.settings.n, temperature: this.settings.temperature, top_p: this.settings.topP, - frequency_penalty: this.settings.frequencyPenalty, - presence_penalty: this.settings.presencePenalty, - }); - result = { ok: true, completions: response.data.choices.map((choice) => choice.text || "") }; - } catch (e) { - result = { ok: false, status: e.response.status, message: e.response.data.error.message }; - } - return result; + frequency_penalty: this.settings.frequencyPenalty, + presence_penalty: this.settings.presencePenalty, + }); + result = { + ok: true, + completions: response.data.choices.map((choice) => choice.text || ""), + }; + } catch (e) { + result = { + ok: false, + status: e.response.status, + message: e.response.data.error.message, + }; + } + return result; } async completeAzureChat(prompt: string) { - prompt = this.trimOpenAIPrompt(prompt); - let result: CompletionResult; - try { - const response = await this.azure.createChatCompletion({ + prompt = this.trimOpenAIPrompt(prompt); + let result: CompletionResult; + try { + const response = await this.azure.createChatCompletion({ model: getPreset(this.settings).model, messages: [{ role: "assistant", content: prompt }], max_tokens: this.settings.maxTokens, n: this.settings.n, temperature: this.settings.temperature, top_p: this.settings.topP, - frequency_penalty: this.settings.frequencyPenalty, - presence_penalty: this.settings.presencePenalty, - }); - result = { ok: true, completions: response.data.choices.map((choice) => choice.message?.content || "") }; - } catch (e) { - result = { ok: false, status: e.response.status, message: e.response.data.error.message }; - } - return result; + frequency_penalty: this.settings.frequencyPenalty, + presence_penalty: this.settings.presencePenalty, + }); + result = { + ok: true, + completions: response.data.choices.map( + (choice) => choice.message?.content || "" + ), + }; + } catch (e) { + result = { + ok: false, + status: e.response.status, + message: e.response.data.error.message, + }; + } + return result; } async completeAnthropic(prompt: string) { - const completions = await Promise.all([...Array(this.settings.n).keys()].map(async () => { return await this.getAnthropicResponse(prompt);} )); + const completions = await Promise.all( + [...Array(this.settings.n).keys()].map(async () => { + return await this.getAnthropicResponse(prompt); + }) + ); const result: CompletionResult = { ok: true, completions }; return result; @@ -1555,17 +1738,21 @@ export default class LoomPlugin extends Plugin { async getAnthropicResponse(prompt: string) { prompt = this.trimOpenAIPrompt(prompt); // let result: CompletionResult; - const body = JSON.stringify({ - model: getPreset(this.settings).model, - max_tokens: this.settings.maxTokens, - temperature: this.settings.temperature, - system: this.settings.systemPrompt, - messages: [ - {"role": "user", "content": `${this.settings.userMessage}`}, - {"role": "assistant", "content": `${prompt}`} - ], - }, null, 2); - if(this.settings.logApiCalls) { + const body = JSON.stringify( + { + model: getPreset(this.settings).model, + max_tokens: this.settings.maxTokens, + temperature: this.settings.temperature, + system: this.settings.systemPrompt, + messages: [ + { role: "user", content: `${this.settings.userMessage}` }, + { role: "assistant", content: `${prompt}` }, + ], + }, + null, + 2 + ); + if (this.settings.logApiCalls) { console.log(`request body: ${body}`); } try { @@ -1575,28 +1762,28 @@ export default class LoomPlugin extends Plugin { headers: { "content-type": "application/json", "anthropic-version": "2023-06-01", - 'x-api-key': this.anthropicApiKey, + "x-api-key": this.anthropicApiKey, }, body, }); - if(response.status !== 200) { + if (response.status !== 200) { console.error("response", response); return null; } - + const result = response.json.content[0]?.text || ""; - // ? { ok: true, completions: [response.json.content[0]?.text || ""] } - // : { ok: false, status: response.status, message: "" }; + // ? { ok: true, completions: [response.json.content[0]?.text || ""] } + // : { ok: false, status: response.status, message: "" }; - if(this.settings.logApiCalls) { + if (this.settings.logApiCalls) { console.log(result); } return result; } catch (e) { - console.error(e) + console.error(e); return null; } } @@ -1649,358 +1836,544 @@ class LoomSettingTab extends PluginSettingTab { method2.createEl("kbd", { text: "Loom: Open Loom pane" }); method2.createEl("span", { text: " command." }); - const presetHeader = containerEl.createDiv({ cls: "setting-item setting-item-heading" }); - presetHeader.createDiv({ cls: "setting-item-name", text: "Presets" }); + const presetHeader = containerEl.createDiv({ + cls: "setting-item setting-item-heading", + }); + presetHeader.createDiv({ cls: "setting-item-name", text: "Presets" }); - const presetEditor = containerEl.createDiv({ cls: "loom__preset-editor setting-item" }); - - const presetList = presetEditor.createDiv({ cls: "loom__preset-list" }); + const presetEditor = containerEl.createDiv({ + cls: "loom__preset-editor setting-item", + }); - const selectPreset = (index: number) => { - this.plugin.settings.modelPreset = index; - this.plugin.save(); - updatePresetFields(); - updatePresetList(); - }; + const presetList = presetEditor.createDiv({ cls: "loom__preset-list" }); - const deletePreset = (index: number) => { - this.plugin.settings.modelPresets.splice(index, 1); - this.plugin.save(); + const selectPreset = (index: number) => { + this.plugin.settings.modelPreset = index; + this.plugin.save(); + updatePresetFields(); + updatePresetList(); + }; - if (index === this.plugin.settings.modelPreset) { - if (this.plugin.settings.modelPresets.length === 0) selectPreset(-1); - else if (index === this.plugin.settings.modelPresets.length) selectPreset(index - 1); - else selectPreset(index); - } - }; + const deletePreset = (index: number) => { + this.plugin.settings.modelPresets.splice(index, 1); + this.plugin.save(); + + if (index === this.plugin.settings.modelPreset) { + if (this.plugin.settings.modelPresets.length === 0) selectPreset(-1); + else if (index === this.plugin.settings.modelPresets.length) + selectPreset(index - 1); + else selectPreset(index); + } + }; const createPreset = (preset: ModelPreset) => { this.plugin.settings.modelPresets.push(preset); - this.plugin.save(); - selectPreset(this.plugin.settings.modelPresets.length - 1); - } - - const newPresetButtons = presetEditor.createDiv({ cls: "loom__new-preset-buttons" }); - - const newPresetButton = newPresetButtons.createEl("button", { text: "New preset" }); - newPresetButton.addEventListener("click", () => { - const newPreset: ModelPreset<"openai"> = { - name: "New preset", - provider: "openai", - model: "davinci-002", - contextLength: 16384, - apiKey: "", - organization: "", - }; - createPreset(newPreset); - }, - ); - - const fillInModelDropdown = newPresetButtons.createEl("select", { cls: "loom__new-preset-button dropdown" }); - fillInModelDropdown.createEl("option", { - text: "Fill in model details...", - attr: { value: "none", selected: "", disabled: "" }, - }); - - fillInModelDropdown.createEl("option", { text: "davinci-002", attr: { value: "davinci-002" } }); - fillInModelDropdown.createEl("option", { text: "code-davinci-002", attr: { value: "code-davinci-002" } }); - fillInModelDropdown.createEl("option", { text: "code-davinci-002 (Proxy)", attr: { value: "code-davinci-002-proxy" } }); - fillInModelDropdown.createEl("option", { text: "gpt-4-base", attr: { value: "gpt-4-base" } }); - fillInModelDropdown.createEl("option", { text: "claude-3-opus", attr: { value: "claude-3-opus" } }); - - fillInModelDropdown.addEventListener("change", (event) => { - const value = (event.target as HTMLSelectElement).value; - switch (value) { - case "davinci-002": { - this.plugin.settings.modelPresets[this.plugin.settings.modelPreset].provider = "openai"; - this.plugin.settings.modelPresets[this.plugin.settings.modelPreset].model = "davinci-002"; - this.plugin.settings.modelPresets[this.plugin.settings.modelPreset].contextLength = 16384; - break; - } - case "code-davinci-002": { - this.plugin.settings.modelPresets[this.plugin.settings.modelPreset].provider = "openai"; - this.plugin.settings.modelPresets[this.plugin.settings.modelPreset].model = "code-davinci-002"; - this.plugin.settings.modelPresets[this.plugin.settings.modelPreset].contextLength = 8001; - break; - } - case "code-davinci-002-proxy": { - this.plugin.settings.modelPresets[this.plugin.settings.modelPreset].provider = "ocp"; - this.plugin.settings.modelPresets[this.plugin.settings.modelPreset].model = "code-davinci-002"; - this.plugin.settings.modelPresets[this.plugin.settings.modelPreset].contextLength = 8001; - break; - } - case "gpt-4-base": { - this.plugin.settings.modelPresets[this.plugin.settings.modelPreset].provider = "openai"; - this.plugin.settings.modelPresets[this.plugin.settings.modelPreset].model = "gpt-4-base"; - this.plugin.settings.modelPresets[this.plugin.settings.modelPreset].contextLength = 8192; - break; - } - case "claude-3-opus": { - this.plugin.settings.modelPresets[this.plugin.settings.modelPreset].provider = "anthropic"; - this.plugin.settings.modelPresets[this.plugin.settings.modelPreset].model = "claude-3-opus-20240229"; - this.plugin.settings.modelPresets[this.plugin.settings.modelPreset].contextLength = 20000; - break; - } - } - this.plugin.save(); - updatePresetFields(); - - fillInModelDropdown.value = "none"; - }); - - const restoreApiKeyDropdown = newPresetButtons.createEl("select", { cls: "loom__new-preset-button dropdown" }); - restoreApiKeyDropdown.createEl("option", { - text: "Restore API key from pre-1.19...", - attr: { value: "none", selected: "", disabled: "" }, - }); - - restoreApiKeyDropdown.createEl("option", { text: "OpenAI", attr: { value: "openai" } }); - restoreApiKeyDropdown.createEl("option", { text: "OpenAI-compatible API", attr: { value: "ocp" } }); - restoreApiKeyDropdown.createEl("option", { text: "Cohere", attr: { value: "cohere" } }); - restoreApiKeyDropdown.createEl("option", { text: "TextSynth", attr: { value: "textsynth" } }); - restoreApiKeyDropdown.createEl("option", { text: "Azure", attr: { value: "azure" } }); - restoreApiKeyDropdown.createEl("option", { text: "Anthropic", attr: { value: "anthropic" } }); - - restoreApiKeyDropdown.addEventListener("change", (event) => { - const provider = (event.target as HTMLSelectElement).value as Provider; - let preset = { name: "New preset", provider, model: "", contextLength: "" }; - switch (provider) { - case "openai": { - preset = { - ...preset, - // @ts-expect-error - apiKey: this.plugin.settings.openaiApiKey || "", - // @ts-expect-error - organization: this.plugin.settings.openaiOrganization || "", - }; - break; - } - case "ocp": { - preset = { - ...preset, - // @ts-expect-error - apiKey: this.plugin.settings.ocpApiKey || "", - // @ts-expect-error - url: this.plugin.settings.ocpUrl || "", - }; - break; - } - case "cohere": { - preset = { - ...preset, - // @ts-expect-error - apiKey: this.plugin.settings.cohereApiKey || "", - }; - break; - } - case "textsynth": { - preset = { - ...preset, - // @ts-expect-error - apiKey: this.plugin.settings.textsynthApiKey || "", - }; - break; - } - case "azure": { - preset = { - ...preset, - // @ts-expect-error - apiKey: this.plugin.settings.azureApiKey || "", - // @ts-expect-error - endpoint: this.plugin.settings.azureEndpoint || "", - }; - break; - } - case "anthropic": { - preset = { - ...preset, - // @ts-expect-error - apiKey: this.plugin.settings.anthropicApiKey || "", - // // @ts-expect-error - // systemPrompt: this.plugin.settings.anthropicSystemPrompt || "", - // // @ts-expect-error - // userMessage: this.plugin.settings.anthropicUserMessage || "", + this.plugin.save(); + selectPreset(this.plugin.settings.modelPresets.length - 1); + }; + + const newPresetButtons = presetEditor.createDiv({ + cls: "loom__new-preset-buttons", + }); + + const newPresetButton = newPresetButtons.createEl("button", { + text: "New preset", + }); + newPresetButton.addEventListener("click", () => { + const newPreset: ModelPreset<"openai"> = { + name: "New preset", + provider: "openai", + model: "davinci-002", + contextLength: 16384, + apiKey: "", + organization: "", }; - break; - } - default: { - throw new Error(`Unknown provider: ${provider}`); - } - } - // @ts-expect-error TODO + createPreset(newPreset); + }); + + const fillInModelDropdown = newPresetButtons.createEl("select", { + cls: "loom__new-preset-button dropdown", + }); + fillInModelDropdown.createEl("option", { + text: "Fill in model details...", + attr: { value: "none", selected: "", disabled: "" }, + }); + + fillInModelDropdown.createEl("option", { + text: "davinci-002", + attr: { value: "davinci-002" }, + }); + fillInModelDropdown.createEl("option", { + text: "code-davinci-002", + attr: { value: "code-davinci-002" }, + }); + fillInModelDropdown.createEl("option", { + text: "code-davinci-002 (Proxy)", + attr: { value: "code-davinci-002-proxy" }, + }); + fillInModelDropdown.createEl("option", { + text: "gpt-4-base", + attr: { value: "gpt-4-base" }, + }); + fillInModelDropdown.createEl("option", { + text: "claude-3-opus", + attr: { value: "claude-3-opus" }, + }); + + fillInModelDropdown.addEventListener("change", (event) => { + const value = (event.target as HTMLSelectElement).value; + switch (value) { + case "davinci-002": { + this.plugin.settings.modelPresets[ + this.plugin.settings.modelPreset + ].provider = "openai"; + this.plugin.settings.modelPresets[ + this.plugin.settings.modelPreset + ].model = "davinci-002"; + this.plugin.settings.modelPresets[ + this.plugin.settings.modelPreset + ].contextLength = 16384; + break; + } + case "code-davinci-002": { + this.plugin.settings.modelPresets[ + this.plugin.settings.modelPreset + ].provider = "openai"; + this.plugin.settings.modelPresets[ + this.plugin.settings.modelPreset + ].model = "code-davinci-002"; + this.plugin.settings.modelPresets[ + this.plugin.settings.modelPreset + ].contextLength = 8001; + break; + } + case "code-davinci-002-proxy": { + this.plugin.settings.modelPresets[ + this.plugin.settings.modelPreset + ].provider = "openai-compat"; + this.plugin.settings.modelPresets[ + this.plugin.settings.modelPreset + ].model = "code-davinci-002"; + this.plugin.settings.modelPresets[ + this.plugin.settings.modelPreset + ].contextLength = 8001; + break; + } + case "gpt-4-base": { + this.plugin.settings.modelPresets[ + this.plugin.settings.modelPreset + ].provider = "openai"; + this.plugin.settings.modelPresets[ + this.plugin.settings.modelPreset + ].model = "gpt-4-base"; + this.plugin.settings.modelPresets[ + this.plugin.settings.modelPreset + ].contextLength = 8192; + break; + } + case "claude-3-opus": { + this.plugin.settings.modelPresets[ + this.plugin.settings.modelPreset + ].provider = "anthropic"; + this.plugin.settings.modelPresets[ + this.plugin.settings.modelPreset + ].model = "claude-3-opus-20240229"; + this.plugin.settings.modelPresets[ + this.plugin.settings.modelPreset + ].contextLength = 20000; + break; + } + } + this.plugin.save(); + updatePresetFields(); + + fillInModelDropdown.value = "none"; + }); + + const restoreApiKeyDropdown = newPresetButtons.createEl("select", { + cls: "loom__new-preset-button dropdown", + }); + restoreApiKeyDropdown.createEl("option", { + text: "Restore API key from pre-1.19...", + attr: { value: "none", selected: "", disabled: "" }, + }); + + restoreApiKeyDropdown.createEl("option", { + text: "OpenAI-compatible API", + attr: { value: "openai-compat" }, + }); + restoreApiKeyDropdown.createEl("option", { + text: "Anthropic", + attr: { value: "anthropic" }, + }); + restoreApiKeyDropdown.createEl("option", { + text: "OpenAI", + attr: { value: "openai" }, + }); + restoreApiKeyDropdown.createEl("option", { + text: "Azure", + attr: { value: "azure" }, + }); + restoreApiKeyDropdown.createEl("option", { + text: "Cohere", + attr: { value: "cohere" }, + }); + restoreApiKeyDropdown.createEl("option", { + text: "TextSynth", + attr: { value: "textsynth" }, + }); + + restoreApiKeyDropdown.addEventListener("change", (event) => { + const provider = (event.target as HTMLSelectElement).value as Provider; + let preset = { + name: "New preset", + provider, + model: "", + contextLength: "", + }; + switch (provider) { + case "openai": { + preset = { + ...preset, + // @ts-expect-error + apiKey: this.plugin.settings.openaiApiKey || "", + // @ts-expect-error + organization: this.plugin.settings.openaiOrganization || "", + }; + break; + } + case "openai-compat": { + preset = { + ...preset, + // @ts-expect-error + apiKey: this.plugin.settings.ocpApiKey || "", + // @ts-expect-error + url: this.plugin.settings.ocpUrl || "", + }; + break; + } + case "cohere": { + preset = { + ...preset, + // @ts-expect-error + apiKey: this.plugin.settings.cohereApiKey || "", + }; + break; + } + case "textsynth": { + preset = { + ...preset, + // @ts-expect-error + apiKey: this.plugin.settings.textsynthApiKey || "", + }; + break; + } + case "azure": { + preset = { + ...preset, + // @ts-expect-error + apiKey: this.plugin.settings.azureApiKey || "", + // @ts-expect-error + endpoint: this.plugin.settings.azureEndpoint || "", + }; + break; + } + case "anthropic": { + preset = { + ...preset, + // @ts-expect-error + apiKey: this.plugin.settings.anthropicApiKey || "", + // // @ts-expect-error + // systemPrompt: this.plugin.settings.anthropicSystemPrompt || "", + // // @ts-expect-error + // userMessage: this.plugin.settings.anthropicUserMessage || "", + }; + break; + } + default: { + throw new Error(`Unknown provider: ${provider}`); + } + } + // @ts-expect-error TODO createPreset(preset); - restoreApiKeyDropdown.value = "none"; - }); + restoreApiKeyDropdown.value = "none"; + }); - // edit preset fields + // edit preset fields const presetFields = containerEl.createDiv(); const updatePresetFields = () => { - presetFields.empty(); - - if (this.plugin.settings.modelPreset === -1) { - presetFields.createEl("p", { cls: "loom__no-preset-selected", text: "No preset selected." }); - return; - } - - new Setting(presetFields).setName("Name").addText((text) => - text.setValue(this.plugin.settings.modelPresets[this.plugin.settings.modelPreset].name).onChange((value) => { - this.plugin.settings.modelPresets[this.plugin.settings.modelPreset].name = value; - this.plugin.saveAndRender(); - updatePresetList(); - }), - ); - - new Setting(presetFields).setName("Provider").addDropdown((dropdown) => { - const options: Record = { - cohere: "Cohere", - textsynth: "TextSynth", - ocp: "OpenAI-compatible API", - openai: "OpenAI", - "openai-chat": "OpenAI (Chat)", - azure: "Azure", - "azure-chat": "Azure (Chat)", - anthropic: "Anthropic", - }; - dropdown.addOptions(options); - dropdown.setValue(this.plugin.settings.modelPresets[this.plugin.settings.modelPreset].provider); - dropdown.onChange(async (value) => { - this.plugin.settings.modelPresets[this.plugin.settings.modelPreset].provider = value; - await this.plugin.save(); - updatePresetFields(); - }); - }); - - new Setting(presetFields).setName("Model").addText((text) => - text.setValue(this.plugin.settings.modelPresets[this.plugin.settings.modelPreset].model).onChange(async (value) => { - this.plugin.settings.modelPresets[this.plugin.settings.modelPreset].model = value; - await this.plugin.save(); - }), - ); - - new Setting(presetFields).setName("Context length").addText((text) => - text.setValue(this.plugin.settings.modelPresets[this.plugin.settings.modelPreset].contextLength.toString()).onChange(async (value) => { - this.plugin.settings.modelPresets[this.plugin.settings.modelPreset].contextLength = parseInt(value); - await this.plugin.save(); - }), - ); - - new Setting(presetFields).setName("API key").addText((text) => - text.setValue(this.plugin.settings.modelPresets[this.plugin.settings.modelPreset].apiKey).onChange(async (value) => { - this.plugin.settings.modelPresets[this.plugin.settings.modelPreset].apiKey = value; - await this.plugin.save(); - }), - ); - - if (["openai", "openai-chat"].includes(this.plugin.settings.modelPresets[this.plugin.settings.modelPreset].provider)) { - new Setting(presetFields).setName("Organization").addText((text) => - // @ts-expect-error TODO - text.setValue(this.plugin.settings.modelPresets[this.plugin.settings.modelPreset].organization).onChange(async (value) => { - // @ts-expect-error TODO - this.plugin.settings.modelPresets[this.plugin.settings.modelPreset].organization = value; - await this.plugin.save(); - }), - ); - } - - if (["ocp", "azure", "azure-chat"].includes(this.plugin.settings.modelPresets[this.plugin.settings.modelPreset].provider)) { - new Setting(presetFields).setName("URL").addText((text) => - // @ts-expect-error TODO - text.setValue(this.plugin.settings.modelPresets[this.plugin.settings.modelPreset].url).onChange(async (value) => { - // @ts-expect-error TODO - this.plugin.settings.modelPresets[this.plugin.settings.modelPreset].url = value; - await this.plugin.save(); - }), - ); - } - } + presetFields.empty(); + + if (this.plugin.settings.modelPreset === -1) { + presetFields.createEl("p", { + cls: "loom__no-preset-selected", + text: "No preset selected.", + }); + return; + } + + new Setting(presetFields).setName("Name").addText((text) => + text + .setValue( + this.plugin.settings.modelPresets[this.plugin.settings.modelPreset] + .name + ) + .onChange((value) => { + this.plugin.settings.modelPresets[ + this.plugin.settings.modelPreset + ].name = value; + this.plugin.saveAndRender(); + updatePresetList(); + }) + ); + + new Setting(presetFields).setName("Provider").addDropdown((dropdown) => { + const options: Record = { + "openai-compat": "OpenAI-compatible API", + "openrouter": "OpenRouter", + anthropic: "Anthropic", + openai: "OpenAI", + "openai-chat": "OpenAI (Chat)", + azure: "Azure", + "azure-chat": "Azure (Chat)", + cohere: "Cohere", + textsynth: "TextSynth", + }; + dropdown.addOptions(options); + dropdown.setValue( + this.plugin.settings.modelPresets[this.plugin.settings.modelPreset] + .provider + ); + dropdown.onChange(async (value) => { + this.plugin.settings.modelPresets[ + this.plugin.settings.modelPreset + ].provider = value; + await this.plugin.save(); + updatePresetFields(); + }); + }); + + new Setting(presetFields).setName("Model").addText((text) => + text + .setValue( + this.plugin.settings.modelPresets[this.plugin.settings.modelPreset] + .model + ) + .onChange(async (value) => { + this.plugin.settings.modelPresets[ + this.plugin.settings.modelPreset + ].model = value; + await this.plugin.save(); + }) + ); + + new Setting(presetFields).setName("Context length").addText((text) => + text + .setValue( + this.plugin.settings.modelPresets[ + this.plugin.settings.modelPreset + ].contextLength.toString() + ) + .onChange(async (value) => { + this.plugin.settings.modelPresets[ + this.plugin.settings.modelPreset + ].contextLength = parseInt(value); + await this.plugin.save(); + }) + ); + + new Setting(presetFields).setName("API key").addText((text) => + text + .setValue( + this.plugin.settings.modelPresets[this.plugin.settings.modelPreset] + .apiKey + ) + .onChange(async (value) => { + this.plugin.settings.modelPresets[ + this.plugin.settings.modelPreset + ].apiKey = value; + await this.plugin.save(); + }) + ); + + if ( + ["openai", "openai-chat"].includes( + this.plugin.settings.modelPresets[this.plugin.settings.modelPreset] + .provider + ) + ) { + new Setting(presetFields).setName("Organization").addText((text) => + text + .setValue( + this.plugin.settings.modelPresets[ + this.plugin.settings.modelPreset + // @ts-expect-error TODO + ].organization + ) + .onChange(async (value) => { + this.plugin.settings.modelPresets[ + this.plugin.settings.modelPreset + // @ts-expect-error TODO + ].organization = value; + await this.plugin.save(); + }) + ); + } + + if ( + ["openai-compat", "azure", "azure-chat"].includes( + this.plugin.settings.modelPresets[this.plugin.settings.modelPreset] + .provider + ) + ) { + new Setting(presetFields).setName("URL").addText((text) => + text + .setValue( + this.plugin.settings.modelPresets[ + this.plugin.settings.modelPreset + // @ts-expect-error TODO + ].url + ) + .onChange(async (value) => { + this.plugin.settings.modelPresets[ + this.plugin.settings.modelPreset + // @ts-expect-error TODO + ].url = value; + await this.plugin.save(); + }) + ); + } + + if (this.plugin.settings.modelPresets[this.plugin.settings.modelPreset].provider === "openrouter") { + new Setting(presetFields).setName("Quantization").addDropdown((dropdown) => + dropdown + .addOptions({ + bf16: "bf16", + fp16: "fp16", + fp8: "fp8", + int8: "int8", + int4: "int4" + }) + .setValue( + this.plugin.settings.modelPresets[ + this.plugin.settings.modelPreset + // @ts-expect-error TODO + ].quantization + ) + .onChange(async (value) => { + this.plugin.settings.modelPresets[ + this.plugin.settings.modelPreset + // @ts-expect-error TODO + ].quantization = value; + await this.plugin.save(); + }) + ); + } + }; const updatePresetList = () => { - presetList.empty(); - for (const i in this.plugin.settings.modelPresets) { - const preset = this.plugin.settings.modelPresets[i]; - const isActive = this.plugin.settings.modelPreset === parseInt(i); - - const presetContainer = presetList.createDiv( - { cls: `loom__preset is-clickable outgoing-link-item tree-item-self${isActive ? " is-active" : ""}` } - ); - presetContainer.addEventListener("click", () => selectPreset(parseInt(i))); - - presetContainer.createSpan({ cls: "loom__preset-name tree-item-inner", text: preset.name }); - - const deletePresetOuter = presetContainer.createDiv({ cls: "loom__preset-buttons" }); - const deletePresetInner = deletePresetOuter.createDiv({ - cls: "loom__preset-button", - attr: { "aria-label": "Delete" }, - }); - setIcon(deletePresetInner, "trash-2"); - deletePresetInner.addEventListener("click", (event) => { + presetList.empty(); + for (const i in this.plugin.settings.modelPresets) { + const preset = this.plugin.settings.modelPresets[i]; + const isActive = this.plugin.settings.modelPreset === parseInt(i); + + const presetContainer = presetList.createDiv({ + cls: `loom__preset is-clickable outgoing-link-item tree-item-self${ + isActive ? " is-active" : "" + }`, + }); + presetContainer.addEventListener("click", () => + selectPreset(parseInt(i)) + ); + + presetContainer.createSpan({ + cls: "loom__preset-name tree-item-inner", + text: preset.name, + }); + + const deletePresetOuter = presetContainer.createDiv({ + cls: "loom__preset-buttons", + }); + const deletePresetInner = deletePresetOuter.createDiv({ + cls: "loom__preset-button", + attr: { "aria-label": "Delete" }, + }); + setIcon(deletePresetInner, "trash-2"); + deletePresetInner.addEventListener("click", (event) => { event.stopPropagation(); - deletePreset(parseInt(i)) - }); - } - }; + deletePreset(parseInt(i)); + }); + } + }; - updatePresetFields(); - updatePresetList(); + updatePresetFields(); + updatePresetList(); // TODO simplify below? - const passagesHeader = containerEl.createDiv({ cls: "setting-item setting-item-heading" }); - passagesHeader.createDiv({ cls: "setting-item-name", text: "Passages" }); + const passagesHeader = containerEl.createDiv({ + cls: "setting-item setting-item-heading", + }); + passagesHeader.createDiv({ cls: "setting-item-name", text: "Passages" }); - const setting = ( - name: string, - key: LoomSettingKey, - toText: (value: any) => string, - fromText: (text: string) => any - ) => { + const setting = ( + name: string, + key: LoomSettingKey, + toText: (value: any) => string, + fromText: (text: string) => any + ) => { new Setting(containerEl).setName(name).addText((text) => - text.setValue(toText(this.plugin.settings[key])).onChange(async (value) => { - // @ts-expect-error - this.plugin.settings[key] = fromText(value); - await this.plugin.save(); - }) - ); - } - - const idSetting = (name: string, key: LoomSettingKey) => - setting(name, key, (value) => value, (text) => text); - - new Setting(containerEl) - .setName("Passage folder location") - .setDesc("Passages can be quickly combined into a multipart prompt") - .addText((text) => - text.setValue(this.plugin.settings.passageFolder).onChange(async (value) => { - this.plugin.settings.passageFolder = value; - await this.plugin.save(); - }) - ); - + text + .setValue(toText(this.plugin.settings[key])) + .onChange(async (value) => { + // @ts-expect-error + this.plugin.settings[key] = fromText(value); + await this.plugin.save(); + }) + ); + }; + + const idSetting = (name: string, key: LoomSettingKey) => + setting( + name, + key, + (value) => value, + (text) => text + ); + + new Setting(containerEl) + .setName("Passage folder location") + .setDesc("Passages can be quickly combined into a multipart prompt") + .addText((text) => + text + .setValue(this.plugin.settings.passageFolder) + .onChange(async (value) => { + this.plugin.settings.passageFolder = value; + await this.plugin.save(); + }) + ); + idSetting("Default passage separator", "defaultPassageSeparator"); idSetting("Default passage frontmatter", "defaultPassageFrontmatter"); - - const debugHeader = containerEl.createDiv({ cls: "setting-item setting-item-heading" }); + const debugHeader = containerEl.createDiv({ + cls: "setting-item setting-item-heading", + }); debugHeader.createDiv({ cls: "setting-item-name", text: "Debug" }); - + new Setting(containerEl) .setName("Log API calls") .setDesc("Log API calls to the console") .addToggle((toggle) => - toggle.setValue(this.plugin.settings.logApiCalls).onChange(async (value) => { - this.plugin.settings.logApiCalls = value; - await this.plugin.save(); - }) + toggle + .setValue(this.plugin.settings.logApiCalls) + .onChange(async (value) => { + this.plugin.settings.logApiCalls = value; + await this.plugin.save(); + }) ); - - new Setting(containerEl) - } - - - - } diff --git a/manifest.json b/manifest.json index 0493d8e..fcc5c0b 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "loom", "name": "Loom", - "version": "1.21.3", + "version": "1.22.0", "minAppVersion": "0.15.0", "description": "Loom in Obsidian", "author": "celeste", diff --git a/package.json b/package.json index 42989e9..bc177cd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-loom", - "version": "1.21.3", + "version": "1.22.0", "description": "Loom in Obsidian", "main": "main.js", "scripts": { diff --git a/views.ts b/views.ts index e0c7108..5b65556 100644 --- a/views.ts +++ b/views.ts @@ -1,5 +1,13 @@ import { LoomSettings, Node, NoteState } from "./common"; -import { App, ItemView, Menu, Modal, Setting, WorkspaceLeaf, setIcon } from "obsidian"; +import { + App, + ItemView, + Menu, + Modal, + Setting, + WorkspaceLeaf, + setIcon, +} from "obsidian"; import { Range } from "@codemirror/state"; import { Decoration, @@ -19,32 +27,37 @@ interface NodeContext { deletable: boolean; } -const showNodeMenu = (event: MouseEvent, { app, state, id, node, deletable }: NodeContext) => { +const showNodeMenu = ( + event: MouseEvent, + { app, state, id, node, deletable }: NodeContext +) => { const menu = new Menu(); const menuItem = (name: string, icon: string, callback: () => void) => menu.addItem((item) => { item.setTitle(name); - item.setIcon(icon); - item.onClick(callback); - }); - + item.setIcon(icon); + item.onClick(callback); + }); + const zeroArgMenuItem = (name: string, icon: string, event: string) => menuItem(name, icon, () => app.workspace.trigger(event)); const selfArgMenuItem = (name: string, icon: string, event: string) => menuItem(name, icon, () => app.workspace.trigger(event, id)); const selfListArgMenuItem = (name: string, icon: string, event: string) => menuItem(name, icon, () => app.workspace.trigger(event, [id])); - + if (state.hoisted[state.hoisted.length - 1] === id) zeroArgMenuItem("Unhoist", "arrow-down", "loom:unhoist"); - else - selfArgMenuItem("Hoist", "arrow-up", "loom:hoist"); - + else selfArgMenuItem("Hoist", "arrow-up", "loom:hoist"); + if (node.bookmarked) - selfArgMenuItem("Remove bookmark", "bookmark-minus", "loom:toggle-bookmark"); - else - selfArgMenuItem("Bookmark", "bookmark", "loom:toggle-bookmark"); + selfArgMenuItem( + "Remove bookmark", + "bookmark-minus", + "loom:toggle-bookmark" + ); + else selfArgMenuItem("Bookmark", "bookmark", "loom:toggle-bookmark"); menu.addSeparator(); selfArgMenuItem("Create child", "plus", "loom:create-child"); @@ -56,49 +69,63 @@ const showNodeMenu = (event: MouseEvent, { app, state, id, node, deletable }: No if (node.parentId !== null) { menu.addSeparator(); - selfArgMenuItem("Merge with parent", "arrow-up-left", "loom:merge-with-parent"); + selfArgMenuItem( + "Merge with parent", + "arrow-up-left", + "loom:merge-with-parent" + ); } if (deletable) { - menu.addSeparator(); - selfListArgMenuItem("Delete", "trash", "loom:delete"); + menu.addSeparator(); + selfListArgMenuItem("Delete", "trash", "loom:delete"); } - + menu.showAtMouseEvent(event); -} +}; const renderNodeButtons = ( container: HTMLElement, { app, state, id, node, deletable }: NodeContext ) => { - const button = (label: string, icon: string, callback: (event: MouseEvent) => void) => { - const button_ = container.createDiv({ - cls: "loom__node-button", - attr: { "aria-label": label }, - }); - setIcon(button_, icon); - button_.addEventListener("click", event => { event.stopPropagation(); callback(event); }); + const button = ( + label: string, + icon: string, + callback: (event: MouseEvent) => void + ) => { + const button_ = container.createDiv({ + cls: "loom__node-button", + attr: { "aria-label": label }, + }); + setIcon(button_, icon); + button_.addEventListener("click", (event) => { + event.stopPropagation(); + callback(event); + }); }; - button("Show menu", "menu", (event) => showNodeMenu(event, { app, state, id, node, deletable })); + button("Show menu", "menu", (event) => + showNodeMenu(event, { app, state, id, node, deletable }) + ); if (state.hoisted[state.hoisted.length - 1] === id) - button("Unhoist", "arrow-down", () => app.workspace.trigger("loom:unhoist")); - else button("Hoist", "arrow-up", () => app.workspace.trigger("loom:hoist", id)); + button("Unhoist", "arrow-down", () => + app.workspace.trigger("loom:unhoist") + ); + else + button("Hoist", "arrow-up", () => app.workspace.trigger("loom:hoist", id)); if (node.bookmarked) - button( - "Remove bookmark", - "bookmark-minus", - () => app.workspace.trigger("loom:toggle-bookmark", id) - ); + button("Remove bookmark", "bookmark-minus", () => + app.workspace.trigger("loom:toggle-bookmark", id) + ); else - button("Bookmark", "bookmark", () => - app.workspace.trigger("loom:toggle-bookmark", id) - ); - + button("Bookmark", "bookmark", () => + app.workspace.trigger("loom:toggle-bookmark", id) + ); + if (deletable) - button("Delete", "trash", () => app.workspace.trigger("loom:delete", [id])); + button("Delete", "trash", () => app.workspace.trigger("loom:delete", [id])); }; export class LoomView extends ItemView { @@ -109,13 +136,13 @@ export class LoomView extends ItemView { constructor( leaf: WorkspaceLeaf, - getNoteState: () => NoteState | null, - getSettings: () => LoomSettings + getNoteState: () => NoteState | null, + getSettings: () => LoomSettings ) { super(leaf); - this.getNoteState = getNoteState; - this.getSettings = getSettings; + this.getNoteState = getNoteState; + this.getSettings = getSettings; } async onOpen() { @@ -124,38 +151,41 @@ export class LoomView extends ItemView { render() { const state = this.getNoteState(); - const settings = this.getSettings(); + const settings = this.getSettings(); - const scroll = this.containerEl.scrollTop; + const scroll = this.containerEl.scrollTop; - this.containerEl.empty(); - this.containerEl.addClass("loom__view"); + this.containerEl.empty(); + this.containerEl.addClass("loom__view"); - this.renderNavButtons(settings); - const container = this.containerEl.createDiv({ cls: "outline" }); - if (settings.showExport) this.renderAltExportInterface(container); - if (settings.showSearchBar) this.renderSearchBar(container, state); - if (settings.showSettings) this.renderSettings(container, settings); + this.renderNavButtons(settings); + const container = this.containerEl.createDiv({ cls: "outline" }); + if (settings.showExport) this.renderAltExportInterface(container); + if (settings.showSearchBar) this.renderSearchBar(container, state); + if (settings.showSettings) this.renderSettings(container, settings); - if (!state) { + if (!state) { container.createDiv({ cls: "pane-empty", text: "No note selected." }); - return; - } - this.renderBookmarks(container, state); - this.tree = container.createDiv(); - this.renderTree(this.tree, state); - - this.containerEl.scrollTop = scroll; - - // scroll to active node in the tree - const activeNode = this.tree.querySelector(".is-active"); - if (activeNode){ //&& !container.contains(activeNode)){ - activeNode.scrollIntoView({ block: "nearest" }); - } + return; + } + this.renderBookmarks(container, state); + this.tree = container.createDiv(); + this.renderTree(this.tree, state); + + this.containerEl.scrollTop = scroll; + + // scroll to active node in the tree + const activeNode = this.tree.querySelector(".is-active"); + if (activeNode) { + //&& !container.contains(activeNode)){ + activeNode.scrollIntoView({ block: "nearest" }); + } } renderNavButtons(settings: LoomSettings) { - const navButtonsContainer = this.containerEl.createDiv({ cls: "nav-buttons-container loom__nav-buttons" }); + const navButtonsContainer = this.containerEl.createDiv({ + cls: "nav-buttons-container loom__nav-buttons", + }); // buttons to toggle 1) settings 2) node borders in the editor @@ -194,362 +224,453 @@ export class LoomView extends ItemView { "Show node borders in the editor" ); - // the import button - - const importInput = navButtonsContainer.createEl("input", { - cls: "hidden", - attr: { type: "file", id: "loom__import-input" }, - }); + // the import button + + const importInput = navButtonsContainer.createEl("input", { + cls: "hidden", + attr: { type: "file", id: "loom__import-input" }, + }); const importNavButton = navButtonsContainer.createEl("label", { cls: "clickable-icon nav-action-button", attr: { "aria-label": "Import JSON", for: "loom__import-input" }, }); - setIcon(importNavButton, "import"); - - importInput.addEventListener("change", () => { - // @ts-expect-error - const path = importInput.files?.[0].path; - if (path) this.app.workspace.trigger("loom:import", path); - }); - - // the export button - - const exportNavButton = navButtonsContainer.createDiv({ - cls: `clickable-icon nav-action-button${settings.showExport ? " is-active" : ""}`, - attr: { "aria-label": "Export to JSON" }, - }); - setIcon(exportNavButton, "download"); - - exportNavButton.addEventListener("click", (event) => { - if (event.shiftKey) { - this.app.workspace.trigger("loom:set-setting", "showExport", !settings.showExport); - return; - } - dialog - .showSaveDialog({ title: "Export to JSON", filters: [{ extensions: ["json"] }] }) - .then((result: any) => { - if (result && result.filePath) - this.app.workspace.trigger("loom:export", result.filePath); - }); - }); + setIcon(importNavButton, "import"); + + importInput.addEventListener("change", () => { + // @ts-expect-error + const path = importInput.files?.[0].path; + if (path) this.app.workspace.trigger("loom:import", path); + }); + + // the export button + + const exportNavButton = navButtonsContainer.createDiv({ + cls: `clickable-icon nav-action-button${ + settings.showExport ? " is-active" : "" + }`, + attr: { "aria-label": "Export to JSON" }, + }); + setIcon(exportNavButton, "download"); + + exportNavButton.addEventListener("click", (event) => { + if (event.shiftKey) { + this.app.workspace.trigger( + "loom:set-setting", + "showExport", + !settings.showExport + ); + return; + } + dialog + .showSaveDialog({ + title: "Export to JSON", + filters: [{ extensions: ["json"] }], + }) + .then((result: any) => { + if (result && result.filePath) + this.app.workspace.trigger("loom:export", result.filePath); + }); + }); } renderAltExportInterface(container: HTMLElement) { - const exportContainer = container.createDiv({ cls: "loom__alt-export-field" }); - const exportInput = exportContainer.createEl("input", { - attr: { type: "text", placeholder: "Path to export to" }, - }); - const exportButton = exportContainer.createEl("button", {}); - setIcon(exportButton, "download"); - - exportButton.addEventListener("click", () => { - if (exportInput.value) this.app.workspace.trigger("loom:export", exportInput.value); - }); + const exportContainer = container.createDiv({ + cls: "loom__alt-export-field", + }); + const exportInput = exportContainer.createEl("input", { + attr: { type: "text", placeholder: "Path to export to" }, + }); + const exportButton = exportContainer.createEl("button", {}); + setIcon(exportButton, "download"); + + exportButton.addEventListener("click", () => { + if (exportInput.value) + this.app.workspace.trigger("loom:export", exportInput.value); + }); } renderSearchBar(container: HTMLElement, state: NoteState | null) { - const searchBar = container.createEl("input", { - cls: "loom__search-bar", - value: state?.searchTerm || "", - attr: { type: "text", placeholder: "Search" }, - }); - searchBar.addEventListener("input", () => { - const state = this.getNoteState(); - this.app.workspace.trigger("loom:search", searchBar.value); - if (state) { - this.renderTree(this.tree, state); - if (Object.values(state.nodes).every((node) => node.searchResultState === "none")) - searchBar.addClass("loom__search-bar-no-results"); - else searchBar.removeClass("loom__search-bar-no-results"); - } - }); + const searchBar = container.createEl("input", { + cls: "loom__search-bar", + value: state?.searchTerm || "", + attr: { type: "text", placeholder: "Search" }, + }); + searchBar.addEventListener("input", () => { + const state = this.getNoteState(); + this.app.workspace.trigger("loom:search", searchBar.value); + if (state) { + this.renderTree(this.tree, state); + if ( + Object.values(state.nodes).every( + (node) => node.searchResultState === "none" + ) + ) + searchBar.addClass("loom__search-bar-no-results"); + else searchBar.removeClass("loom__search-bar-no-results"); + } + }); } renderSettings(container: HTMLElement, settings: LoomSettings) { const settingsContainer = container.createDiv({ cls: "loom__settings" }); - + // visibility checkboxes - - const visibilityContainer = settingsContainer.createDiv({ cls: "loom__visibility" }); - - const createCheckbox = (id: string, label: string, ellipsis: boolean = false) => { - const checkboxContainer = visibilityContainer.createSpan({ cls: "loom__visibility-item" }); - const checkbox = checkboxContainer.createEl("input", { - attr: { - id: `loom__${id}-checkbox`, - checked: settings.visibility[id] ? "checked" : null - }, - type: "checkbox", - }); + + const visibilityContainer = settingsContainer.createDiv({ + cls: "loom__visibility", + }); + + const createCheckbox = ( + id: string, + label: string, + ellipsis: boolean = false + ) => { + const checkboxContainer = visibilityContainer.createSpan({ + cls: "loom__visibility-item", + }); + const checkbox = checkboxContainer.createEl("input", { + attr: { + id: `loom__${id}-checkbox`, + checked: settings.visibility[id] ? "checked" : null, + }, + type: "checkbox", + }); checkbox.addEventListener("change", () => - this.app.workspace.trigger("loom:set-visibility-setting", id, checkbox.checked) - ); - - const checkboxLabel = checkboxContainer.createEl("label", { - attr: { for: `loom__${id}-checkbox` }, - cls: "loom__visibility-item-label", - text: label, - }); - if (ellipsis && !settings.visibility.visibility) checkboxLabel.createSpan({ - cls: "loom__no-metavisibility", - text: "...", - }); - }; - - createCheckbox("visibility", "These checkboxes", true); - if (settings.visibility["visibility"]) { - createCheckbox("modelPreset", "Model preset"); - createCheckbox("maxTokens", "Length"); - createCheckbox("n", "Number of completions"); - createCheckbox("bestOf", "Best of"); - createCheckbox("temperature", "Temperature"); - createCheckbox("topP", "Top p"); - createCheckbox("frequencyPenalty", "Frequency penalty"); - createCheckbox("presencePenalty", "Presence penalty"); - createCheckbox("prepend", "Prepend sequence"); - createCheckbox("systemPrompt", "System prompt"); - createCheckbox("userMessage", "User message"); - } - + this.app.workspace.trigger( + "loom:set-visibility-setting", + id, + checkbox.checked + ) + ); + + const checkboxLabel = checkboxContainer.createEl("label", { + attr: { for: `loom__${id}-checkbox` }, + cls: "loom__visibility-item-label", + text: label, + }); + if (ellipsis && !settings.visibility.visibility) + checkboxLabel.createSpan({ + cls: "loom__no-metavisibility", + text: "...", + }); + }; + + createCheckbox("visibility", "These checkboxes", true); + if (settings.visibility["visibility"]) { + createCheckbox("modelPreset", "Model preset"); + createCheckbox("maxTokens", "Length"); + createCheckbox("n", "Number of completions"); + createCheckbox("bestOf", "Best of"); + createCheckbox("temperature", "Temperature"); + createCheckbox("topP", "Top p"); + createCheckbox("frequencyPenalty", "Frequency penalty"); + createCheckbox("presencePenalty", "Presence penalty"); + createCheckbox("prepend", "Prepend sequence"); + createCheckbox("systemPrompt", "System prompt"); + createCheckbox("userMessage", "User message"); + } + // preset dropdown - if (settings.visibility["modelPreset"]) { - const presetContainer = settingsContainer.createDiv({ cls: "loom__setting" }); - presetContainer.createEl("label", { text: "Model preset" }); - const presetDropdown = presetContainer.createEl("select"); + if (settings.visibility["modelPreset"]) { + const presetContainer = settingsContainer.createDiv({ + cls: "loom__setting", + }); + presetContainer.createEl("label", { text: "Model preset" }); + const presetDropdown = presetContainer.createEl("select"); if (settings.modelPresets.length === 0) - presetDropdown.createEl("option").createEl("i", { text: "[You have no presets. Go to Settings → Loom.]" }); + presetDropdown + .createEl("option") + .createEl("i", { + text: "[You have no presets. Go to Settings → Loom.]", + }); else { - for (const i in settings.modelPresets) { - const preset = settings.modelPresets[i]; - presetDropdown.createEl("option", { - text: preset.name, - attr: { selected: settings.modelPreset === parseInt(i) ? "" : null, value: i }, - }); - } - - presetDropdown.addEventListener("change", () => - this.app.workspace.trigger("loom:set-setting", "modelPreset", parseInt(presetDropdown.value)) - ); - } - } - - // other settings - - const setting = ( - label: string, - setting: string, - value: string, - type: "string" | "int" | "int?" | "float" - ) => { - if (!settings.visibility[setting]) return; + for (const i in settings.modelPresets) { + const preset = settings.modelPresets[i]; + presetDropdown.createEl("option", { + text: preset.name, + attr: { + selected: settings.modelPreset === parseInt(i) ? "" : null, + value: i, + }, + }); + } + + presetDropdown.addEventListener("change", () => + this.app.workspace.trigger( + "loom:set-setting", + "modelPreset", + parseInt(presetDropdown.value) + ) + ); + } + } + + // other settings + + const setting = ( + label: string, + setting: string, + value: string, + type: "string" | "int" | "int?" | "float" + ) => { + if (!settings.visibility[setting]) return; const parsers = { - "string": (value: string) => value, - "int": (value: string) => parseInt(value), - "int?": (value: string) => value === "" ? 0 : parseInt(value), - "float": (value: string) => parseFloat(value), - }; - - const settingContainer = settingsContainer.createDiv({ cls: "loom__setting" }); - settingContainer.createEl("label", { text: label }); - const settingInput = settingContainer.createEl("input", { - type: type === "string" ? "text" : "number", value - }); + string: (value: string) => value, + int: (value: string) => parseInt(value), + "int?": (value: string) => (value === "" ? 0 : parseInt(value)), + float: (value: string) => parseFloat(value), + }; + + const settingContainer = settingsContainer.createDiv({ + cls: "loom__setting", + }); + settingContainer.createEl("label", { text: label }); + const settingInput = settingContainer.createEl("input", { + type: type === "string" ? "text" : "number", + value, + }); settingInput.addEventListener("blur", () => - this.app.workspace.trigger( - "loom:set-setting", setting, parsers[type](settingInput.value) - ) - ); - } - - setting("Length (in tokens)", "maxTokens", String(settings.maxTokens), "int"); - setting("Number of completions", "n", String(settings.n), "int"); - setting("Best of", "bestOf", settings.bestOf === 0 ? "" : String(settings.bestOf), "int?"); - setting("Temperature", "temperature", String(settings.temperature), "float"); - setting("Top p", "topP", String(settings.topP), "float"); - setting("Frequency penalty", "frequencyPenalty", String(settings.frequencyPenalty), "float"); - setting("Presence penalty", "presencePenalty", String(settings.presencePenalty), "float"); - setting("Prepend sequence", "prepend", settings.prepend, "string"); - setting("System prompt", "systemPrompt", settings.systemPrompt, "string"); - setting("User message", "userMessage", settings.userMessage, "string"); + this.app.workspace.trigger( + "loom:set-setting", + setting, + parsers[type](settingInput.value) + ) + ); + }; + + setting( + "Length (in tokens)", + "maxTokens", + String(settings.maxTokens), + "int" + ); + setting("Number of completions", "n", String(settings.n), "int"); + setting( + "Best of", + "bestOf", + settings.bestOf === 0 ? "" : String(settings.bestOf), + "int?" + ); + setting( + "Temperature", + "temperature", + String(settings.temperature), + "float" + ); + setting("Top p", "topP", String(settings.topP), "float"); + setting( + "Frequency penalty", + "frequencyPenalty", + String(settings.frequencyPenalty), + "float" + ); + setting( + "Presence penalty", + "presencePenalty", + String(settings.presencePenalty), + "float" + ); + setting("Prepend sequence", "prepend", settings.prepend, "string"); + setting("System prompt", "systemPrompt", settings.systemPrompt, "string"); + setting("User message", "userMessage", settings.userMessage, "string"); } renderBookmarks(container: HTMLElement, state: NoteState) { - const bookmarks = Object.entries(state.nodes).filter(([, node]) => node.bookmarked); + const bookmarks = Object.entries(state.nodes).filter( + ([, node]) => node.bookmarked + ); const bookmarksContainer = container.createDiv({ cls: "loom__bookmarks" }); - const bookmarksHeader = bookmarksContainer.createDiv({ - cls: "tree-item-self loom__tree-header" - }); + const bookmarksHeader = bookmarksContainer.createDiv({ + cls: "tree-item-self loom__tree-header", + }); + bookmarksHeader.createSpan({ + cls: "tree-item-inner loom__tree-header-text", + text: "Bookmarks", + }); bookmarksHeader.createSpan({ - cls: "tree-item-inner loom__tree-header-text", text: "Bookmarks" - }); - bookmarksHeader.createSpan({ - cls: "tree-item-flair-outer loom__bookmarks-count", - text: String(bookmarks.length) - }); - - for (const [id,] of bookmarks) - this.renderNode(bookmarksContainer, state, id, false); + cls: "tree-item-flair-outer loom__bookmarks-count", + text: String(bookmarks.length), + }); + + for (const [id] of bookmarks) + this.renderNode(bookmarksContainer, state, id, false); } renderTree(container: HTMLElement, state: NoteState) { container.empty(); const treeHeader = container.createDiv({ - cls: "tree-item-self loom__tree-header" - }); - let headerText; - if (state.searchTerm) { - if (state.hoisted.length > 0) headerText = "Search results under hoisted node"; + cls: "tree-item-self loom__tree-header", + }); + let headerText; + if (state.searchTerm) { + if (state.hoisted.length > 0) + headerText = "Search results under hoisted node"; else headerText = "Search results"; - } else if (state.hoisted.length > 0) headerText = "Hoisted node"; - else headerText = "All nodes"; - treeHeader.createSpan({ - cls: "tree-item-inner loom__tree-header-text", - text: headerText, - }); - - if (state.hoisted.length > 0) - this.renderNode(container, state, state.hoisted[state.hoisted.length - 1], true); + } else if (state.hoisted.length > 0) headerText = "Hoisted node"; + else headerText = "All nodes"; + treeHeader.createSpan({ + cls: "tree-item-inner loom__tree-header-text", + text: headerText, + }); + + if (state.hoisted.length > 0) + this.renderNode( + container, + state, + state.hoisted[state.hoisted.length - 1], + true + ); else { const rootIds = Object.entries(state.nodes) - .filter(([, node]) => node.parentId === null) - .map(([id]) => id); - for (const rootId of rootIds) - this.renderNode(container, state, rootId, true); - } + .filter(([, node]) => node.parentId === null) + .map(([id]) => id); + for (const rootId of rootIds) + this.renderNode(container, state, rootId, true); + } } renderNode( - container: HTMLElement, - state: NoteState, - id: string, - inTree: boolean + container: HTMLElement, + state: NoteState, + id: string, + inTree: boolean ) { - const node = state.nodes[id]; + const node = state.nodes[id]; - if (inTree && node.searchResultState === "none") return; + if (inTree && node.searchResultState === "none") return; - const branchContainer = container.createDiv({}); + const branchContainer = container.createDiv({}); const nodeContainer = branchContainer.createDiv({ - cls: "is-clickable outgoing-link-item tree-item-self loom__node", - attr: { id: inTree ? `loom__node-${id}` : null }, - }); - if (id === state.current) nodeContainer.addClass("is-active"); - if (node.searchResultState === "result") - nodeContainer.addClass("loom__node-search-result"); - if (node.unread) nodeContainer.addClass("loom__node-unread"); - - const children = Object.entries(state.nodes) - .filter(([, node]) => node.parentId === id) - .map(([id]) => id); - - // if the node has children, add an expand/collapse button - - if (inTree && children.length > 0) { - const collapseButton = nodeContainer.createDiv({ - cls: "collapse-icon loom__collapse-button" - }); - if (node.collapsed) collapseButton.addClass("loom__is-collapsed"); - setIcon(collapseButton, "right-triangle"); - - collapseButton.addEventListener("click", () => - this.app.workspace.trigger("loom:toggle-collapse", id) - ); - } - - // if the node is bookmarked, add a bookmark icon - - if (node.bookmarked) { - const bookmarkIcon = nodeContainer.createDiv({ cls: "loom__node-bookmark-icon" }); - setIcon(bookmarkIcon, "bookmark"); - } - - // if the node is unread, add an unread indicator - - if (node.unread) nodeContainer.createDiv({ cls: "loom__node-unread-indicator" }); - - // add the node's text - - const nodeText = nodeContainer.createEl(node.text.trim() ? "span" : "em", { + cls: "is-clickable outgoing-link-item tree-item-self loom__node", + attr: { id: inTree ? `loom__node-${id}` : null }, + }); + if (id === state.current) nodeContainer.addClass("is-active"); + if (node.searchResultState === "result") + nodeContainer.addClass("loom__node-search-result"); + if (node.unread) nodeContainer.addClass("loom__node-unread"); + + const children = Object.entries(state.nodes) + .filter(([, node]) => node.parentId === id) + .map(([id]) => id); + + // if the node has children, add an expand/collapse button + + if (inTree && children.length > 0) { + const collapseButton = nodeContainer.createDiv({ + cls: "collapse-icon loom__collapse-button", + }); + if (node.collapsed) collapseButton.addClass("loom__is-collapsed"); + setIcon(collapseButton, "right-triangle"); + + collapseButton.addEventListener("click", () => + this.app.workspace.trigger("loom:toggle-collapse", id) + ); + } + + // if the node is bookmarked, add a bookmark icon + + if (node.bookmarked) { + const bookmarkIcon = nodeContainer.createDiv({ + cls: "loom__node-bookmark-icon", + }); + setIcon(bookmarkIcon, "bookmark"); + } + + // if the node is unread, add an unread indicator + + if (node.unread) + nodeContainer.createDiv({ cls: "loom__node-unread-indicator" }); + + // add the node's text + + const nodeText = nodeContainer.createEl(node.text.trim() ? "span" : "em", { cls: "tree-item-inner loom__node-text", - text: node.text.trim() || "No text", - }); - nodeText.addEventListener("click", () => - this.app.workspace.trigger("loom:switch-to", id) - ); - - const rootNodes = Object.entries(state.nodes) - .filter(([, node]) => node.parentId === null) - const deletable = rootNodes.length !== 1 || rootNodes[0][0] !== id; - - const nodeContext: NodeContext = { app: this.app, state, id, node, deletable }; - - nodeContainer.addEventListener("contextmenu", (event) => { - event.preventDefault(); - showNodeMenu(event, nodeContext); - }); - - // add buttons on hover - - const nodeButtonsContainer = nodeContainer.createDiv({ - cls: "loom__node-buttons" - }); - - renderNodeButtons(nodeButtonsContainer, nodeContext); - - // indicate if loom is currently generating children for this node - - if (inTree && state.generating === id) { - const generatingContainer = branchContainer.createDiv({ - cls: "loom__node-footer" - }); - const generatingIcon = generatingContainer.createDiv({ - cls: "loom__node-generating-icon" - }); - setIcon(generatingIcon, "loader-2"); - generatingContainer.createSpan({ - cls: "loom__node-footer-text", - text: "Generating..." - }); - } - - // if in a tree, and if the node isn't collapsed, render its children - - if (!inTree || node.collapsed) return; - - if (branchContainer.offsetWidth < 150) { + text: node.text.trim() || "No text", + }); + nodeText.addEventListener("click", () => + this.app.workspace.trigger("loom:switch-to", id) + ); + + const rootNodes = Object.entries(state.nodes).filter( + ([, node]) => node.parentId === null + ); + const deletable = rootNodes.length !== 1 || rootNodes[0][0] !== id; + + const nodeContext: NodeContext = { + app: this.app, + state, + id, + node, + deletable, + }; + + nodeContainer.addEventListener("contextmenu", (event) => { + event.preventDefault(); + showNodeMenu(event, nodeContext); + }); + + // add buttons on hover + + const nodeButtonsContainer = nodeContainer.createDiv({ + cls: "loom__node-buttons", + }); + + renderNodeButtons(nodeButtonsContainer, nodeContext); + + // indicate if loom is currently generating children for this node + + if (inTree && state.generating === id) { + const generatingContainer = branchContainer.createDiv({ + cls: "loom__node-footer", + }); + const generatingIcon = generatingContainer.createDiv({ + cls: "loom__node-generating-icon", + }); + setIcon(generatingIcon, "loader-2"); + generatingContainer.createSpan({ + cls: "loom__node-footer-text", + text: "Generating...", + }); + } + + // if in a tree, and if the node isn't collapsed, render its children + + if (!inTree || node.collapsed) return; + + if (branchContainer.offsetWidth < 150) { if (children.length > 0) { const showMore = branchContainer.createDiv({ - cls: "loom__node-footer loom__node-show-more" - }); - setIcon(showMore, "arrow-up"); - showMore.createSpan({ - cls: "loom__node-footer-text", - text: "Show more...", - }); - - showMore.addEventListener("click", () => - this.app.workspace.trigger("loom:hoist", id) - ); - } - - return; - } - - const childrenContainer = branchContainer.createDiv({ - cls: "loom__node-children" - }); - for (const childId of children) - this.renderNode(childrenContainer, state, childId, true); + cls: "loom__node-footer loom__node-show-more", + }); + setIcon(showMore, "arrow-up"); + showMore.createSpan({ + cls: "loom__node-footer-text", + text: "Show more...", + }); + + showMore.addEventListener("click", () => + this.app.workspace.trigger("loom:hoist", id) + ); + } + + return; + } + + const childrenContainer = branchContainer.createDiv({ + cls: "loom__node-children", + }); + for (const childId of children) + this.renderNode(childrenContainer, state, childId, true); } - + getViewType(): string { return "loom"; } @@ -567,88 +688,95 @@ export class LoomSiblingsView extends ItemView { getNoteState: () => NoteState | null; constructor(leaf: WorkspaceLeaf, getNoteState: () => NoteState | null) { - super(leaf); - this.getNoteState = getNoteState; - this.render(); + super(leaf); + this.getNoteState = getNoteState; + this.render(); } render() { const scroll = this.containerEl.scrollTop; - this.containerEl.empty(); - this.containerEl.addClass("loom__view"); - const container = this.containerEl.createDiv({ cls: "outline" }); - - const state = this.getNoteState(); - - if (state === null) { - container.createDiv({ - cls: "pane-empty", - text: "No note selected.", - }); - return; - } - - const parentId = state.nodes[state.current].parentId; - const siblings = Object.entries(state.nodes).filter( - ([, node]) => node.parentId === parentId - ); - - let currentNodeContainer = null; - for (const i in siblings) { - const [id, node] = siblings[i]; - - const nodeContainer = container.createDiv({ - cls: `loom__sibling${id === state.current ? " is-active" : ""}`, - }); - if (parentId !== null) - nodeContainer.createSpan({ - text: "…", - cls: "loom__sibling-ellipsis", - }); - nodeContainer.createSpan({ text: node.text.trim() }); - nodeContainer.addEventListener("click", () => - this.app.workspace.trigger("loom:switch-to", id) - ); + this.containerEl.empty(); + this.containerEl.addClass("loom__view"); + const container = this.containerEl.createDiv({ cls: "outline" }); + + const state = this.getNoteState(); + + if (state === null) { + container.createDiv({ + cls: "pane-empty", + text: "No note selected.", + }); + return; + } + + const parentId = state.nodes[state.current].parentId; + const siblings = Object.entries(state.nodes).filter( + ([, node]) => node.parentId === parentId + ); - const rootNodes = Object.entries(state.nodes) - .filter(([, node]) => node.parentId === null) - const deletable = rootNodes.length !== 1 || rootNodes[0][0] !== id; + let currentNodeContainer = null; + for (const i in siblings) { + const [id, node] = siblings[i]; - const nodeContext: NodeContext = { app: this.app, state, id, node, deletable }; + const nodeContainer = container.createDiv({ + cls: `loom__sibling${id === state.current ? " is-active" : ""}`, + }); + if (parentId !== null) + nodeContainer.createSpan({ + text: "…", + cls: "loom__sibling-ellipsis", + }); + nodeContainer.createSpan({ text: node.text.trim() }); + nodeContainer.addEventListener("click", () => + this.app.workspace.trigger("loom:switch-to", id) + ); - const nodeButtonsContainer = nodeContainer.createDiv({ - cls: "loom__sibling-buttons" - }); - renderNodeButtons(nodeButtonsContainer, nodeContext); + const rootNodes = Object.entries(state.nodes).filter( + ([, node]) => node.parentId === null + ); + const deletable = rootNodes.length !== 1 || rootNodes[0][0] !== id; + + const nodeContext: NodeContext = { + app: this.app, + state, + id, + node, + deletable, + }; + + const nodeButtonsContainer = nodeContainer.createDiv({ + cls: "loom__sibling-buttons", + }); + renderNodeButtons(nodeButtonsContainer, nodeContext); - nodeContainer.addEventListener("contextmenu", (event) => { - event.preventDefault(); - showNodeMenu(event, nodeContext); - }); + nodeContainer.addEventListener("contextmenu", (event) => { + event.preventDefault(); + showNodeMenu(event, nodeContext); + }); - if (parseInt(i) !== siblings.length - 1) - container.createEl("hr", { cls: "loom__sibling-separator" }); + if (parseInt(i) !== siblings.length - 1) + container.createEl("hr", { cls: "loom__sibling-separator" }); - if (id === state.current) currentNodeContainer = nodeContainer; - } + if (id === state.current) currentNodeContainer = nodeContainer; + } - this.containerEl.scrollTop = scroll; + this.containerEl.scrollTop = scroll; - if (currentNodeContainer !== null) - currentNodeContainer.scrollIntoView({ block: "nearest" }); + if (currentNodeContainer !== null) + currentNodeContainer.scrollIntoView({ block: "nearest" }); } getViewType(): string { - return "loom-siblings"; + return "loom-siblings"; } getDisplayText(): string { - return "Siblings"; + return "Siblings"; } getIcon(): string { - return "layout-list"; + return "layout-list"; } } @@ -663,51 +791,51 @@ export class LoomEditorPlugin implements PluginValue { constructor() { this.decorations = Decoration.none; - this.state = { ancestorLengths: [], showNodeBorders: false }; + this.state = { ancestorLengths: [], showNodeBorders: false }; } update() { let decorations: Range[] = []; - const addRange = (from: number, to: number, id: string) => { - try { - const range = Decoration.mark({ + const addRange = (from: number, to: number, id: string) => { + try { + const range = Decoration.mark({ class: `loom__editor-node loom__editor-node-${id}`, - }).range(from, to); - decorations.push(range); - } catch (e) { - // this happens if the range is empty. it's ok. it's fine, - } - }; - - const addBorder = (at: number) => { - const range = Decoration.widget({ - widget: new LoomNodeBorderWidget(), - side: -1, - }).range(at, at); - decorations.push(range); - }; - - let i = 0; - for (const [id, length] of this.state.ancestorLengths) { - addRange(i, i + length, id); - i += length; - if (this.state.showNodeBorders) addBorder(i); - } - - this.decorations = Decoration.set(decorations); + }).range(from, to); + decorations.push(range); + } catch (e) { + // this happens if the range is empty. it's ok. it's fine, + } + }; + + const addBorder = (at: number) => { + const range = Decoration.widget({ + widget: new LoomNodeBorderWidget(), + side: -1, + }).range(at, at); + decorations.push(range); + }; + + let i = 0; + for (const [id, length] of this.state.ancestorLengths) { + addRange(i, i + length, id); + i += length; + if (this.state.showNodeBorders) addBorder(i); + } + + this.decorations = Decoration.set(decorations); } } class LoomNodeBorderWidget extends WidgetType { toDOM() { - const el = document.createElement("span"); - el.classList.add("loom__editor-node-border"); - return el; + const el = document.createElement("span"); + el.classList.add("loom__editor-node-border"); + return el; } eq() { - return true; + return true; } } @@ -762,117 +890,128 @@ export class MakePromptFromPassagesModal extends Modal { getSettings: () => LoomSettings; constructor(app: App, getSettings: () => LoomSettings) { - super(app); - this.getSettings = getSettings; + super(app); + this.getSettings = getSettings; } onOpen() { - this.contentEl.createDiv({ - cls: "modal-title", - text: "Make prompt from passages", - }); - - const pathPrefix = this.getSettings().passageFolder.trim().replace(/\/?$/, "/"); - const passages = this.app.vault.getFiles().filter((file) => - file.path.startsWith(pathPrefix) && file.extension === "md" - ).sort((a, b) => b.stat.mtime - a.stat.mtime); - - let selectedPassages: string[] = []; - - const unselectedContainer = this.contentEl.createDiv({ - cls: "loom__passage-list", - }); - this.contentEl.createDiv({ - cls: "loom__selected-passages-title", - text: "Selected passages", - }); - const selectedContainer = this.contentEl.createDiv({ - cls: "loom__passage-list loom__selected-passage-list", - }); - let button: HTMLElement; - - const cleanName = (name: string) => name.slice(pathPrefix.length, -3); - - const renderPassageList = () => { - unselectedContainer.empty(); - selectedContainer.empty(); - - const unselectedPassages = passages.filter( - (passage) => !selectedPassages.includes(passage.path) - ); - - for (const passage of unselectedPassages) { + this.contentEl.createDiv({ + cls: "modal-title", + text: "Make prompt from passages", + }); + + const pathPrefix = this.getSettings() + .passageFolder.trim() + .replace(/\/?$/, "/"); + const passages = this.app.vault + .getFiles() + .filter( + (file) => file.path.startsWith(pathPrefix) && file.extension === "md" + ) + .sort((a, b) => b.stat.mtime - a.stat.mtime); + + let selectedPassages: string[] = []; + + const unselectedContainer = this.contentEl.createDiv({ + cls: "loom__passage-list", + }); + this.contentEl.createDiv({ + cls: "loom__selected-passages-title", + text: "Selected passages", + }); + const selectedContainer = this.contentEl.createDiv({ + cls: "loom__passage-list loom__selected-passage-list", + }); + let button: HTMLElement; + + const cleanName = (name: string) => name.slice(pathPrefix.length, -3); + + const renderPassageList = () => { + unselectedContainer.empty(); + selectedContainer.empty(); + + const unselectedPassages = passages.filter( + (passage) => !selectedPassages.includes(passage.path) + ); + + for (const passage of unselectedPassages) { const passageContainer = unselectedContainer.createDiv({ - cls: "tree-item-self loom__passage" - }); - passageContainer.createSpan({ - cls: "tree-item-inner", - text: cleanName(passage.path), - }); + cls: "tree-item-self loom__passage", + }); + passageContainer.createSpan({ + cls: "tree-item-inner", + text: cleanName(passage.path), + }); passageContainer.addEventListener("click", () => { - selectedPassages.push(passage.path) - renderPassageList(); - }); - } + selectedPassages.push(passage.path); + renderPassageList(); + }); + } if (selectedPassages.length === 0) { - selectedContainer.createDiv({ - cls: "loom__no-passages-selected", - text: "No passages selected.", - }); - } - for (const passage of selectedPassages) { - const passageContainer = selectedContainer.createDiv({ - cls: "tree-item-self loom__passage", - }); - passageContainer.createSpan({ - cls: "tree-item-inner", - text: cleanName(passage), - }); - passageContainer.addEventListener("click", () => { - selectedPassages = selectedPassages.filter((p) => p !== passage); - renderPassageList(); - }); - } - }; - - let separator = this.getSettings().defaultPassageSeparator; - let passageFrontmatter = this.getSettings().defaultPassageFrontmatter; + selectedContainer.createDiv({ + cls: "loom__no-passages-selected", + text: "No passages selected.", + }); + } + for (const passage of selectedPassages) { + const passageContainer = selectedContainer.createDiv({ + cls: "tree-item-self loom__passage", + }); + passageContainer.createSpan({ + cls: "tree-item-inner", + text: cleanName(passage), + }); + passageContainer.addEventListener("click", () => { + selectedPassages = selectedPassages.filter((p) => p !== passage); + renderPassageList(); + }); + } + }; + + let separator = this.getSettings().defaultPassageSeparator; + let passageFrontmatter = this.getSettings().defaultPassageFrontmatter; new Setting(this.contentEl) - .setName("Separator") - .setDesc("Use \\n to denote a newline.") - .addText((text) => - text.setValue(separator).onChange((value) => (separator = value))); - new Setting(this.contentEl) - .setName("Passage frontmatter") - .setDesc("This will be added before each passage and at the end. %n: 1, 2, 3..., %r: I, II, III...") - .addText((text) => - text.setValue(passageFrontmatter).onChange((value) => (passageFrontmatter = value))); - - const buttonContainer = this.contentEl.createDiv({ - cls: "modal-button-container", - }); - button = buttonContainer.createEl("button", { - cls: "mod-cta", - text: "Submit", - }); - button.addEventListener("click", () => { - if (selectedPassages.length === 0) return; - - this.app.workspace.trigger( - "loom:make-prompt-from-passages", - selectedPassages, - separator, - passageFrontmatter, - ); - this.close(); - }); - - renderPassageList(); + .setName("Separator") + .setDesc("Use \\n to denote a newline.") + .addText((text) => + text.setValue(separator).onChange((value) => (separator = value)) + ); + new Setting(this.contentEl) + .setName("Passage frontmatter") + .setDesc( + "This will be added before each passage and at the end. %n: 1, 2, 3..., %r: I, II, III..." + ) + .addText((text) => + text + .setValue(passageFrontmatter) + .onChange((value) => (passageFrontmatter = value)) + ); + + const buttonContainer = this.contentEl.createDiv({ + cls: "modal-button-container", + }); + button = buttonContainer.createEl("button", { + cls: "mod-cta", + text: "Submit", + }); + button.addEventListener("click", () => { + if (selectedPassages.length === 0) return; + + this.app.workspace.trigger( + "loom:make-prompt-from-passages", + selectedPassages, + separator, + passageFrontmatter + ); + this.close(); + }); + + renderPassageList(); } onClose() { - this.contentEl.empty(); + this.contentEl.empty(); } }