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: "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: "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;
+ // 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 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);
+ // // 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);
+ 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 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;
- }
+ // 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;
- }
- })();
- }
+ // 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];
+ // 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.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.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;
- }
+ 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);
+ 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;
- }
- }
+ 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;
- });
+ 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.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");
+ 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 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];
+ 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;
- }
+ 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.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";
+ 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"];
+ 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;
+ 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 selectPreset = (index: number) => {
+ this.plugin.settings.modelPreset = index;
+ this.plugin.save();
+ updatePresetFields();
+ updatePresetList();
+ };
const deletePreset = (index: number) => {
- this.plugin.settings.modelPresets.splice(index, 1);
- this.plugin.save();
+ 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);
- }
- };
+ 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);
- }
+ this.plugin.save();
+ selectPreset(this.plugin.settings.modelPresets.length - 1);
+ };
- const newPresetButtons = presetEditor.createDiv({ cls: "loom__new-preset-buttons" });
+ 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 || "",
+ 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();
+ presetFields.empty();
- if (this.plugin.settings.modelPreset === -1) {
- presetFields.createEl("p", { cls: "loom__no-preset-selected", text: "No preset selected." });
- return;
- }
+ 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("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("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("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("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();
- }),
- );
+ 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 (
+ ["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 (["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();
- }),
- );
- }
- }
+ 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);
+ 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)));
+ 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 });
+ 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) => {
+ 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();
- })
- );
- }
+ 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);
+ 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();
+ })
+ );
- 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);
+ return;
+ }
+ this.renderBookmarks(container, state);
+ this.tree = container.createDiv();
+ this.renderTree(this.tree, state);
- this.containerEl.scrollTop = scroll;
+ 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" });
- }
+ // 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);
- });
+ setIcon(importNavButton, "import");
- // 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");
+ importInput.addEventListener("change", () => {
+ // @ts-expect-error
+ const path = importInput.files?.[0].path;
+ if (path) this.app.workspace.trigger("loom:import", path);
+ });
- 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);
- });
- });
+ // 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");
+ 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);
- });
+ 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)
- );
+ 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: "...",
- });
- };
+ 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");
+ }
- 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 },
- });
- }
+ 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))
- );
- }
- }
+ 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;
+ // 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),
- };
+ 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
- });
+ 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)
- )
- );
- }
+ 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");
+ 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-flair-outer loom__bookmarks-count",
- text: String(bookmarks.length)
- });
+ 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);
+ 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,
- });
+ } 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);
+ 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");
+ 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);
+ 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 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");
+ 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", {
+ 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)
- );
+ 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 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);
- });
+ const nodeContext: NodeContext = {
+ app: this.app,
+ state,
+ id,
+ node,
+ deletable,
+ };
- // add buttons on hover
-
- const nodeButtonsContainer = nodeContainer.createDiv({
- cls: "loom__node-buttons"
- });
+ nodeContainer.addEventListener("contextmenu", (event) => {
+ event.preventDefault();
+ showNodeMenu(event, nodeContext);
+ });
- renderNodeButtons(nodeButtonsContainer, nodeContext);
-
- // indicate if loom is currently generating children for this node
+ // add buttons on hover
- 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
+ const nodeButtonsContainer = nodeContainer.createDiv({
+ cls: "loom__node-buttons",
+ });
- if (!inTree || node.collapsed) return;
+ renderNodeButtons(nodeButtonsContainer, nodeContext);
- if (branchContainer.offsetWidth < 150) {
+ // 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...",
- });
+ 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)
- );
- }
+ showMore.addEventListener("click", () =>
+ this.app.workspace.trigger("loom:hoist", id)
+ );
+ }
- return;
- }
+ return;
+ }
- const childrenContainer = branchContainer.createDiv({
- cls: "loom__node-children"
- });
- for (const childId of children)
- this.renderNode(childrenContainer, state, childId, true);
+ 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" });
+ this.containerEl.empty();
+ this.containerEl.addClass("loom__view");
+ const container = this.containerEl.createDiv({ cls: "outline" });
- const state = this.getNoteState();
+ 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
- );
+ if (state === null) {
+ container.createDiv({
+ cls: "pane-empty",
+ text: "No note selected.",
+ });
+ return;
+ }
- let currentNodeContainer = null;
- for (const i in siblings) {
- const [id, node] = siblings[i];
+ const parentId = state.nodes[state.current].parentId;
+ const siblings = Object.entries(state.nodes).filter(
+ ([, node]) => node.parentId === parentId
+ );
- const nodeContainer = container.createDiv({
- cls: `loom__sibling${id === state.current ? " is-active" : ""}`,
- });
- if (parentId !== null)
- nodeContainer.createSpan({
+ 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)
- );
+ cls: "loom__sibling-ellipsis",
+ });
+ nodeContainer.createSpan({ text: node.text.trim() });
+ nodeContainer.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 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 nodeContext: NodeContext = {
+ app: this.app,
+ state,
+ id,
+ node,
+ deletable,
+ };
- const nodeButtonsContainer = nodeContainer.createDiv({
- cls: "loom__sibling-buttons"
- });
- renderNodeButtons(nodeButtonsContainer, nodeContext);
+ 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,
- }
- };
+ }).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);
- };
+ 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);
- }
+ 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);
+ 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",
- });
+ 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);
+ 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[] = [];
+ 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 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 cleanName = (name: string) => name.slice(pathPrefix.length, -3);
- const renderPassageList = () => {
- unselectedContainer.empty();
- selectedContainer.empty();
+ const renderPassageList = () => {
+ unselectedContainer.empty();
+ selectedContainer.empty();
- const unselectedPassages = passages.filter(
- (passage) => !selectedPassages.includes(passage.path)
- );
+ const unselectedPassages = passages.filter(
+ (passage) => !selectedPassages.includes(passage.path)
+ );
- for (const passage of unselectedPassages) {
+ 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();
- });
- }
- };
+ 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;
+ 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)));
+ .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;
+ 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();
- });
+ this.app.workspace.trigger(
+ "loom:make-prompt-from-passages",
+ selectedPassages,
+ separator,
+ passageFrontmatter
+ );
+ this.close();
+ });
- renderPassageList();
+ renderPassageList();
}
onClose() {
- this.contentEl.empty();
+ this.contentEl.empty();
}
}