From f3aed396f08145a51bb7822d8ed698b46991749f Mon Sep 17 00:00:00 2001 From: celeste Date: Mon, 3 Jul 2023 02:34:18 -0400 Subject: [PATCH] add passages --- common.ts | 4 ++ main.ts | 87 +++++++++++++++++++++++++++++++-- manifest.json | 2 +- package-lock.json | 16 +++++- package.json | 4 +- styles.css | 31 ++++++++++++ views.ts | 121 +++++++++++++++++++++++++++++++++++++++++++++- 7 files changed, 256 insertions(+), 9 deletions(-) diff --git a/common.ts b/common.ts index c30030b..143774f 100644 --- a/common.ts +++ b/common.ts @@ -14,6 +14,10 @@ export interface LoomSettings { ocpApiKey: string; ocpUrl: string; + passageFolder: string; + defaultPassageSeparator: string; + defaultPassageFrontmatter: string; + provider: Provider; model: string; maxTokens: number; diff --git a/main.ts b/main.ts index 2e6fe1c..7dc0c89 100644 --- a/main.ts +++ b/main.ts @@ -1,4 +1,10 @@ -import { LoomView, LoomSiblingsView, LoomEditorPlugin, loomEditorPluginSpec } from './views'; +import { + LoomView, + LoomSiblingsView, + LoomEditorPlugin, + loomEditorPluginSpec, + MakePromptFromPassagesModal, +} from './views'; import { PROVIDERS, Provider, LoomSettings, Node, NoteState } from './common'; import { @@ -23,6 +29,7 @@ import p50k from "gpt-tokenizer/esm/model/text-davinci-003"; import r50k from "gpt-tokenizer/esm/model/davinci"; import * as fs from "fs"; +import { toRoman } from "roman-numerals"; import { v4 as uuidv4 } from "uuid"; const untildify = require("untildify") as any; @@ -48,6 +55,10 @@ const DEFAULT_SETTINGS: LoomSettings = { ocpApiKey: "", ocpUrl: "", + passageFolder: "", + defaultPassageSeparator: "\\n\\n---\\n\\n", + defaultPassageFrontmatter: "%r:\\n", + provider: "ocp", model: "code-davinci-002", maxTokens: 60, @@ -494,6 +505,24 @@ export default class LoomPlugin extends Plugin { }), }); + const getState = () => this.withFile((file) => this.state[file.path]); + 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(); + } + }); + this.addCommand({ id: "open-pane", name: "Open Loom pane", @@ -518,9 +547,6 @@ export default class LoomPlugin extends Plugin { callback: () => this.wftsar((file) => (this.state[file.path].hoisted = [])), }); - const getState = () => this.withFile((file) => this.state[file.path]); - const getSettings = () => this.settings; - this.registerView( "loom", (leaf) => new LoomView(leaf, getState, getSettings) @@ -921,6 +947,46 @@ export default class LoomPlugin extends Plugin { ) ); + this.registerEvent( + this.app.workspace.on( + // @ts-expect-error + "loom:make-prompt-from-passages", + ( + passages: string[], + rawSeparator: string, + rawFrontmatter: string, + ) => this.wftsar((file) => { + const separator = rawSeparator.replace(/\\n/g, "\n"); + const frontmatter = (index: number) => rawFrontmatter + .replace(/%n/g, (index + 1).toString()) + .replace(/%r/g, toRoman(index + 1)) + .replace(/\\n/g, "\n"); + + const passageTexts = passages.map((passage, index) => { + return Object.entries(this.state[passage].nodes) + .filter(([, node]) => node.parentId === null) + .map(([, node]) => frontmatter(index) + node.text); + }); + const text = `${passageTexts.join(separator)}${separator}${frontmatter(passages.length)}`; + + const state = this.state[file.path]; + const currentNode = state.nodes[state.current]; + + let id; + if (currentNode.text === "" && currentNode.parentId === null) { + this.state[file.path].nodes[state.current].text = text; + id = state.current; + } else { + const [newId, newNode] = this.newNode(text, null); + this.state[file.path].nodes[newId] = newNode; + id = newId; + } + + this.app.workspace.trigger("loom:switch-to", id); + }) + ) + ); + const onFileOpen = (file: TFile) => { if (file.extension !== "md") return; @@ -1396,6 +1462,19 @@ class LoomSettingTab extends PluginSettingTab { idSetting("OpenAI organization ID", "openaiOrganization"); apiKeySetting("Azure", "azureApiKey") idSetting("Azure resource endpoint", "azureEndpoint"); + + 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"); idSetting("Model", "model"); intSetting("Length (in tokens)", "maxTokens"); diff --git a/manifest.json b/manifest.json index c7ca861..2c0e761 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "loom", "name": "Loom", - "version": "1.13.1", + "version": "1.14.0", "minAppVersion": "0.15.0", "description": "Loom in Obsidian", "author": "celeste", diff --git a/package-lock.json b/package-lock.json index 7027afd..c7ac377 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,21 +1,23 @@ { "name": "obsidian-loom", - "version": "1.12.2", + "version": "1.13.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "obsidian-loom", - "version": "1.12.2", + "version": "1.13.1", "license": "MIT", "dependencies": { "@codemirror/state": "^6.2.0", "@codemirror/view": "^6.9.3", "@types/lodash": "^4.14.191", + "@types/roman-numerals": "^0.3.0", "azure-openai": "^0.9.4", "cohere-ai": "^6.1.0", "gpt-tokenizer": "^2.1.1", "openai": "^3.2.0", + "roman-numerals": "^0.3.2", "untildify": "^4.0.0", "uuid": "^9.0.0" }, @@ -195,6 +197,11 @@ "integrity": "sha512-vzLe5NaNMjIE3mcddFVGlAXN1LEWueUsMsOJWaT6wWMJGyljHAWHznqfnKUQWGzu7TLPrGvWdNAsvQYW+C0xtw==", "dev": true }, + "node_modules/@types/roman-numerals": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@types/roman-numerals/-/roman-numerals-0.3.0.tgz", + "integrity": "sha512-sfO4vwDEH5hpm9GHQMkcJaRUWgcrlgJn9YLQv+6l9JBQk+Xe4nx9zfbHgS+3x1sMwZUnFc0sgTY0eAHr9Foqhw==" + }, "node_modules/@types/semver": { "version": "7.3.13", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", @@ -1655,6 +1662,11 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/roman-numerals": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/roman-numerals/-/roman-numerals-0.3.2.tgz", + "integrity": "sha512-aTmaKtxczT3K4ZhtJDauOZ6LY3e1xNDf2SBn7aRY/ErAtCCuVYuwNQbPuh+SWTjGmbg9p5Nlt8sJbQNkto8uyg==" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", diff --git a/package.json b/package.json index 2e4ae7a..ff61b7a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-loom", - "version": "1.13.1", + "version": "1.14.0", "description": "Loom in Obsidian", "main": "main.js", "scripts": { @@ -27,10 +27,12 @@ "@codemirror/state": "^6.2.0", "@codemirror/view": "^6.9.3", "@types/lodash": "^4.14.191", + "@types/roman-numerals": "^0.3.0", "azure-openai": "^0.9.4", "cohere-ai": "^6.1.0", "gpt-tokenizer": "^2.1.1", "openai": "^3.2.0", + "roman-numerals": "^0.3.2", "untildify": "^4.0.0", "uuid": "^9.0.0" } diff --git a/styles.css b/styles.css index 1e0d940..f67a0f7 100644 --- a/styles.css +++ b/styles.css @@ -252,6 +252,37 @@ body { border-right: 2px dashed #333; } +/* "make prompt from passages" modal */ + +.loom__passage-list { + max-height: 10em; + overflow-y: auto; +} + +.loom__passage { + display: flex; + align-items: center; + padding: 0.35em 0 0.35em 0.6em; +} +.loom__passage:hover { + background-color: var(--nav-item-background-active); +} + +.loom__selected-passages-title { + font-weight: bold; + margin: 1em 0 0.5em; +} + +.loom__selected-passage-list { + margin-bottom: 0.75em; +} + +.loom__no-passages-selected { + color: var(--text-faint); + font-size: var(--font-ui-small); + padding: 0.35em 0 0.35em 0.6em; +} + /* hidden -- must be at end */ .hidden { diff --git a/views.ts b/views.ts index ec54801..bd65fee 100644 --- a/views.ts +++ b/views.ts @@ -1,5 +1,5 @@ import { LoomSettings, NoteState } from "./common"; -import { ItemView, Menu, WorkspaceLeaf, setIcon } from "obsidian"; +import { App, ItemView, Menu, Modal, Setting, WorkspaceLeaf, setIcon } from "obsidian"; import { Range } from "@codemirror/state"; import { Decoration, @@ -636,3 +636,122 @@ export const loomEditorPluginSpec: PluginSpec = { }, }, }; + +export class MakePromptFromPassagesModal extends Modal { + getSettings: () => LoomSettings; + + constructor(app: App, getSettings: () => LoomSettings) { + super(app); + this.getSettings = getSettings; + } + + onOpen() { + this.contentEl.createDiv({ + cls: "modal-title", + text: "Make prompt from passages", + }); + + const pathPrefix = this.getSettings().passageFolder.trim().replace(/\/?$/, "/"); + const passages = this.app.vault.getFiles().filter((file) => + file.path.startsWith(pathPrefix) && file.extension === "md" + ).sort((a, b) => b.stat.mtime - a.stat.mtime); + + let selectedPassages: string[] = []; + + const unselectedContainer = this.contentEl.createDiv({ + cls: "loom__passage-list", + }); + this.contentEl.createDiv({ + cls: "loom__selected-passages-title", + text: "Selected passages", + }); + const selectedContainer = this.contentEl.createDiv({ + cls: "loom__passage-list loom__selected-passage-list", + }); + let button: HTMLElement; + + const cleanName = (name: string) => name.slice(pathPrefix.length, -3); + + const renderPassageList = () => { + unselectedContainer.empty(); + selectedContainer.empty(); + + const unselectedPassages = passages.filter( + (passage) => !selectedPassages.includes(passage.path) + ); + + for (const passage of unselectedPassages) { + const passageContainer = unselectedContainer.createDiv({ + cls: "tree-item-self loom__passage" + }); + passageContainer.createSpan({ + cls: "tree-item-inner", + text: cleanName(passage.path), + }); + passageContainer.addEventListener("click", () => { + selectedPassages.push(passage.path) + renderPassageList(); + }); + } + + if (selectedPassages.length === 0) { + selectedContainer.createDiv({ + cls: "loom__no-passages-selected", + text: "No passages selected.", + }); + } + for (const passage of selectedPassages) { + const passageContainer = selectedContainer.createDiv({ + cls: "tree-item-self loom__passage", + }); + passageContainer.createSpan({ + cls: "tree-item-inner", + text: cleanName(passage), + }); + passageContainer.addEventListener("click", () => { + selectedPassages = selectedPassages.filter((p) => p !== passage); + renderPassageList(); + }); + } + }; + + let separator = this.getSettings().defaultPassageSeparator; + let passageFrontmatter = this.getSettings().defaultPassageFrontmatter; + + new Setting(this.contentEl) + .setName("Separator") + .setDesc("Use \\n to denote a newline.") + .addText((text) => + text.setValue(separator).onChange((value) => (separator = value))); + new Setting(this.contentEl) + .setName("Passage frontmatter") + .setDesc("This will be added before each passage and at the end. %n: 1, 2, 3..., %r: I, II, III...") + .addText((text) => + text.setValue(passageFrontmatter).onChange((value) => (passageFrontmatter = value))); + + const buttonContainer = this.contentEl.createDiv({ + cls: "modal-button-container", + }); + button = buttonContainer.createEl("button", { + cls: "mod-cta", + text: "Submit", + }); + button.addEventListener("click", () => { + if (selectedPassages.length === 0) return; + + this.app.workspace.trigger( + "loom:make-prompt-from-passages", + selectedPassages, + separator, + passageFrontmatter, + ); + this.close(); + }); + + renderPassageList(); + } + + onClose() { + this.contentEl.empty(); + } +}